Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2ad0eb7
Lift ESLint exemptions and fix lint drift
paullinator Aug 8, 2026
987a1cc
Serialize DeviceSettings init and writes
paullinator Aug 8, 2026
d456f5c
Split env.json into config and keys files
paullinator Aug 14, 2026
4d7a63c
Fetch Remote Secrets Via InfoRollup appKeys
paullinator Aug 14, 2026
1eaecde
Add native Edge API HMAC signer with edgeKey.json.
paullinator Aug 18, 2026
5707c51
Log appKeys LAYER sentinels and native HMAC signer status on launch.
paullinator Aug 27, 2026
568056f
Report plugins that failed to load after login.
paullinator Aug 31, 2026
01e6cdf
Declare the hash.js dependency that HMAC auth imports
paullinator Sep 4, 2026
d495c0b
Make network and utils Node-safe
paullinator Aug 7, 2026
91b8b6b
Split exchange rates into Node-safe core and GUI wire-up
paullinator Aug 7, 2026
4cc03b4
Repair the branch base so it compiles and tests clean
paullinator Sep 3, 2026
04f86c9
Extract Node-safe locale detection
paullinator Sep 3, 2026
de081aa
Extract Node-safe transaction display metadata
paullinator Sep 3, 2026
8090a64
Extract Node-safe denominations, spam threshold and local settings
paullinator Sep 3, 2026
ba230d4
Extract the Node-safe transaction export pipeline
paullinator Sep 3, 2026
a87c518
Extract Node-safe transaction tagging
paullinator Sep 3, 2026
a09f64d
Add the Edge CLI engine, its REST API, and the declaration format
paullinator Sep 3, 2026
fe0897d
Implement the remaining Edge CLI endpoints
paullinator Sep 3, 2026
c0c5f13
Build every usage string from one function
paullinator Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
env.json
config.json
keys.json
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ temp/
/android/app/google-services.json
/android/google-java-format-*.jar
/deploy-config.json
/edgeKey.json
/env.json
/config.json
/keys.json
/keys.*.json
/fastlane.json
/ios/edge/GoogleService-Info.plist
/ios/Pods/
Expand All @@ -17,6 +21,7 @@ IDEWorkspaceChecks.plist
android-release.bundle.map
ios-release.bundle.map
keystores/
/.edgeApiSigner.stamp

# Debugging
overrideTheme.json
Expand All @@ -35,6 +40,16 @@ coverage/
# Generated headers
/android/app/src/main/java/co/edgesecure/app/EdgeApiKey.java
/ios/EdgeApiKey.swift
/ios/EdgeApiSecret.c
/ios/EdgeApiSecret.h
/android/app/src/main/cpp/edge_api_secret.c
/android/app/src/main/cpp/edge_api_secret.h
/native/edge-api-signer/node/edge_api_secret.c
/native/edge-api-signer/node/edge_api_secret.h
/native/edge-api-signer/node/build/
/vendor/*.tgz
/vendor/edge-core-js-*.tgz
/*.tgz

# Checkpoint jsons
/android/app/src/main/assets/saplingtree/
Expand Down Expand Up @@ -128,3 +143,13 @@ yarn-error.log
!.yarn/sdks
!.yarn/versions
/.husky/_

# Edge CLI runtime
.edge-cli/

# Built CLI
lib/


# Maestro run output
/reports/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

## 4.51.0 (staging)

- added: Native Edge API HMAC signer (`edgeKey.json` + XOR-split C shards) so login-server requests can be signed outside the JS bundle via `apiSigner`, with JS `KEYS.EDGE_API_*` remaining as a fallback.
- added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks.
- added: Remote signed `GET /v1/infoRollup/:appId` `appKeys` fetch so plugin secrets can rotate without an app release, with DeviceSettings cache and baked-in `keys.json` fallback
- added: App/device attestation for gated info-server requests
- added: Swapter swap provider
- added: "-m" tag on the version number in the Help scene for Maestro test builds
Expand All @@ -14,6 +16,8 @@
- changed: Adopt the iOS UIScene lifecycle, removing the deprecated app-delegate window and lifecycle APIs ahead of Xcode 27.
- changed: Target Android 16 (API level 36), which Google Play requires for app updates submitted after Aug 30, 2026. Predictive back is opted out of for now, since React Native 0.79 cannot handle it, so the back button behaves exactly as it did before.
- changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade.
- changed: Split runtime `env.json` into non-secret `config.json` and secret `keys.json`; deploy-config branch overrides move from `envJson` to `configJson`/`keysJson` (legacy `envJson` is ignored on this GUI)
- changed: Replace the flat `ENV` singleton with separate `CONFIG`, `KEYS`/`globalKeys`, and `pluginMaps` accessors (no top-level globalKeys flatten)
- changed: Style the entire "Already have an account? Sign in" line in the getting-started USP carousel with the tertiary link color, not just "Sign in".
- changed: Refresh the buy, sell, sort, scan-QR and FIO names icons to the updated design.
- changed: Display "MoonPay" instead of "Moonpay" wherever the partner name appears in the app.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ This project uses npm to manage Javascript dependencies (npm ships with Node).

This bundler process needs to run in the background, so feel free to run this in its own terminal window.

### Add API key in env.json
### Add API key in keys.json

A public API key is built into the edge-core-js which can be used to build and test the Edge app. This key is severely rate limited and should not be used for production. For production use, get an API key by emailing info@edge.app.

Change the `AIRBITZ_API_KEY` in `env.json` to the API key you received from Edge. To use the public API key, leave `AIRBITZ_API_KEY` blank.
`npm run prepare` creates `config.json` (non-secret) and `keys.json` (secret) with defaults. Set `EDGE_API_KEY` in `keys.json` to the key you received from Edge. To use the public API key, leave `EDGE_API_KEY` blank. If you still have a legacy `env.json`, run `npm run split-env-json` once to produce the two files.

### Run the app in debug mode

Expand Down
52 changes: 52 additions & 0 deletions WIP_README_DELETE_ME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# WIP — delete this file before a production PR

This file records temporary state that exists only while `paul/cli` is in
progress. **None of it should reach a production pull request.** When the
blockers below clear, delete this file along with the workarounds it describes.

## This branch does not compile from a clean clone

`tsc` reports five errors, and `npm run precommit` therefore fails:

```
src/cli/engine/fetchPluginKeys.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner'
src/cli/engine/nodeApiSigner.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner'
src/util/edgeApiSigner.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner'
src/util/keysServer.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner'
src/components/services/EdgeCoreManager.tsx
Property 'apiSigner' does not exist on type 'EdgeContextOptions'
```

`package.json` asks for `edge-core-js@^2.48.1`, which is the newest published
version and does not export `EdgeApiSigner`. The five files above are all new
on this branch and all need it.

### Working around it

Pack `edge-core-js` from its own worktree and install the tarball here. The
`.tgz` files in the repository root are the packs already made for this — they
are gitignored, so they exist only on machines that built them.

```bash
npm install --no-save ./edge-core-js-<version>-<stamp>.tgz
```

`--no-save` is deliberate: pointing `package.json` at a gitignored tarball
would break the build for everyone else. That leaves `package.json` and
`node_modules` disagreeing, which is the whole reason this file exists.

### Clearing it

Either is enough, and both make this section obsolete:

- `edge-core-js` publishes a release exporting `EdgeApiSigner`, and
`package.json` moves to it; or
- the `apiSigner` work comes out of this branch and ships separately.

## Checklist before opening a production PR

- [ ] `EdgeApiSigner` resolves from a published `edge-core-js`
- [ ] `npx tsc --noEmit` is clean with no `--no-save` install
- [ ] `npm run precommit` passes from a fresh `npm ci`
- [ ] No `*.tgz` in the repository root
- [ ] Delete this file
65 changes: 65 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ android {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a' // Exclude Intel
}
externalNativeBuild {
cmake {
cppFlags ""
arguments "-DANDROID_STL=c++_shared"
}
}
}

externalNativeBuild {
cmake {
path file("src/main/cpp/CMakeLists.txt")
version "3.22.1"
}
}

// Edge addition: sideloadable per-ABI APKs for distribution outside
Expand Down Expand Up @@ -395,3 +408,55 @@ if (!sentrySlug.contains('SENTRY_ORGANIZATION')) {
telemetry = true
}
}

// Gradle daemons started from Android Studio inherit a minimal PATH that
// usually lacks nvm / Homebrew node, so reuse the NODE_BINARY that the React
// Native iOS build already depends on before falling back to a PATH lookup.
def resolveNodeBinary(File repoRoot) {
def pattern = ~'^\\s*export\\s+NODE_BINARY=(.+)$'
for (String name : ['ios/.xcode.env.local', 'ios/.xcode.env']) {
File file = new File(repoRoot, name)
if (!file.exists()) continue
for (String line : file.readLines()) {
def matcher = pattern.matcher(line)
if (!matcher.find()) continue
String value = matcher.group(1).trim().replaceAll('^["\']|["\']$', '')
// Skip `$(command -v node)` and friends: this is not a shell.
if (value.contains('$')) continue
if (new File(value).canExecute()) return value
}
}
return 'node'
}

// Regenerate XOR-split API secret C sources and EdgeApiKey.{swift,java} before
// every native build. Explicitly clear ALLOW_STUB so a stub from `npm prepare`
// cannot leak into the signer outputs.
def nodeBinary = resolveNodeBinary(rootProject.projectDir.parentFile)
tasks.register("generateEdgeApiSigner", Exec) {
def repoRoot = rootProject.projectDir.parentFile
workingDir repoRoot
environment "EDGE_API_SIGNER_ALLOW_STUB", ""
commandLine nodeBinary, "-r", "sucrase/register", "./scripts/makeApiSigner.ts"
}
// Same edgeKey.json feeds EdgeApiKey used by native push registration; keep it
// in lockstep with the signer so a key rotation cannot leave AppDelegate /
// MessagesWorker on the previous public key.
tasks.register("generateEdgeApiKeyHeaders", Exec) {
def repoRoot = rootProject.projectDir.parentFile
workingDir repoRoot
commandLine nodeBinary, "-r", "sucrase/register", "./scripts/makeNativeHeaders.ts"
}
generateEdgeApiKeyHeaders.dependsOn("generateEdgeApiSigner")
preBuild.dependsOn("generateEdgeApiKeyHeaders")

// edge_api_secret.c is gitignored but listed in CMakeLists.txt, and the CMake
// configure/build tasks do not run behind preBuild, so wire them up directly
// or a fresh checkout fails with "Cannot find source file".
tasks.matching {
it.name.startsWith("configureCMake") ||
it.name.startsWith("buildCMake") ||
it.name.startsWith("externalNativeBuild")
}.configureEach {
dependsOn("generateEdgeApiSigner")
}
27 changes: 27 additions & 0 deletions android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.18.1)
project(edge_api_signer)

set(NATIVE_SIGNER_DIR "${CMAKE_SOURCE_DIR}/../../../../../native/edge-api-signer")

add_library(
edge_api_signer
SHARED
edge_api_secret.c
edge_api_signer_jni.c
"${NATIVE_SIGNER_DIR}/edge_hmac.c"
)

target_include_directories(
edge_api_signer
PRIVATE
${CMAKE_SOURCE_DIR}
${NATIVE_SIGNER_DIR}
)

target_compile_options(edge_api_signer PRIVATE -fvisibility=hidden -O2)

# Pixel / Android 15+: 16 KB page-size ELF alignment
target_link_options(edge_api_signer PRIVATE "-Wl,-z,max-page-size=16384")

find_library(log-lib log)
target_link_libraries(edge_api_signer ${log-lib})
95 changes: 95 additions & 0 deletions android/app/src/main/cpp/edge_api_signer_jni.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "edge_api_sign.h"

static void throw_by_name(JNIEnv *env, const char *class_name, const char *msg) {
jclass ex = (*env)->FindClass(env, class_name);
if (ex != NULL) {
(*env)->ThrowNew(env, ex, msg);
}
}

static void throw_illegal_argument(JNIEnv *env, const char *msg) {
throw_by_name(env, "java/lang/IllegalArgumentException", msg);
}

static void throw_runtime(JNIEnv *env, const char *msg) {
throw_by_name(env, "java/lang/RuntimeException", msg);
}

JNIEXPORT jbyteArray JNICALL
Java_co_edgesecure_app_EdgeApiSignerModule_nativeSignMessage(
JNIEnv *env,
jobject thiz,
jbyteArray message_utf8,
jbyteArray package_name_utf8
) {
if (message_utf8 == NULL || package_name_utf8 == NULL) {
throw_illegal_argument(env, "messageUtf8 and packageNameUtf8 are required");
return NULL;
}

jsize msg_len = (*env)->GetArrayLength(env, message_utf8);
jbyte *msg_bytes = (*env)->GetByteArrayElements(env, message_utf8, NULL);
if (msg_bytes == NULL) return NULL;

jsize pkg_len = (*env)->GetArrayLength(env, package_name_utf8);
jbyte *pkg_bytes = (*env)->GetByteArrayElements(env, package_name_utf8, NULL);
if (pkg_bytes == NULL) {
(*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT);
return NULL;
}

/* edge_api_hmac_sign expects a C string bundle id (NUL-terminated). */
char *bundle_id = (char *)malloc((size_t)pkg_len + 1);
if (bundle_id == NULL) {
(*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, package_name_utf8, pkg_bytes, JNI_ABORT);
throw_by_name(env, "java/lang/OutOfMemoryError", "bundle id allocation failed");
return NULL;
}
memcpy(bundle_id, pkg_bytes, (size_t)pkg_len);
bundle_id[pkg_len] = '\0';

uint8_t signature[32];
int rc = edge_api_hmac_sign(
(const uint8_t *)msg_bytes,
(size_t)msg_len,
bundle_id,
signature
);
(*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, package_name_utf8, pkg_bytes, JNI_ABORT);
free(bundle_id);

if (rc != 0) {
throw_runtime(env, "edge_api_hmac_sign failed");
return NULL;
}

/* nativeSignMessage is declared non-null in Kotlin, so a bare NULL return
would surface as an NPE far from its cause. */
jbyteArray out = (*env)->NewByteArray(env, 32);
if (out == NULL) {
throw_by_name(env, "java/lang/OutOfMemoryError", "signature allocation failed");
return NULL;
}
(*env)->SetByteArrayRegion(env, out, 0, 32, (const jbyte *)signature);
memset(signature, 0, sizeof(signature));
return out;
}

JNIEXPORT jstring JNICALL
Java_co_edgesecure_app_EdgeApiSignerModule_nativeApiKey(
JNIEnv *env,
jobject thiz
) {
jstring out = (*env)->NewStringUTF(env, edge_api_key());
if (out == NULL) {
throw_runtime(env, "apiKey allocation failed");
}
return out;
}
Loading
Loading