@palmyr/cli 1.8.1 → 1.8.2
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 +22 -3
- package/dist/cli.js +57 -6
- package/dist/cli.js.map +1 -1
- package/dist/pay-preflight.js +14 -2
- package/dist/pay-preflight.js.map +1 -1
- package/dist/vault.d.ts +34 -0
- package/dist/vault.js +60 -0
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,23 @@ palmyr wallet import --mnemonic "twelve word seed phrase ..." --name imported
|
|
|
160
160
|
palmyr wallet import --mnemonic "..." --name from-backup --tag restored --solana
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
### Durable recovery — passphrase fallback
|
|
164
|
+
|
|
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:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# At create time — env var preferred (keeps the phrase out of shell history)
|
|
169
|
+
PALMYR_WALLET_PASSPHRASE="your-passphrase" palmyr wallet create --name agent-prod
|
|
170
|
+
|
|
171
|
+
# Migrate an existing wallet — run on the original machine while the OS session secret still works
|
|
172
|
+
palmyr wallet rekey <WALLET_ID> --passphrase "your-passphrase"
|
|
173
|
+
|
|
174
|
+
# Recover on a new machine — copy ~/.palmyr/wallet/wallets/<id>.json over, then:
|
|
175
|
+
PALMYR_WALLET_PASSPHRASE="your-passphrase" palmyr wallet info <WALLET_ID>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
When set, the wallet file gets a second AES-256-GCM blob (`owner_crypto`, scrypt KDF on the passphrase + random salt). Decryption tries the OS session secret first, then falls back to `PALMYR_WALLET_PASSPHRASE` / `--passphrase`. Minimum length 8 chars. Re-running `wallet rekey` rotates — old passphrase stops working.
|
|
179
|
+
|
|
163
180
|
### Exporting a seed
|
|
164
181
|
|
|
165
182
|
```bash
|
|
@@ -409,8 +426,9 @@ All wallet operations except `addresses`, `api-key`, `config`, and `request-appr
|
|
|
409
426
|
|
|
410
427
|
| Command | Network | Notes |
|
|
411
428
|
|---|---|---|
|
|
412
|
-
| `palmyr wallet create [--name N] [--managed] [--solana\|--base] [--tag T] [--count N] [--name-prefix 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. |
|
|
413
|
-
| `palmyr wallet import --mnemonic "..." [--name N] [--managed] [--solana\|--base] [--tag T]` | local | Restore from BIP-39. Same chain / tag flags as `create`. |
|
|
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`. |
|
|
431
|
+
| `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. |
|
|
414
432
|
| `palmyr wallet list [--tag T]` | local | Lists wallets in the local vault. `--tag` filters to one folder. |
|
|
415
433
|
| `palmyr wallet info <ID>` | local | Show one wallet (id, name, addresses, mode, tag). |
|
|
416
434
|
| `palmyr wallet tags` | local | List all tags with wallet count, chains, and date range. |
|
|
@@ -779,7 +797,7 @@ Config is stored in `~/.palmyr/config.json`. Environment variables override file
|
|
|
779
797
|
| `PALMYR_PAY_WALLET` | Force a specific wallet ID for x402 payment. |
|
|
780
798
|
| `PALMYR_WALLET_PATH` | Override vault directory (default `~/.palmyr/wallet`). |
|
|
781
799
|
| `PALMYR_KEYFILE` | Solana keyfile path (legacy single-key flow). |
|
|
782
|
-
| `PALMYR_WALLET_PASSPHRASE` |
|
|
800
|
+
| `PALMYR_WALLET_PASSPHRASE` | Vault decryption passphrase. Used when the wallet was created with `--passphrase` (or env) or migrated via `wallet rekey`. Falls back to the OS-keychain session secret when unset. |
|
|
783
801
|
|
|
784
802
|
### Chain selection during payment
|
|
785
803
|
|
|
@@ -838,6 +856,7 @@ The CLI uses distinct exit codes so scripts and agents can branch on failure mod
|
|
|
838
856
|
- **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.
|
|
839
857
|
- **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.
|
|
840
858
|
- **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.
|
|
841
860
|
- **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`.
|
|
842
861
|
- **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.
|
|
843
862
|
- **No `--no-verify`.** Hooks, signatures, and webhooks are always verified.
|
package/dist/cli.js
CHANGED
|
@@ -219,6 +219,7 @@ const WALLET_HELP = {
|
|
|
219
219
|
{ flag: '--tag <name>', desc: 'Folder-like grouping tag', hint: 'e.g. palmyr-demo — required with --count' },
|
|
220
220
|
{ flag: '--count <N>', desc: 'Bulk-create N wallets in one call (1-500)', hint: 'unmanaged only; requires --tag' },
|
|
221
221
|
{ 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)' },
|
|
222
223
|
],
|
|
223
224
|
import: [
|
|
224
225
|
{ flag: '--mnemonic <words>', desc: 'BIP-39 mnemonic phrase (required)' },
|
|
@@ -227,6 +228,13 @@ const WALLET_HELP = {
|
|
|
227
228
|
{ flag: '--solana', desc: 'Materialize the Solana account only' },
|
|
228
229
|
{ flag: '--base', desc: 'Materialize the Base/EVM account only' },
|
|
229
230
|
{ 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)' },
|
|
232
|
+
],
|
|
233
|
+
rekey: [
|
|
234
|
+
{ flag: '<WALLET_ID>', desc: 'Wallet ID or name (positional or --id)' },
|
|
235
|
+
{ flag: '--passphrase <p>', desc: 'New passphrase to seal the mnemonic with (≥8 chars)', hint: 'or PALMYR_WALLET_PASSPHRASE env; interactive prompt if neither set' },
|
|
236
|
+
{ flag: '--current-passphrase <p>', desc: 'Existing passphrase, if the wallet was already rekeyed and the OS session secret is gone', hint: 'or PALMYR_WALLET_PASSPHRASE_CURRENT env' },
|
|
237
|
+
{ flag: '(note)', desc: 'Run on the original machine while the OS session secret is still resolvable. Becomes the durable recovery path on any other host.' },
|
|
230
238
|
],
|
|
231
239
|
tags: [
|
|
232
240
|
{ flag: '(no args)', desc: 'List all tags with wallet count, chains, and date range' },
|
|
@@ -2153,6 +2161,7 @@ async function main() {
|
|
|
2153
2161
|
{ name: 'tag-delete', description: 'Cascade-delete every wallet under a tag', hint: 'TAG --confirm' },
|
|
2154
2162
|
{ name: 'sign-message', description: 'Sign a message', hint: 'WALLET_ID --chain evm --msg "hello"' },
|
|
2155
2163
|
{ name: 'export', description: 'Export mnemonic for backup', hint: 'WALLET_ID --confirm' },
|
|
2164
|
+
{ name: 'rekey', description: 'Add or rotate the passphrase fallback (durable across OS-keychain loss)', hint: 'WALLET_ID --passphrase <p>' },
|
|
2156
2165
|
{ name: 'api-key', description: 'Create agent API key', hint: 'WALLET_ID --name my-agent' },
|
|
2157
2166
|
{ name: 'config', description: 'Get agent config', hint: 'WALLET_ID' },
|
|
2158
2167
|
{ name: 'use', description: 'Set default pay wallet', hint: 'WALLET_ID' },
|
|
@@ -2201,6 +2210,10 @@ async function main() {
|
|
|
2201
2210
|
const chains = (wantSol && !wantBase) ? ['solana']
|
|
2202
2211
|
: (wantBase && !wantSol) ? ['base']
|
|
2203
2212
|
: ['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;
|
|
2204
2217
|
// ─── Bulk path ───
|
|
2205
2218
|
if (count > 1) {
|
|
2206
2219
|
if (isManaged)
|
|
@@ -2214,12 +2227,12 @@ async function main() {
|
|
|
2214
2227
|
const { storeSecretsBatch } = await import('./credential-store.js');
|
|
2215
2228
|
// Progress to stderr so JSON on stdout stays clean
|
|
2216
2229
|
if (!AGENT_MODE)
|
|
2217
|
-
process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"...\n`);
|
|
2218
|
-
const results = createLocalWalletsBatch(prefix, count, 'unmanaged', { tag: tagRaw, chains });
|
|
2230
|
+
process.stderr.write(`creating ${count} wallets under tag "${tagRaw}"${passphrase ? ' (+ passphrase fallback)' : ''}...\n`);
|
|
2231
|
+
const results = createLocalWalletsBatch(prefix, count, 'unmanaged', { tag: tagRaw, chains, passphrase });
|
|
2219
2232
|
if (!AGENT_MODE)
|
|
2220
2233
|
process.stderr.write(`sealing ${count} session secrets in OS credential store...\n`);
|
|
2221
2234
|
storeSecretsBatch(results.map(r => ({ account: r.id, secret: r.sessionSecret })));
|
|
2222
|
-
log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')})`);
|
|
2235
|
+
log(`wallet create: ${count} wallets under tag "${tagRaw}" (chains=${chains.join(',')}${passphrase ? ', passphrase=set' : ''})`);
|
|
2223
2236
|
if (AGENT_MODE) {
|
|
2224
2237
|
print({
|
|
2225
2238
|
count: results.length,
|
|
@@ -2251,7 +2264,7 @@ async function main() {
|
|
|
2251
2264
|
const mode = isManaged ? 'managed' : 'unmanaged';
|
|
2252
2265
|
// Create locally — no server needed for the key material
|
|
2253
2266
|
const { createLocalWallet } = await import('./vault.js');
|
|
2254
|
-
const w = createLocalWallet(name, mode, { tag: tagRaw, chains });
|
|
2267
|
+
const w = createLocalWallet(name, mode, { tag: tagRaw, chains, passphrase });
|
|
2255
2268
|
// Store session secret in OS credential store
|
|
2256
2269
|
const { storeSecret } = await import('./credential-store.js');
|
|
2257
2270
|
storeSecret(w.id, w.sessionSecret);
|
|
@@ -2315,12 +2328,13 @@ async function main() {
|
|
|
2315
2328
|
const chains = (wantSol && !wantBase) ? ['solana']
|
|
2316
2329
|
: (wantBase && !wantSol) ? ['base']
|
|
2317
2330
|
: ['solana', 'base'];
|
|
2331
|
+
const passphrase = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
|
|
2318
2332
|
const { importLocalWallet } = await import('./vault.js');
|
|
2319
|
-
const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains });
|
|
2333
|
+
const w = importLocalWallet(name, mnemonic, mode, { tag: tagRaw, chains, passphrase });
|
|
2320
2334
|
// Store session secret
|
|
2321
2335
|
const { storeSecret } = await import('./credential-store.js');
|
|
2322
2336
|
storeSecret(w.id, w.sessionSecret);
|
|
2323
|
-
log(`wallet import: ${w.id}`);
|
|
2337
|
+
log(`wallet import: ${w.id}${passphrase ? ' (+ passphrase fallback)' : ''}`);
|
|
2324
2338
|
if (!AGENT_MODE) {
|
|
2325
2339
|
render(React.createElement(WalletCreateScreen, {
|
|
2326
2340
|
version: VERSION,
|
|
@@ -2629,6 +2643,43 @@ async function main() {
|
|
|
2629
2643
|
}
|
|
2630
2644
|
break;
|
|
2631
2645
|
}
|
|
2646
|
+
case 'rekey': {
|
|
2647
|
+
const walletId = positional[0] || flags.id;
|
|
2648
|
+
if (!walletId)
|
|
2649
|
+
err('Wallet ID required: palmyr wallet rekey <WALLET_ID> --passphrase <p>', EXIT.BAD_INPUT);
|
|
2650
|
+
// New passphrase: flag → env → interactive prompt (TTY only).
|
|
2651
|
+
let newPass = flags.passphrase || process.env.PALMYR_WALLET_PASSPHRASE || undefined;
|
|
2652
|
+
if (!newPass) {
|
|
2653
|
+
if (!process.stdin.isTTY)
|
|
2654
|
+
err('--passphrase or PALMYR_WALLET_PASSPHRASE required (no TTY for interactive prompt)', EXIT.BAD_INPUT);
|
|
2655
|
+
const { promptNewPassphrase } = await import('./passphrase-prompt.js');
|
|
2656
|
+
newPass = await promptNewPassphrase();
|
|
2657
|
+
}
|
|
2658
|
+
// Current passphrase is only needed if the wallet was already
|
|
2659
|
+
// passphrase-sealed and the OS session secret is gone (rare —
|
|
2660
|
+
// typically the rekey runs on the original machine where the
|
|
2661
|
+
// session secret still resolves).
|
|
2662
|
+
const currentPass = flags['current-passphrase'] || process.env.PALMYR_WALLET_PASSPHRASE_CURRENT || undefined;
|
|
2663
|
+
const { rekeyWallet } = await import('./vault.js');
|
|
2664
|
+
let result;
|
|
2665
|
+
try {
|
|
2666
|
+
result = rekeyWallet(walletId, newPass, currentPass);
|
|
2667
|
+
}
|
|
2668
|
+
catch (e) {
|
|
2669
|
+
const code = e.message?.includes('SECURITY') ? EXIT.SECURITY : EXIT.GENERAL;
|
|
2670
|
+
err(e.message, code);
|
|
2671
|
+
}
|
|
2672
|
+
log(`wallet rekey: ${result.id} (${result.rotated ? 'rotated' : 'added'})`);
|
|
2673
|
+
if (!AGENT_MODE) {
|
|
2674
|
+
const verb = result.rotated ? 'Rotated' : 'Added';
|
|
2675
|
+
console.log(`\n ${t.success}✔${t.reset} ${verb} passphrase fallback on ${t.accent}${result.name}${t.reset}`);
|
|
2676
|
+
console.log(` ${t.muted}Wallet now decrypts with PALMYR_WALLET_PASSPHRASE on any machine (in addition to the OS keychain on this one).${t.reset}\n`);
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
print({ success: true, ...result });
|
|
2680
|
+
}
|
|
2681
|
+
break;
|
|
2682
|
+
}
|
|
2632
2683
|
case 'buy': {
|
|
2633
2684
|
const chain = (positional[0] || 'solana').toLowerCase();
|
|
2634
2685
|
if (chain !== 'solana' && chain !== 'base')
|