@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 +27 -14
- package/dist/cli.js +210 -34
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +15 -1
- package/dist/config.js.map +1 -1
- package/dist/passphrase-prompt.d.ts +4 -1
- package/dist/passphrase-prompt.js +5 -2
- package/dist/passphrase-prompt.js.map +1 -1
- package/dist/vault.d.ts +7 -2
- package/dist/vault.js +8 -3
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
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
|
-
#
|
|
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
|
-
#
|
|
120
|
-
palmyr wallet create --name
|
|
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
|
-
|
|
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]
|
|
430
|
-
| `palmyr wallet import --mnemonic "..." [--name N] [--managed] [--solana\|--base] [--tag T]
|
|
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.
|
|
807
|
-
2. `
|
|
808
|
-
3. `
|
|
809
|
-
|
|
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
|
-
- **
|
|
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: '
|
|
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: '
|
|
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
|
-
//
|
|
2214
|
-
//
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2231
|
+
// Passphrase resolution — recoverable-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
|
-
|
|
2235
|
-
|
|
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
|
-
|
|
2271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2337
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
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
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
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 {
|