Skip to content

Add CLI on top of config keys refactor - #6082

Draft
paullinator wants to merge 43 commits into
developfrom
paul/cli
Draft

Add CLI on top of config keys refactor#6082
paullinator wants to merge 43 commits into
developfrom
paul/cli

Conversation

@paullinator

@paullinator paullinator commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a Node-safe engine-based Edge CLI with JSON REST API (edge-cli / engine).
  • Makes shared network/utils and exchange-rate core Node-loadable for the CLI.
  • Includes native Edge API HMAC signing (mobile + Node N-API) and config/keys split work already on this branch vs develop.
  • Follow-up fixups address review-code findings (edge-login races, infoServer interval stacking, stub signer fail-closed, testMode config, etc.).

Notes for reviewers

  • This PR currently stacks several related stacks vs develop (config/keys, native HMAC, Node-safe splits, CLI). It is intentionally draft-style until dependencies / base strategy are finalized; there is no future! pseudo-merge in the history.
  • CLI publish (publish:cli) is a placeholder until packaging/bin metadata is restored.
  • Production Node HMAC addon must be built from edgeKey.json (build:cli:native); stub builds are refused.

Test plan

  • npm run test:cli:node-safe
  • npm run build:cli / npm run build:cli:native (with edgeKey.json)
  • npm run test:cli:node-hmac (with edgeKey.json)
  • npm run test:cli / npm run test:cli:edge-login as applicable
  • npm test / tsc

@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Medium
Low adoption: npm babel-plugin-transform-fake-error-class

Location: Package overview

From: package-lock.jsonnpm/babel-plugin-transform-fake-error-class@1.0.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/babel-plugin-transform-fake-error-class@1.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Low adoption: npm lib-cmdparse

Location: Package overview

From: package-lock.jsonnpm/lib-cmdparse@0.1.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/lib-cmdparse@0.1.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@paullinator
paullinator force-pushed the paul/cli branch 8 times, most recently from 285b97c to d1a82c6 Compare August 7, 2026 04:09
Separate env into config.json and keys.json, then load
plugin keys from the info server with attestation budget,
DeviceSettings cache, and baked-in fallback.
Keep the Edge API secret out of JS and keys.json by generating XOR-split C
sources from edgeKey.json at build time, bridging HMAC through native modules,
and deriving the runtime pad from post-patch applicationId / bundle ID.
Drop RN from network/utils load paths via fiatConstants, lazy
locale boot, injected initInfoServer params, and configureNetwork.
@paullinator
paullinator force-pushed the paul/cli branch 2 times, most recently from 2c77f58 to 67406a5 Compare August 8, 2026 06:47
Addresses review-code critical in src/util/network.ts: NetInfo
reconnect called initInfoServer again and stacked setInterval
pollers. Keep a single module-level interval handle so reconnect
only re-queries without stacking timers.
Keep exchangeRates on network.fetchRates and utils.removeIsoPrefix;
inject only Airship showError via exchangeRatesGui at app start.
Include a Node N-API Edge API HMAC signer for production embeds,
node-safe smoke in precommit, and CLI rollup build deps.
Addresses review-code critical in src/cli/engine/cliConfig.ts:
asCliConfig omitted testMode so cleaners stripped edge-cli.conf
testMode and left the engine on production servers. Add
asOptional(asBoolean) so file config matches CliConfig.
Addresses review-code criticals in src/cli/engine/routes/login.ts:
watch and GET raced sessions.create for the same pending login, and
DELETE left pending.watch live so a late done could create a
session after cancel. Serialize via sessionPromise, unwatch on
DELETE/expire, and refuse/create-then-logout when cancelled.
Also serialize auto-logout ticks so overlapping forceLogout cannot
run concurrently (sessions.ts warning).
Addresses review-code criticals on makeApiSigner/buildNodeApiSigner:
missing apiSecret no longer writes Node shards (and deletes prior
ones), keeps existing mobile shards unless --allow-stub, and
buildNodeApiSigner fails closed. Node shards always use the fixed
co.edgesecure.app pad so branded mobile ids cannot desync HMAC.
Addresses review-code warnings in makeCoreContext.ts: Buffer.from
of 0x-prefixed secrets yielded empty keys, and a present N-API
addon ignored -k/keys.json. Strip 0x before hex decode; skip the
native signer when -k or EDGE_CLI_FORCE_KEYS_JSON is set.
Addresses review-code warning that package.json publish:cli pointed
at a missing scripts/publishCli.ts. Add a stub that exits with a
clear message until the real publisher is restored.
Addresses review-code warning in envFiles.test.ts: the empty {}
ramp skip was stale—envSplit keeps empty stubs. Restore full
round-trip coverage so keep-empty regressions fail CI.
Addresses review-code warning in nodeApiSigner.ts: require()
failures for an existing .node were swallowed, hiding ABI/OS
mismatches. Log the path and error at warn level before trying
the next candidate.
Addresses review-code warning in solveCaptcha.ts: hung challenge
GETs/POSTs stalled --solve-captcha indefinitely. Set a 30s
req.setTimeout that destroys the socket so login retry fails cleanly.
Addresses review-code warning in src/cli/index.ts: key-login was
in loginCommands but retryLoginCommand fell through to a re-run
hint. POST /v1/login/key with username, loginKey, and challengeId
like the other login retries.
Addresses review-code warning in idleShutdown.ts: shuttingDown
stayed true after a failed onFire, so later idle fires no-op'd and
the engine never exited. Reset the flag, re-arm the timer, and log
timer-path errors instead of an empty catch.
Addresses review-code warning in objectHandles.ts: async sweep()
on setInterval could overlap slow onExpire handlers. Guard with
sweepInFlight so expiry cleanup cannot run concurrently.
Addresses review-code warning in keysConfig.ts: loadKeys caught all
errors and treated a present but invalid keys.json like a missing
file. Only skip ENOENT; throw on JSON/cleaner failures.
Addresses review-code suggestion in cliNodeSafeSmoke.js: include
nodeApiSigner.ts in SHARED_MODULES so RN leaks on the signer
loader path fail precommit.
paullinator and others added 24 commits August 8, 2026 00:25
Addresses review-code criticals and warnings on the engine's HTTP
listeners (src/cli/engine/server.ts, json.ts, discovery.ts).

The REST API had no authentication and explicitly accepted
text/plain, which is CORS-safelisted. Any web page the user visited
could therefore issue simple cross-site requests against an engine
started with --tcp and drive spends, enumerate sessions, or shut it
down. Now: requests carrying Origin or Sec-Fetch-Mode are refused,
the TCP listener requires a per-run bearer token (32 random bytes
recorded in the 0600 run file and sent by ApiClient), the Host
header is pinned to loopback plus the bind host to blunt DNS
rebinding, and only application/json bodies reach a handler. The
unix socket is already owner-only at 0600, so it carries no token.

readJsonBody buffered request bodies with no cap, so one large or
slow upload could exhaust engine memory and take live sessions down
with it. Bodies are now rejected at 4 MB (both by declared
Content-Length and while streaming) and both listeners set
headersTimeout/requestTimeout.

listenUnix unconditionally unlinked the socket before binding while
cleanupStaleLock only removed artifacts for dead pids, so starting a
second engine for the same profile silently stole the live socket
and left two EdgeContexts writing the same directory. cleanupStaleLock
now reports a live pid (treating EPERM as alive) and startup exits
with a clear message instead.

Also drops waitForPortFree, which was unreferenced and leaked a
socket on its error path.

Verified against a running engine: unauthenticated TCP 401, valid
token 200, bad token 401, Origin 403, rebound Host 403, text/plain
415, 5 MB body 413 with the connection closed, unix socket 200, and
a second engine for the same profile refuses to start while the
first stays reachable.
Addresses review-code warning on src/cli/engine/idleShutdown.ts: the
idle timer was disarmed for as long as an account was logged in, and
nothing re-armed it when the last session went away. IdleShutdown.reset
returns early while getSessionCount() > 0 and only ran again on the
next inbound request, but the logout itself calls touch() before the
session is removed. An engine whose user logged out therefore stayed
resident indefinitely, holding the profile lock and an EdgeContext.

SessionStore now reports membership changes through onSessionsChanged,
which the engine wires to IdleShutdown.notifySessionsChanged. Both the
request path and the auto-logout ticker path re-arm as a result. The
callback is invoked inside a try/catch so a listener can never break
login or logout.
Addresses three review-code findings around key resolution and
startup diagnostics.

loadKeys returned the first keys.json that parsed, so running from a
GUI checkout meant the repo-root keys.json — which carries 66 plugin
keys but no edgeApiKey — permanently shadowed ~/.edge-cli/keys.json.
The engine then fell back to an empty apiKey and failed at login with
an opaque server error. A file with no edgeApiKey is now kept only as
a fallback for its plugin keys and does not stop the search, and
makeCoreContext fails immediately, naming the searched paths, when it
has neither a native signer nor a key.

makeCoreContext also paired an explicit -k key with the keys.json
edgeApiSecret, signing every request with a secret belonging to a
different key. The secret is dropped when -k overrides the key.

ensureEngine spawned the engine with stdio 'ignore', so a child that
died during startup (unreadable keys.json, plugin load failure)
surfaced only as a 30-second timeout with no cause. Child output now
goes to engine-startup.log in the run directory and its tail is
included in the timeout error.
Addresses review-code warning in src/cli/engine/routes/admin.ts:
POST /v1/admin/lobby returned the lobby's id and current replies and
then dropped the only reference to the EdgeLobby. A lobby polls the
login server until closed, so every call leaked a timer that ran for
the remaining life of the engine.

The lobby is now parked in the object handle store under a new
'lobby' kind, with onExpire closing it. It is released by the normal
5-minute TTL sweep, by clearAll on shutdown, or immediately via
DELETE /v1/admin/lobby-handle/{objectId}. That path is distinct from
GET /v1/admin/lobby/{lobbyId}, which still takes the server-side
lobby id rather than an engine handle.
Addresses review-code warning in src/cli/engine/routes/spend.ts: the
combined spend route ran broadcastTx then saveTx, and a saveTx
rejection propagated as a 500. The transaction was already on the
network at that point, so the caller lost the txid of a real spend
and had no way to recover it from the response.

After a successful broadcast, a saveTx failure is now logged and
returned as a `saveError` field alongside the transaction. When the
caller asked for save without broadcast nothing has happened yet, so
that case still throws.
Documents the transport rules the engine now enforces, which the API
reference still described as unauthenticated.

Section 2 covers the TCP bearer token and where to read it, Host
header pinning, the blanket rejection of browser-originated requests,
the 4 MiB body cap, the captured startup log, and the
one-engine-per-profile rule. Section 7 gains the UNAUTHORIZED,
FORBIDDEN, and PAYLOAD_TOO_LARGE codes, and the run file example
gains tcpToken.

Also replaces /Users/paul in two sample payloads with /Users/you, so
the reference stops showing one developer's home directory.
Addresses review-code warnings on engine log and run-file handling.

EngineLogger created ~/.edge-cli/logs and its log files at the
default umask, so engine logs — which record usernames, login ids
and core diagnostics — were world-readable. The directory is now
0700 and log files 0600, and both are chmodded on open so files left
by an earlier run are tightened rather than trusted. writeRunFile
gets the same treatment because its `mode` option only applies when
creating, and that file now carries the TCP bearer token.

EngineLogger.close also returned before the write stream drained, so
the last lines before an idle or requested shutdown could be lost.
It now resolves once the stream has flushed, and shutdown awaits it.
Addresses review-code warning in src/cli/engine/events.ts: EventHub
wrote to every SSE response and ignored the return value of write,
so a subscriber that stopped reading — a paused terminal, a wedged
script — made Node buffer every subsequent event in the engine's
heap with no ceiling.

Clients that have ended or been destroyed are now dropped on the
next emit, and any client with more than 1 MiB still queued is
disconnected rather than buffered further. Events are best-effort
notifications, so dropping a stalled listener is preferable to
growing unboundedly on its behalf.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Align EDGE_CLI_API.md and EDGE_CLI.md with the real admin, voucher,
and object-handle routes. Removes invented paths (username-hash,
filename-hash, /admin/lobbies*, repos without dataKey) and documents
hash-username, lobby handles, repo delete, and objects GET/DELETE.
Cancel edge-login tears down an already-created session, and
ensureEdgeSession stops retrying after a sticky create error so GET
cannot wedge a failed pending login into a create loop.
Honor pluginApiKeys[id] === false (and enabled:false) for currency
plugins the same way accountbased plugins already did, so keys.json
can disable a currency plugin.
Validate bare --idle-timeout <n> the same as --idle-timeout=<n>, and
fail closed when no edge-engine entry file exists instead of spawning
a nonexistent default path.
Treat CAPTCHA HTTP bodies as utf8 Buffer chunks and require 2xx on the
challenge GET/POST so a non-HTML error page cannot look like success.
Clarify object-handle TTL is absolute from create/update (reads do not
slide it), and drop the redundant bare node_modules gitignore entry.
Move the module JSDoc above the imports so simple-import-sort and
readers see the Node-safe rates contract at the top of the file.
Reconcile CLI command names with the registry (engine-status etc.),
mark REST-only endpoints that have no edge-cli wrapper, and document
EDGE_CLI_FORCE_KEYS_JSON.
Bugbot: currency-configs listed swap plugins; GET /v1/users omitted the
users envelope; wallet-create and sign-bytes field names disagreed with
the REST docs. Filter to currency plugins, wrap localUsers, and accept
both documented and CLI field aliases.
Drop TCP bearer auth, Host allowlisting, and Origin/Sec-Fetch
rejection. The engine is a local open-source convenience daemon;
Edge account authentication remains on the login server.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant