@palmyr/cli 1.8.2 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,18 +112,28 @@ from a single 12-word mnemonic. Wallets are created locally; the seed never leav
112
112
 
113
113
  ### Creating a wallet
114
114
 
115
+ Wallet creation **requires** a recoverable passphrase fallback (the env var keeps the phrase out of shell history) — or an explicit `--session-only` opt-out for ephemeral wallets you're OK losing on reboot / keyring change / host migration.
116
+
115
117
  ```bash
116
- # Unmanaged: one command, ready to sign
117
- palmyr wallet create --name agent-prod
118
+ # Recommended env-var passphrase fallback (durable across reboots and machines)
119
+ PALMYR_WALLET_PASSPHRASE="your-passphrase" palmyr wallet create --name agent-prod
118
120
 
119
- # Managed: prints a setup link to send to a human
120
- palmyr wallet create --name treasury --managed
121
+ # Equivalent flag form (less safe, ends up in shell history)
122
+ palmyr wallet create --name agent-prod --passphrase "your-passphrase"
123
+
124
+ # Managed: same passphrase rules; also prints a setup link to send to a human
125
+ PALMYR_WALLET_PASSPHRASE="..." palmyr wallet create --name treasury --managed
121
126
 
122
127
  # Single-chain: skip the other chain's account
123
- palmyr wallet create --name sol-only --solana
124
- palmyr wallet create --name base-only --base
128
+ PALMYR_WALLET_PASSPHRASE="..." palmyr wallet create --name sol-only --solana
129
+ PALMYR_WALLET_PASSPHRASE="..." palmyr wallet create --name base-only --base
130
+
131
+ # OPT OUT — bound to this machine's OS keychain, NOT recoverable from the JSON file alone
132
+ palmyr wallet create --name throwaway --session-only
125
133
  ```
126
134
 
135
+ On a TTY without env or flag, the CLI prompts twice with confirmation; non-TTY callers (CI, agents) must provide one of the three knobs above or get a clear error.
136
+
127
137
  By default a wallet derives both Solana and Base/EVM accounts. Pass `--solana` or `--base` (not both) to materialize only one side. The mnemonic always derives both — `--solana` / `--base` controls *which addresses are surfaced and stored*, not which keys exist cryptographically.
128
138
 
129
139
  The managed flow returns a one-time URL. The recipient opens it in a browser, registers a WebAuthn passkey, and sets spending limits. From that point on, transactions inside the limit sign instantly; transactions over the limit emit an `approvalUrl` that the human visits to authenticate and approve.
@@ -162,7 +172,9 @@ palmyr wallet import --mnemonic "..." --name from-backup --tag restored --solana
162
172
 
163
173
  ### Durable recovery — passphrase fallback
164
174
 
165
- By default a wallet is **session-only**: the mnemonic is encrypted with a random session secret stored in your OS credential store (DPAPI / Keychain / `secret-tool`). That secret does not survive a different OS user, a fresh install, or a headless box without an unlocked keyring. Two ways to add a durable scrypt-derived passphrase that decrypts on any machine:
175
+ Wallets created with a passphrase store **two** decryption blobs: one keyed by the OS-keychain session secret (fast, local), one keyed by scrypt(passphrase, salt) (durable). Decryption tries the keychain first, then falls back to `PALMYR_WALLET_PASSPHRASE`. That second blob is what lets the wallet survive a reboot, OS-keychain password change, fresh OS install, or copy to another machine.
176
+
177
+ Session-only wallets (`--session-only`) **only** have the keychain blob and are NOT recoverable from the JSON file alone. If you have legacy session-only wallets and still have access to the original machine, add a fallback retroactively:
166
178
 
167
179
  ```bash
168
180
  # At create time — env var preferred (keeps the phrase out of shell history)
@@ -426,8 +438,8 @@ All wallet operations except `addresses`, `api-key`, `config`, and `request-appr
426
438
 
427
439
  | Command | Network | Notes |
428
440
  |---|---|---|
429
- | `palmyr wallet create [--name N] [--managed] [--solana\|--base] [--tag T] [--count N] [--name-prefix P] [--passphrase P]` | local *(server only if `--managed`)* | New wallet. Stores session secret in OS credential store. `--count > 1` bulk-creates N unmanaged wallets under a required `--tag` (max 500/call, batched DPAPI seal on Windows). `--solana` / `--base` materializes only one chain. `--passphrase` (or `PALMYR_WALLET_PASSPHRASE` env) also seals the mnemonic with scrypt so the wallet survives OS-keychain loss. |
430
- | `palmyr wallet import --mnemonic "..." [--name N] [--managed] [--solana\|--base] [--tag T] [--passphrase P]` | local | Restore from BIP-39. Same chain / tag / passphrase flags as `create`. |
441
+ | `palmyr wallet create [--name N] [--managed] [--solana\|--base] [--tag T] [--count N] [--name-prefix P] (--passphrase P \| PALMYR_WALLET_PASSPHRASE env \| --session-only)` | local *(server only if `--managed`)* | New wallet. **Requires** either a passphrase (recoverable across reboot / OS-keychain loss / host migration) or explicit `--session-only` opt-out. On TTY without env/flag, prompts twice. Stores keychain secret + (with passphrase) a scrypt-sealed `owner_crypto` blob. `--count > 1` bulk-creates N unmanaged wallets under a required `--tag` (max 500/call, batched DPAPI seal on Windows). |
442
+ | `palmyr wallet import --mnemonic "..." [--name N] [--managed] [--solana\|--base] [--tag T] (--passphrase P \| PALMYR_WALLET_PASSPHRASE env \| --session-only)` | local | Restore from BIP-39. Same passphrase rules as `create` — re-importing on a new machine to recover from keychain loss should always set a passphrase so you don't get trapped again. |
431
443
  | `palmyr wallet rekey <ID> --passphrase P` | local | Add (or rotate) the scrypt passphrase fallback on an existing wallet. Wallet must be decryptable right now (session secret still works, or `--current-passphrase` provided). Run on the original machine, then `PALMYR_WALLET_PASSPHRASE` decrypts the wallet anywhere. |
432
444
  | `palmyr wallet list [--tag T]` | local | Lists wallets in the local vault. `--tag` filters to one folder. |
433
445
  | `palmyr wallet info <ID>` | local | Show one wallet (id, name, addresses, mode, tag). |
@@ -803,10 +815,11 @@ Config is stored in `~/.palmyr/config.json`. Environment variables override file
803
815
 
804
816
  The payment chain is resolved in this order:
805
817
 
806
- 1. The `--chain` flag passed to `wallet use`, persisted as `defaultPayChain`.
807
- 2. `PALMYR_PAY_CHAIN` environment variable.
808
- 3. `defaultChain` in config.
809
- 4. `solana` as the final default.
818
+ 1. `--chain` flag on the command itself (e.g. `wallet pay-preflight --chain base`).
819
+ 2. `defaultPayChain` in `~/.palmyr/config.json` — set by `palmyr wallet use <ID> --chain <chain>`.
820
+ 3. `solana` as the final default.
821
+
822
+ (Earlier versions of this doc listed `PALMYR_PAY_CHAIN` and `defaultChain` as fallbacks — neither is read by the current pay path. `defaultChain` is legacy keyfile-flow state and `saveConfig` strips it from vault-only configs.)
810
823
 
811
824
  If the server doesn't offer the chosen chain for an endpoint, the CLI errors loudly. There is no silent fallback — the assumption is that an agent should know which chain it pays from.
812
825
 
@@ -856,7 +869,7 @@ The CLI uses distinct exit codes so scripts and agents can branch on failure mod
856
869
  - **Keys never leave the machine.** `wallet create` and `wallet import` never transmit seed material to the server. Even managed wallets register only the wallet ID and the derived public addresses with the server.
857
870
  - **Encryption at rest.** The wallet file is AES-256-GCM with the session secret as the key. The session secret is generated on wallet creation and never stored on disk in plaintext.
858
871
  - **OS credential store.** Session secrets live in DPAPI (Windows), Keychain (macOS), or `secret-tool` (libsecret on Linux). If none is available the CLI errors rather than falling back to plaintext.
859
- - **Optional passphrase fallback.** Create with `--passphrase` (or `PALMYR_WALLET_PASSPHRASE` env), or migrate with `wallet rekey`, to add a second AES-256-GCM blob keyed by scrypt(passphrase, random salt). Lets the wallet decrypt on a different machine / user / headless box where the OS keychain isn't reachable. Decryption tries the OS session secret first, then falls back to the env/flag passphrase.
872
+ - **Recoverable by default.** `wallet create` and `wallet import` require a scrypt passphrase fallback (via `--passphrase` or `PALMYR_WALLET_PASSPHRASE`) or an explicit `--session-only` opt-out. The passphrase blob is a second AES-256-GCM ciphertext keyed by scrypt(passphrase, random salt) and lets the wallet decrypt on a different machine / user / headless box where the OS keychain isn't reachable. Decryption tries the OS session secret first, then falls back to the env/flag passphrase. Session-only wallets are bound to this machine's OS keychain — explicit opt-in for ephemeral use only.
860
873
  - **Bidirectional vault integrity.** On every load, the CLI checks that every account stored in the wallet file is still derivable from the seed, **and** that every account derivable from the seed is still in the file. Either direction failing returns exit code `7`.
861
874
  - **Pre-flight validation.** Endpoints that are easy to fail (bad pubkey for an inbox, malformed E.164 for SMS, unsupported destination country) are validated **before** the x402 paywall, so you don't pay for requests that were never going to succeed.
862
875
  - **No `--no-verify`.** Hooks, signatures, and webhooks are always verified.
package/dist/cli.js CHANGED
@@ -209,6 +209,22 @@ function subcommandHelp(command, subcommand, options) {
209
209
  }
210
210
  console.log();
211
211
  }
212
+ /**
213
+ * Loud, single-shot warning printed when `--session-only` was chosen. Goes to
214
+ * stderr so JSON on stdout stays clean. Caller supplies the write fn so we
215
+ * route through the right stream in agent vs TTY mode.
216
+ *
217
+ * Why this is here: 1.8.2 and earlier defaulted to session-only without
218
+ * warning. A user lost three wallets to a routine keyring change on a
219
+ * headless box because the JSON file alone is mathematically useless without
220
+ * the keychain secret. 1.8.3 makes the choice explicit; this warning is the
221
+ * reminder for anyone who picks the foot-gun anyway.
222
+ */
223
+ function emitSessionOnlyWarning(write) {
224
+ write(`\n ${t.warn}⚠ session-only wallet — NOT recoverable from the JSON file alone.${t.reset}\n`);
225
+ write(` Reboot, OS-keychain password change, or host copy permanently breaks decryption.\n`);
226
+ write(` Back up the mnemonic externally, or run \`palmyr wallet rekey <id> --passphrase <p>\` later.\n\n`);
227
+ }
212
228
  // ─── Subcommand help definitions ───
213
229
  const WALLET_HELP = {
214
230
  create: [
@@ -219,7 +235,8 @@ const WALLET_HELP = {
219
235
  { flag: '--tag <name>', desc: 'Folder-like grouping tag', hint: 'e.g. palmyr-demo — required with --count' },
220
236
  { flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'unmanaged only; requires --tag' },
221
237
  { flag: '--name-prefix <p>', desc: 'Bulk name prefix; suffixed `-001..-N`', hint: 'default: same as --tag' },
222
- { flag: '--passphrase <p>', desc: 'Also seal the mnemonic with this passphrase (≥8 chars) for durable recovery', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred — keeps phrase out of shell history)' },
238
+ { flag: '--passphrase <p>', desc: 'Seal the mnemonic with this passphrase (≥8 chars) for durable recovery across reboot / OS-keychain loss / host migration', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred — keeps phrase out of shell history). Interactive prompt on TTY when neither set.' },
239
+ { flag: '--session-only', desc: 'OPT OUT of the passphrase fallback. Wallet is bound to this machine\'s OS keychain — dies on reboot/keyring loss/migration.', hint: 'use only for ephemeral / throwaway wallets where loss is acceptable' },
223
240
  ],
224
241
  import: [
225
242
  { flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
@@ -228,7 +245,8 @@ const WALLET_HELP = {
228
245
  { flag: '--solana', desc: 'Materialize the Solana account only' },
229
246
  { flag: '--base', desc: 'Materialize the Base/EVM account only' },
230
247
  { flag: '--tag <name>', desc: 'Assign a tag at import time' },
231
- { flag: '--passphrase <p>', desc: 'Also seal the mnemonic with this passphrase (≥8 chars) for durable recovery', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred)' },
248
+ { flag: '--passphrase <p>', desc: 'Seal the mnemonic with this passphrase (≥8 chars) for durable recovery', hint: 'or PALMYR_WALLET_PASSPHRASE env (env preferred). Interactive prompt on TTY when neither set.' },
249
+ { flag: '--session-only', desc: 'OPT OUT of the passphrase fallback. Wallet is bound to this machine\'s OS keychain.', hint: 'use only for ephemeral / throwaway wallets' },
232
250
  ],
233
251
  rekey: [
234
252
  { flag: '<WALLET_ID>', desc: 'Wallet ID or name (positional or --id)' },
@@ -2210,10 +2228,37 @@ async function main() {
2210
2228
  const chains = (wantSol && !wantBase) ? ['solana']
2211
2229
  : (wantBase && !wantSol) ? ['base']
2212
2230
  : ['solana', 'base'];
2213
- // Optional passphrase fallback seals the mnemonic with scrypt so
2214
- // PALMYR_WALLET_PASSPHRASE can decrypt on a different machine / user /
2215
- // headless box where the OS credential-store secret isn't reachable.
2216
- const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
2231
+ // Passphrase resolutionrecoverable-by-default.
2232
+ // Three paths:
2233
+ // 1. `--passphrase <p>` or `PALMYR_WALLET_PASSPHRASE` env seal with scrypt
2234
+ // 2. `--session-only` explicit opt-out, OS-keychain-only (warned)
2235
+ // 3. nothing + TTY → interactive prompt
2236
+ // 4. nothing + non-TTY → error with the three options
2237
+ // Session-only wallets are recoverable ONLY from this machine's OS
2238
+ // keychain; reboot / keyring change / host migration breaks them
2239
+ // permanently. We pushed agents into that foot-gun in 1.8.2 and
2240
+ // earlier; 1.8.3 forces the choice up front.
2241
+ const sessionOnly = !!flags['session-only'];
2242
+ let passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
2243
+ if (passphrase && sessionOnly) {
2244
+ err('Pass either --passphrase / PALMYR_WALLET_PASSPHRASE OR --session-only, not both.', EXIT.BAD_INPUT);
2245
+ }
2246
+ if (!passphrase && !sessionOnly) {
2247
+ if (process.stdin.isTTY) {
2248
+ const { promptNewPassphrase } = await import('./passphrase-prompt.js');
2249
+ if (!AGENT_MODE)
2250
+ process.stderr.write('\n Wallet creation needs a passphrase fallback so the wallet survives a reboot / OS-keychain change / host migration.\n' +
2251
+ ' (Re-run with --session-only to opt out — ephemeral wallets only.)\n\n');
2252
+ passphrase = await promptNewPassphrase('vault wallet');
2253
+ }
2254
+ else {
2255
+ err('Wallet creation requires a recoverable passphrase fallback OR an explicit opt-out:\n\n' +
2256
+ ' PALMYR_WALLET_PASSPHRASE="<phrase>" palmyr wallet create [...] # recommended (env keeps phrase out of shell history)\n' +
2257
+ ' palmyr wallet create --passphrase "<phrase>" [...] # equivalent\n' +
2258
+ ' palmyr wallet create --session-only [...] # OPT OUT — wallet dies with this machine\'s OS keychain\n\n' +
2259
+ 'Session-only wallets are NOT recoverable from the JSON file alone — reboot, keyring change, or host copy renders them unusable.', EXIT.BAD_INPUT);
2260
+ }
2261
+ }
2217
2262
  // ─── Bulk path ───
2218
2263
  if (count > 1) {
2219
2264
  if (isManaged)
@@ -2227,17 +2272,36 @@ async function main() {
2227
2272
  const { storeSecretsBatch } = await import('./credential-store.js');
2228
2273
  // Progress to stderr so JSON on stdout stays clean
2229
2274
  if (!AGENT_MODE)
2230
- process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"${passphrase ? ' (+ passphrase fallback)' : ''}...\n`);
2275
+ process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"${passphrase ? ' (+ passphrase fallback)' : ' (session-only)'}...\n`);
2231
2276
  const results = createLocalWalletsBatch(prefix, count, 'unmanaged', { tag: tagRaw, chains, passphrase });
2232
2277
  if (!AGENT_MODE)
2233
2278
  process.stderr.write(`sealing ${count} session secrets in OS credential store...\n`);
2234
- storeSecretsBatch(results.map(r => ({ account: r.id, secret: r.sessionSecret })));
2235
- log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')}${passphrase ? ', passphrase=set' : ''})`);
2279
+ // Keychain failure is non-fatal IFF a passphrase fallback was
2280
+ // written the wallets are still recoverable via the env var.
2281
+ let keychainStoreWarning = null;
2282
+ try {
2283
+ storeSecretsBatch(results.map(r => ({ account: r.id, secret: r.sessionSecret })));
2284
+ }
2285
+ catch (e) {
2286
+ if (passphrase) {
2287
+ keychainStoreWarning = e?.message || 'keychain store failed';
2288
+ if (!AGENT_MODE)
2289
+ process.stderr.write(` warning: OS keychain unavailable (${keychainStoreWarning}); wallets remain decryptable via PALMYR_WALLET_PASSPHRASE\n`);
2290
+ }
2291
+ else {
2292
+ throw e;
2293
+ }
2294
+ }
2295
+ if (sessionOnly && !AGENT_MODE)
2296
+ emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
2297
+ log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')}, mode=${passphrase ? 'passphrase' : 'session-only'}${keychainStoreWarning ? ', keychain=failed' : ''})`);
2236
2298
  if (AGENT_MODE) {
2237
2299
  print({
2238
2300
  count: results.length,
2239
2301
  tag: tagRaw,
2240
2302
  chains,
2303
+ recoverable: !!passphrase,
2304
+ ...(keychainStoreWarning ? { keychainWarning: keychainStoreWarning } : {}),
2241
2305
  wallets: results.map(r => ({
2242
2306
  id: r.id,
2243
2307
  name: r.name,
@@ -2253,6 +2317,7 @@ async function main() {
2253
2317
  console.log(`\n ${t.success}✔${t.reset} Created ${count} wallets under tag ${t.accent}${tagRaw}${t.reset}`);
2254
2318
  console.log(` ${t.muted}chains:${t.reset} ${chains.join(', ')}`);
2255
2319
  console.log(` ${t.muted}names: ${t.reset}${results[0].name} … ${results[results.length - 1].name}`);
2320
+ console.log(` ${t.muted}recoverable:${t.reset} ${passphrase ? 'yes (passphrase fallback set)' : 'NO — session-only'}`);
2256
2321
  console.log(`\n ${t.muted}List them: ${t.reset}palmyr wallet list --tag ${tagRaw}`);
2257
2322
  console.log(` ${t.muted}Delete all: ${t.reset}palmyr wallet tag-delete ${tagRaw} --confirm\n`);
2258
2323
  }
@@ -2265,10 +2330,26 @@ async function main() {
2265
2330
  // Create locally — no server needed for the key material
2266
2331
  const { createLocalWallet } = await import('./vault.js');
2267
2332
  const w = createLocalWallet(name, mode, { tag: tagRaw, chains, passphrase });
2268
- // Store session secret in OS credential store
2333
+ // Store session secret in OS credential store. Keychain failure is
2334
+ // non-fatal when a passphrase fallback was written.
2269
2335
  const { storeSecret } = await import('./credential-store.js');
2270
- storeSecret(w.id, w.sessionSecret);
2271
- log(`wallet create: ${w.id} (${mode}${tagRaw ? `, tag=${tagRaw}` : ''}, chains=${chains.join(',')})`);
2336
+ let keychainStoreWarning = null;
2337
+ try {
2338
+ storeSecret(w.id, w.sessionSecret);
2339
+ }
2340
+ catch (e) {
2341
+ if (passphrase) {
2342
+ keychainStoreWarning = e?.message || 'keychain store failed';
2343
+ if (!AGENT_MODE)
2344
+ process.stderr.write(` warning: OS keychain unavailable (${keychainStoreWarning}); wallet remains decryptable via PALMYR_WALLET_PASSPHRASE\n`);
2345
+ }
2346
+ else {
2347
+ throw e;
2348
+ }
2349
+ }
2350
+ if (sessionOnly && !AGENT_MODE)
2351
+ emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
2352
+ log(`wallet create: ${w.id} (${mode}${tagRaw ? `, tag=${tagRaw}` : ''}, chains=${chains.join(',')}, mode=${passphrase ? 'passphrase' : 'session-only'}${keychainStoreWarning ? ', keychain=failed' : ''})`);
2272
2353
  // For managed wallets, register metadata with the server to get a setup link
2273
2354
  let setupLink;
2274
2355
  if (isManaged) {
@@ -2312,7 +2393,7 @@ async function main() {
2312
2393
  }
2313
2394
  }
2314
2395
  else {
2315
- print({ ...w, setupLink });
2396
+ print({ ...w, setupLink, recoverable: !!passphrase, ...(keychainStoreWarning ? { keychainWarning: keychainStoreWarning } : {}) });
2316
2397
  }
2317
2398
  break;
2318
2399
  }
@@ -2328,13 +2409,52 @@ async function main() {
2328
2409
  const chains = (wantSol && !wantBase) ? ['solana']
2329
2410
  : (wantBase && !wantSol) ? ['base']
2330
2411
  : ['solana', 'base'];
2331
- const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
2412
+ // Same recoverability gate as `create`. Import is even more
2413
+ // commonly run on a "new machine" after losing access on the
2414
+ // original — going session-only here would re-trap the user in
2415
+ // the same hole they're recovering from.
2416
+ const importSessionOnly = !!flags['session-only'];
2417
+ let importPassphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
2418
+ if (importPassphrase && importSessionOnly) {
2419
+ err('Pass either --passphrase / PALMYR_WALLET_PASSPHRASE OR --session-only, not both.', EXIT.BAD_INPUT);
2420
+ }
2421
+ if (!importPassphrase && !importSessionOnly) {
2422
+ if (process.stdin.isTTY) {
2423
+ const { promptNewPassphrase } = await import('./passphrase-prompt.js');
2424
+ if (!AGENT_MODE)
2425
+ process.stderr.write('\n Import needs a passphrase fallback so the wallet survives a reboot / OS-keychain change / host migration.\n' +
2426
+ ' (Re-run with --session-only to opt out — ephemeral wallets only.)\n\n');
2427
+ importPassphrase = await promptNewPassphrase('vault wallet');
2428
+ }
2429
+ else {
2430
+ err('Wallet import requires a recoverable passphrase fallback OR an explicit opt-out:\n\n' +
2431
+ ' PALMYR_WALLET_PASSPHRASE="<phrase>" palmyr wallet import --mnemonic "..." # recommended\n' +
2432
+ ' palmyr wallet import --mnemonic "..." --passphrase "<phrase>" # equivalent\n' +
2433
+ ' palmyr wallet import --mnemonic "..." --session-only # OPT OUT — wallet dies with this machine\'s OS keychain', EXIT.BAD_INPUT);
2434
+ }
2435
+ }
2332
2436
  const { importLocalWallet } = await import('./vault.js');
2333
- const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains, passphrase });
2334
- // Store session secret
2437
+ const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains, passphrase: importPassphrase });
2438
+ // Store session secret. Keychain failure is non-fatal when a
2439
+ // passphrase fallback was written.
2335
2440
  const { storeSecret } = await import('./credential-store.js');
2336
- storeSecret(w.id, w.sessionSecret);
2337
- log(`wallet import: ${w.id}${passphrase ? ' (+ passphrase fallback)' : ''}`);
2441
+ let importKeychainWarning = null;
2442
+ try {
2443
+ storeSecret(w.id, w.sessionSecret);
2444
+ }
2445
+ catch (e) {
2446
+ if (importPassphrase) {
2447
+ importKeychainWarning = e?.message || 'keychain store failed';
2448
+ if (!AGENT_MODE)
2449
+ process.stderr.write(` warning: OS keychain unavailable (${importKeychainWarning}); wallet remains decryptable via PALMYR_WALLET_PASSPHRASE\n`);
2450
+ }
2451
+ else {
2452
+ throw e;
2453
+ }
2454
+ }
2455
+ if (importSessionOnly && !AGENT_MODE)
2456
+ emitSessionOnlyWarning(process.stderr.write.bind(process.stderr));
2457
+ log(`wallet import: ${w.id} (mode=${importPassphrase ? 'passphrase' : 'session-only'}${importKeychainWarning ? ', keychain=failed' : ''})`);
2338
2458
  if (!AGENT_MODE) {
2339
2459
  render(React.createElement(WalletCreateScreen, {
2340
2460
  version: VERSION,
@@ -2347,7 +2467,7 @@ async function main() {
2347
2467
  }));
2348
2468
  }
2349
2469
  else {
2350
- print(w);
2470
+ print({ ...w, recoverable: !!importPassphrase, ...(importKeychainWarning ? { keychainWarning: importKeychainWarning } : {}) });
2351
2471
  }
2352
2472
  break;
2353
2473
  }
@@ -2517,9 +2637,13 @@ async function main() {
2517
2637
  const msg = flags.msg || flags.message;
2518
2638
  if (!chain || !msg)
2519
2639
  err('--chain and --msg required');
2520
- // Sign locally — no server needed
2640
+ // Sign locally — no server needed.
2641
+ // Read the same passphrase channel as pay / export / rekey so a
2642
+ // passphrase-backed wallet signs from any machine the env var
2643
+ // reaches (was missing in 1.8.2 — inconsistent with other commands).
2644
+ const signPass = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
2521
2645
  const { signMessageLocal } = await import('./vault.js');
2522
- const data = signMessageLocal(walletId, chain, msg);
2646
+ const data = signMessageLocal(walletId, chain, msg, signPass);
2523
2647
  return print({ success: true, ...data });
2524
2648
  render(React.createElement(SuccessScreen, {
2525
2649
  version: VERSION,
@@ -6575,12 +6699,18 @@ async function main() {
6575
6699
  const { join } = await import('path');
6576
6700
  const vaultDir = process.env.PALMYR_WALLET_PATH || join(homedir(), '.palmyr', 'wallet');
6577
6701
  const { isCredentialStoreAvailable } = await import('./credential-store.js');
6702
+ const { hasLegacyKeyfileWallet } = await import('./config.js');
6703
+ // `defaultChain` is legacy keyfile-flow state — only show it when a
6704
+ // keyfile wallet is actually configured. The functional field is
6705
+ // `payChain` (renamed from the misleadingly-similar `defaultPayChain`
6706
+ // disk key), which the x402 pay path reads.
6707
+ const showLegacyChain = hasLegacyKeyfileWallet(cfg);
6578
6708
  const configData = {
6579
6709
  api: cfg.api,
6580
- defaultChain: cfg.defaultChain,
6581
6710
  setupDone: cfg.setupDone,
6711
+ ...(showLegacyChain ? { legacyKeyfileChain: cfg.defaultChain || 'solana' } : {}),
6582
6712
  defaultPayWalletId: cfg.defaultPayWalletId || null,
6583
- defaultPayChain: cfg.defaultPayChain || 'solana',
6713
+ payChain: cfg.defaultPayChain || 'solana',
6584
6714
  vaultPath: vaultDir,
6585
6715
  credentialStore: isCredentialStoreAvailable() ? 'available' : 'unavailable',
6586
6716
  configPath: join(homedir(), '.palmyr', 'config.json'),
@@ -6611,21 +6741,67 @@ async function main() {
6611
6741
  const { listVaultWallets } = await import('./vault.js');
6612
6742
  const wallets = listVaultWallets();
6613
6743
  checks.push({ name: 'Local wallets', status: wallets.length > 0 ? 'pass' : 'warn', detail: `${wallets.length} wallet(s) found` });
6614
- // 4. Session secrets present for wallets
6744
+ // 4. Decryption readiness for each wallet — tri-state.
6745
+ //
6746
+ // pass → wallet has keychain secret, OR has a passphrase fallback
6747
+ // AND PALMYR_WALLET_PASSPHRASE is set
6748
+ // warn → has passphrase fallback but env unset (recoverable, but
6749
+ // the next command will fail until env is set)
6750
+ // fail → session-only AND keychain secret is gone (unrecoverable
6751
+ // from this machine — needs mnemonic re-import or
6752
+ // rekey-on-original)
6615
6753
  const { retrieveSecret } = await import('./credential-store.js');
6616
- let secretsOk = 0, secretsMissing = 0;
6754
+ const { hasPassphraseFallback } = await import('./vault.js');
6755
+ const envSet = !!process.env.PALMYR_WALLET_PASSPHRASE;
6756
+ let keychainOk = 0;
6757
+ let needsEnv = 0;
6758
+ let unrecoverable = [];
6617
6759
  for (const w of wallets) {
6618
- if (retrieveSecret(w.id))
6619
- secretsOk++;
6620
- else
6621
- secretsMissing++;
6760
+ if (retrieveSecret(w.id)) {
6761
+ keychainOk++;
6762
+ continue;
6763
+ }
6764
+ let hasFallback = false;
6765
+ try {
6766
+ hasFallback = hasPassphraseFallback(w.id);
6767
+ }
6768
+ catch { }
6769
+ if (hasFallback) {
6770
+ needsEnv++;
6771
+ }
6772
+ else {
6773
+ unrecoverable.push(w.name || w.id.slice(0, 8));
6774
+ }
6622
6775
  }
6623
6776
  if (wallets.length > 0) {
6624
- checks.push({
6625
- name: 'Session secrets',
6626
- status: secretsMissing === 0 ? 'pass' : 'fail',
6627
- detail: secretsMissing === 0 ? `All ${secretsOk} wallet(s) have secrets stored` : `${secretsMissing} wallet(s) missing session secret`,
6628
- });
6777
+ if (unrecoverable.length > 0) {
6778
+ checks.push({
6779
+ name: 'Wallet decryption',
6780
+ status: 'fail',
6781
+ detail: `${unrecoverable.length} session-only wallet(s) UNRECOVERABLE from this machine — keychain secret missing and no passphrase fallback (${unrecoverable.slice(0, 3).join(', ')}${unrecoverable.length > 3 ? ', …' : ''}). Recover by importing the mnemonic here, or running \`palmyr wallet rekey <id> --passphrase <p>\` on the original machine.`,
6782
+ });
6783
+ }
6784
+ else if (needsEnv > 0 && !envSet) {
6785
+ checks.push({
6786
+ name: 'Wallet decryption',
6787
+ status: 'warn',
6788
+ detail: `${needsEnv} wallet(s) need PALMYR_WALLET_PASSPHRASE to decrypt (keychain secret missing, passphrase fallback present). Set the env var to unblock pay / sign / export.`,
6789
+ });
6790
+ }
6791
+ else if (needsEnv > 0) {
6792
+ checks.push({
6793
+ name: 'Wallet decryption',
6794
+ status: 'pass',
6795
+ detail: `${keychainOk} via OS keychain, ${needsEnv} via PALMYR_WALLET_PASSPHRASE (env set)`,
6796
+ });
6797
+ }
6798
+ else {
6799
+ checks.push({
6800
+ name: 'Wallet decryption',
6801
+ status: 'pass',
6802
+ detail: `All ${keychainOk} wallet(s) have keychain secrets`,
6803
+ });
6804
+ }
6629
6805
  }
6630
6806
  // 5. API connectivity
6631
6807
  try {