Part of #457.
Description
Add geth-style V3 keystore JSON (scrypt + AES-128-CTR) as the recommended CLI auth mode, replacing PRIVATE_KEY as the documented default. The key sits encrypted on disk; the user is prompted for a passphrase at use time, or supplies KEYSTORE_PASSWORD for non-interactive runs.
synapse-sdk #554 already ships this pattern in its example CLI (examples/cli/src/{client,commands/init,config}.ts); port it into src/core/keystore/.
Scope is ordinary CLI commands only. The pinning server, upload action, and session owner commands are covered by the unify-auth-surfaces child of #457. Beneficiary note: this serves the human at the terminal, not agent runtimes; it is also the prerequisite for the OS-keychain child, which local assistant-spawned MCP tools need for headless unlock.
Scope
- CLI:
keystore create|import|list|remove, --keystore <name> flag, KEYSTORE_PASSWORD env. The named-account UX follows Foundry's cast wallet import pattern: enroll once, reference by name forever, never type hex again.
import accepts a hex private key or an existing V3 JSON file. Mnemonics out of scope.
- Files written
0600 under the platform data dir at keystores/<name>.json (same default dirs as the pinning server).
- Implementation: use
ox's Keystore module (viem's authors; viem itself has no keystore support) rather than pulling in ethers for fromEncryptedJson. scrypt at standard params takes seconds in JS; wire a progress indicator so keystore create does not look hung.
- Ethereum-standard scrypt parameters (N=262144, r=8, p=1) so files round-trip with geth, MetaMask, and other web3 tooling. Do not use Node's
crypto.scrypt defaults.
- Non-interactive behavior: detect via
stdin.isTTY && stdout.isTTY && !CI; when a passphrase prompt would be needed and the session is non-interactive, fail fast naming KEYSTORE_PASSWORD, never hang. Never fall back to reading key material from a plaintext file silently.
- Precedence: extend the source-aware resolution in
src/utils/cli-auth.ts (and its tests) with a full table covering keystore vs PRIVATE_KEY, session key, OWS, and read-only mode. Required behaviors: explicit --keystore beats env; PRIVATE_KEY in env beats an implicit default keystore; conflicting explicit sources error rather than silently pick.
- Default selection: with no flag and no
PRIVATE_KEY, auto-select a keystore only when exactly one exists; otherwise fail with a hint listing names.
- Docs: mark this the recommended interactive mode;
PRIVATE_KEY stays supported but demoted in examples.
Notes
The key still enters process memory at decrypt time; accepted tradeoff for this tier. What it eliminates: raw private keys in env vars, shell history, and CI logs. It is not secretless in CI: KEYSTORE_PASSWORD plus the keystore file is signing authority, so CI should prefer session keys (see #457) over keystore-plus-env-passphrase. Works in Node on all platforms with zero native dependencies.
Part of #457.
Description
Add geth-style V3 keystore JSON (scrypt + AES-128-CTR) as the recommended CLI auth mode, replacing
PRIVATE_KEYas the documented default. The key sits encrypted on disk; the user is prompted for a passphrase at use time, or suppliesKEYSTORE_PASSWORDfor non-interactive runs.synapse-sdk #554 already ships this pattern in its example CLI (
examples/cli/src/{client,commands/init,config}.ts); port it intosrc/core/keystore/.Scope is ordinary CLI commands only. The pinning server, upload action, and session owner commands are covered by the unify-auth-surfaces child of #457. Beneficiary note: this serves the human at the terminal, not agent runtimes; it is also the prerequisite for the OS-keychain child, which local assistant-spawned MCP tools need for headless unlock.
Scope
keystore create|import|list|remove,--keystore <name>flag,KEYSTORE_PASSWORDenv. The named-account UX follows Foundry'scast wallet importpattern: enroll once, reference by name forever, never type hex again.importaccepts a hex private key or an existing V3 JSON file. Mnemonics out of scope.0600under the platform data dir atkeystores/<name>.json(same default dirs as the pinning server).ox'sKeystoremodule (viem's authors; viem itself has no keystore support) rather than pulling in ethers forfromEncryptedJson. scrypt at standard params takes seconds in JS; wire a progress indicator sokeystore createdoes not look hung.crypto.scryptdefaults.stdin.isTTY && stdout.isTTY && !CI; when a passphrase prompt would be needed and the session is non-interactive, fail fast namingKEYSTORE_PASSWORD, never hang. Never fall back to reading key material from a plaintext file silently.src/utils/cli-auth.ts(and its tests) with a full table covering keystore vsPRIVATE_KEY, session key, OWS, and read-only mode. Required behaviors: explicit--keystorebeats env;PRIVATE_KEYin env beats an implicit default keystore; conflicting explicit sources error rather than silently pick.PRIVATE_KEY, auto-select a keystore only when exactly one exists; otherwise fail with a hint listing names.PRIVATE_KEYstays supported but demoted in examples.Notes
The key still enters process memory at decrypt time; accepted tradeoff for this tier. What it eliminates: raw private keys in env vars, shell history, and CI logs. It is not secretless in CI:
KEYSTORE_PASSWORDplus the keystore file is signing authority, so CI should prefer session keys (see #457) over keystore-plus-env-passphrase. Works in Node on all platforms with zero native dependencies.