@papervault/cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 PaperVault.xyz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # PaperVault CLI — Store secrets on paper using threshold encryption
2
+
3
+ **PaperVault 📄🔐** is a free open source tool for creating offline paper-based data vaults for your foundational secrets, such as passwords, 2FA recovery codes, digital asset keys, hard drive encryption keys, and other critical data.
4
+
5
+ This is the **command-line version**. Same crypto, same paper output, same [papervault.xyz/unlock](https://papervault.xyz/unlock) recovery path — but driven from your terminal so you can wire it into shell pipelines, pull straight from secret stores like Azure Key Vault, or import a `.env` file in one command. The browser app lives at [papervault.xyz](https://papervault.xyz).
6
+
7
+ ## 🔐 Overview
8
+
9
+ PaperVault encrypts your secrets and splits the decryption key into shards that can be printed on paper or saved to digital media. Keys are split using [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing). Choose how many keys to create and how many are needed to unlock — for example, 5 keys with any 3 required (3-of-5).
10
+
11
+ The CLI produces the same v2 vault format as the browser app, so kits made here unlock identically at [papervault.xyz/unlock](https://papervault.xyz/unlock).
12
+
13
+ ## 🚀 Quick Start
14
+
15
+ ```bash
16
+ # Install once
17
+ npm install -g @papervault/cli
18
+
19
+ # Walk through the wizard
20
+ papervault init
21
+ ```
22
+
23
+ Or run the wizard without installing:
24
+
25
+ ```bash
26
+ npx @papervault/init
27
+ ```
28
+
29
+ The wizard asks where your secrets live. Three common paths:
30
+
31
+ | Where they are | What to pick | What happens |
32
+ |---|---|---|
33
+ | In your head / on a sticky note | **"I'll add them now"** | Walks you through entering each secret; nothing saved to disk. |
34
+ | In a `.env` file | **"Import from .env file"** | Multiselect which entries to back up; nothing saved to disk. |
35
+ | In Azure Key Vault | **"Azure Key Vault"** | Uses your `az login` cache; writes a `papervault.config.json` so future runs are one command. |
36
+
37
+ After that, your browser opens with a print-ready kit — print the vault page and each key page, distribute, done.
38
+
39
+ Requires Node.js ≥ 24.
40
+
41
+ ## 🔑 Key Features
42
+
43
+ - **Works offline** — No internet required after install; safe to run on an air-gapped machine.
44
+ - **Client-side only** — All crypto runs locally. No data ever leaves your device.
45
+ - **Printable** — Vault and keys come out as standalone HTML pages with embedded QR codes; print or save as PDF.
46
+ - **Flexible thresholds** — Any M-of-N combination (up to 20 keys).
47
+ - **Pluggable sources** — Manual entry, `.env` files, Azure Key Vault today; AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, 1Password on the roadmap.
48
+ - **Social recovery & digital inheritance** — Keys can be distributed for recovery in emergencies.
49
+ - **Round-trip compatible** — Kits unlock at [papervault.xyz/unlock](https://papervault.xyz/unlock), same as web-app vaults.
50
+
51
+ ## 📄 Vault vs key shares (social recovery)
52
+
53
+ The encrypted vault page and the key share pages are **separate documents** — keyholders need both the threshold of keys *and* the vault page to recover. See [Vault vs key shares](https://github.com/boazeb/papervault#-vault-vs-key-shares-social-recovery) in the main repo for the full social-recovery model.
54
+
55
+ ## 📖 How It Works
56
+
57
+ 1. **Pick a source** — manual entry, `.env`, JSON file, or a cloud secret store. Secrets only ever live in memory.
58
+ 2. **Configure shares** — choose number of keys (N) and recovery threshold (M).
59
+ 3. **Encrypt + split** — your secrets are encrypted with a fresh AES-256-GCM key; that key is split via Shamir's algorithm.
60
+ 4. **Print & distribute** — the CLI opens your browser to an ephemeral, in-memory page with a single "Print kit" button. The OS print dialog handles the rest (use "Save as PDF" from the dialog if you'd rather save than print).
61
+ 5. **Recovery** — go to [papervault.xyz/unlock](https://papervault.xyz/unlock) on any device, scan the vault QR, then scan any M key QRs. Your secrets reappear.
62
+
63
+ ## 🔧 Commands
64
+
65
+ ```bash
66
+ papervault init # Interactive wizard (most users start here)
67
+ papervault backup [options] # Generate a kit (reads papervault.config.json if present)
68
+ papervault sources list # Show available secret backends
69
+ papervault verify <kit-dir> # Round-trip check on a saved kit
70
+ ```
71
+
72
+ Run any command with `--help` for the full flag reference.
73
+
74
+ ### Source URIs
75
+
76
+ | Scheme | Status | Notes |
77
+ |---|---|---|
78
+ | `file://<path>` | ✓ ready | JSON file you maintain — see schema below |
79
+ | `stdin` (`-`) | ✓ ready | Pipe JSON in for scripts |
80
+ | `azure-kv://<vault-name>` | ✓ ready | Uses `az login` cache |
81
+ | `aws-sm://` | roadmap | AWS Secrets Manager |
82
+ | `gcp-sm://` | roadmap | GCP Secret Manager |
83
+ | `vault://` | roadmap | HashiCorp Vault |
84
+ | `1password://` | roadmap | 1Password CLI |
85
+
86
+ ### JSON source schema
87
+
88
+ For `file://` and stdin sources:
89
+
90
+ ```json
91
+ {
92
+ "vaultName": "Optional default label",
93
+ "freeText": "Optional freeform notes",
94
+ "secrets": [
95
+ {"kind": "password", "service": "GitHub", "username": "alice", "password": "...", "url": "https://github.com"},
96
+ {"kind": "wallet", "name": "BTC cold", "seed": "...", "address": "..."},
97
+ {"kind": "apikey", "service": "Stripe live", "key": "sk_live_...", "secret": "..."},
98
+ {"kind": "note", "title": "Lawyer contact", "content": "..."},
99
+ {"kind": "custom", "label": "anything", "value": "..."}
100
+ ]
101
+ }
102
+ ```
103
+
104
+ Field names follow the web app's structured-entry format so kits unlock identically there.
105
+
106
+ ### Config file
107
+
108
+ `papervault init` writes `papervault.config.json` in the current directory for recurring sources:
109
+
110
+ ```json
111
+ {
112
+ "version": "1",
113
+ "source": "azure-kv://my-vault",
114
+ "threshold": 2,
115
+ "shares": 3,
116
+ "vaultName": "Production DR Kit",
117
+ "custodianNames": ["alice", "bob", "carol"],
118
+ "savePath": "/Users/me/papervault-backups"
119
+ }
120
+ ```
121
+
122
+ `papervault backup` picks this up automatically. CLI flags override config values. Pass `--no-config` to ignore it. **The config never holds secret values** — only how/where to fetch them at backup time.
123
+
124
+ ## 🔍 Audit with AI
125
+
126
+ This is open source software. Run a quick AI-assisted audit via the [Audit with AI](https://github.com/boazeb/papervault#-audit-with-ai) links in the main repo (ChatGPT / Claude / Gemini / Grok / Perplexity, each one-click).
127
+
128
+ ## 🛡️ Security Model
129
+
130
+ ### Cryptographic Foundation
131
+
132
+ - **Algorithm**: Shamir's Secret Sharing over GF(2^8) via [shamir-secret-sharing](https://github.com/privy-io/shamir-secret-sharing).
133
+ - **Encryption**: AES-256-GCM (authenticated) via the Web Crypto API (`crypto.subtle`). No JavaScript reimplementation of the cipher.
134
+ - **Key Generation**: Cryptographically secure random number generation via `crypto.getRandomValues()`. Never `Math.random()`.
135
+ - **QR Codes**: Level-M error correction (~15% damage recovery) for reliable scanning from paper.
136
+ - **Vault format**: v2 — byte-identical to vaults produced by the web app, so kits unlock at [papervault.xyz/unlock](https://papervault.xyz/unlock).
137
+
138
+ ### Security Best Practices
139
+
140
+ 1. **Air-Gapped Usage**: Run the CLI on an offline computer for maximum security.
141
+ 2. **Source Code Review**: Audit the code before using with critical secrets (see the AI audit links above).
142
+ 3. **Physical Security**: Store paper keys and the vault page in separate, secure locations.
143
+ 4. **Test Recovery**: Always test your recovery process at papervault.xyz/unlock before relying on it.
144
+ 5. **Durable Storage**: For maximum durability, consider archive-grade paper in tamper-evident envelopes.
145
+ 6. **Audit Log**: The CLI writes one line per invocation to `~/.papervault/audit.log`. Secret values are **never** logged; secret names are hashed to a 16-char fingerprint by default.
146
+
147
+ ### Threat Model
148
+
149
+ PaperVault does NOT protect against:
150
+
151
+ - ❌ Physical compromise of threshold number of keys + vault
152
+ - ❌ Shoulder surfing during secret entry
153
+ - ❌ Malicious modifications to the source code
154
+ - ❌ Compromise of the cloud secret store you're pulling from
155
+ - ❌ Social engineering
156
+
157
+ ## 🔧 Technical Details
158
+
159
+ ### Architecture
160
+
161
+ - **Crypto core**: [`@papervault/core`](https://www.npmjs.com/package/@papervault/core) — AES-GCM, Shamir, HTML page generation
162
+ - **CLI shell**: this package — interactive wizard, source adapters, ephemeral print server
163
+ - **MCP server**: [`@papervault/mcp`](https://www.npmjs.com/package/@papervault/mcp) — lets AI agents trigger backups as a safety step
164
+ - **Pure JS**, no native deps. Uses Node 24's built-in WebCrypto.
165
+
166
+ ### Print server
167
+
168
+ By default, `papervault backup` opens an ephemeral localhost HTTP server bound to `127.0.0.1` on a random port. The server holds the kit in memory only, serves a single-page UI with a "Print kit" button, and shuts down (dropping references) when you click "Done". Use `--save <dir>` to also write the HTML files to disk.
169
+
170
+ ### Limits
171
+
172
+ - **Maximum Keys**: 20 (cryptographic library constraint)
173
+ - **Storage Limit**: 300 characters of user content per vault (QR code optimization — same as the web app)
174
+
175
+ ## 🔗 Related Packages
176
+
177
+ - [`@papervault/core`](https://www.npmjs.com/package/@papervault/core) — Crypto + Shamir + page generation library
178
+ - [`@papervault/mcp`](https://www.npmjs.com/package/@papervault/mcp) — MCP server for AI agents
179
+ - [`@papervault/init`](https://www.npmjs.com/package/@papervault/init) — `npx` setup wizard
180
+ - [PaperVault web app](https://papervault.xyz) — same crypto, browser version
181
+ - [Main repo](https://github.com/boazeb/papervault) — issues, docs, SECURITY.md
182
+
183
+ ## 🤝 Contributing
184
+
185
+ Contributions are welcome! See the main repo at [github.com/boazeb/papervault](https://github.com/boazeb/papervault).
186
+
187
+ ## 📄 License
188
+
189
+ MIT — see [LICENSE](LICENSE).
190
+
191
+ ## 🙏 Acknowledgments
192
+
193
+ - [Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) algorithm by Adi Shamir
194
+ - [shamir-secret-sharing](https://github.com/privy-io/shamir-secret-sharing) — Shamir over GF(2^8)
195
+ - [bip39](https://github.com/bitcoinjs/bip39) — two-word key aliases
196
+ - [qrcode](https://github.com/soldair/node-qrcode) — QR generation
197
+ - [@clack/prompts](https://github.com/natemoo-re/clack) — interactive wizard UX
198
+
199
+ ## 📞 Support
200
+
201
+ - **Issues**: [GitHub Issues](https://github.com/boazeb/papervault/issues)
202
+
203
+ ## ⚠️ Disclaimer
204
+
205
+ This software is provided "as is" without warranty. Users are responsible for:
206
+
207
+ - Verifying the security of their implementation
208
+ - Testing recovery procedures before relying on them
209
+ - Maintaining physical security of printed keys
210
+ - Understanding the cryptographic principles involved
211
+
212
+ **Always test with non-critical data first!**
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@papervault/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line PaperVault. Encrypt secrets, split with Shamir Secret Sharing, generate printable disaster-recovery kits from .env files, JSON, Azure Key Vault, and more.",
5
+ "type": "module",
6
+ "bin": {
7
+ "papervault": "./src/index.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./sources": "./src/sources/index.js",
12
+ "./audit": "./src/audit.js",
13
+ "./commands/init.js": "./src/commands/init.js"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=24.x"
22
+ },
23
+ "homepage": "https://papervault.xyz",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/boazeb/papervault.git",
27
+ "directory": "papervault-cli"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/boazeb/papervault/issues"
31
+ },
32
+ "keywords": [
33
+ "papervault",
34
+ "cli",
35
+ "command-line",
36
+ "cryptography",
37
+ "shamir-secret-sharing",
38
+ "aes-256-gcm",
39
+ "secret-sharing",
40
+ "paper-wallet",
41
+ "cold-storage",
42
+ "disaster-recovery",
43
+ "key-vault",
44
+ "azure-key-vault",
45
+ "secrets-management",
46
+ "open-source"
47
+ ],
48
+ "author": "PaperVault.xyz",
49
+ "license": "MIT",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@azure/identity": "^4.4.1",
55
+ "@azure/keyvault-secrets": "^4.9.0",
56
+ "@clack/prompts": "^0.7.0",
57
+ "@papervault/core": "^0.1.0"
58
+ }
59
+ }
package/src/audit.js ADDED
@@ -0,0 +1,52 @@
1
+ // Audit logging. Records what was done, NEVER what was in it.
2
+ // Default location: ~/.papervault/audit.log
3
+ // Format: one JSON object per line (jsonl) — easy to grep + parse.
4
+
5
+ import { appendFile, mkdir } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { dirname, join } from 'node:path';
8
+ import { createHash } from 'node:crypto';
9
+
10
+ function auditPath() {
11
+ return process.env.PAPERVAULT_AUDIT_LOG || join(homedir(), '.papervault', 'audit.log');
12
+ }
13
+
14
+ /**
15
+ * @param {object} entry
16
+ * @param {string} entry.action e.g. 'backup', 'verify', 'sources.list'
17
+ * @param {string} [entry.sourceUri]
18
+ * @param {number} [entry.secretCount]
19
+ * @param {number} [entry.threshold]
20
+ * @param {number} [entry.shares]
21
+ * @param {string} [entry.vaultId]
22
+ * @param {string} [entry.outcome] 'success' | 'failed' | 'aborted'
23
+ * @param {string} [entry.error]
24
+ * @param {string[]} [entry.secretNames] Hashed before write unless PAPERVAULT_LOG_NAMES=1
25
+ */
26
+ export async function audit(entry) {
27
+ const path = auditPath();
28
+ try {
29
+ await mkdir(dirname(path), { recursive: true });
30
+ } catch { /* dir might exist; OK */ }
31
+
32
+ const logNamesPlaintext = process.env.PAPERVAULT_LOG_NAMES === '1';
33
+ const safe = { ...entry };
34
+ if (Array.isArray(entry.secretNames)) {
35
+ if (logNamesPlaintext) {
36
+ safe.secretNames = entry.secretNames;
37
+ } else {
38
+ // Deterministic fingerprint of the sorted name list. Lets you tell
39
+ // two runs covered the same set without revealing the names.
40
+ const sorted = [...entry.secretNames].sort().join('\n');
41
+ safe.namesFingerprint = createHash('sha256').update(sorted).digest('hex').slice(0, 16);
42
+ delete safe.secretNames;
43
+ }
44
+ }
45
+ safe.ts = new Date().toISOString();
46
+ try {
47
+ await appendFile(path, JSON.stringify(safe) + '\n', 'utf8');
48
+ } catch (err) {
49
+ // Audit log failures should never crash the main flow.
50
+ process.stderr.write(`audit log warning: ${err.message}\n`);
51
+ }
52
+ }
package/src/browser.js ADDED
@@ -0,0 +1,24 @@
1
+ // Cross-platform "open URL in default browser".
2
+ // Doesn't shell out for the URL itself — uses spawn with no shell.
3
+
4
+ import { spawn } from 'node:child_process';
5
+
6
+ export function openUrl(url) {
7
+ const platform = process.platform;
8
+ let cmd, args;
9
+ if (platform === 'darwin') {
10
+ cmd = 'open';
11
+ args = [url];
12
+ } else if (platform === 'win32') {
13
+ cmd = 'cmd';
14
+ args = ['/c', 'start', '', url];
15
+ } else {
16
+ cmd = 'xdg-open';
17
+ args = [url];
18
+ }
19
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
20
+ child.unref();
21
+ child.on('error', err => {
22
+ process.stderr.write(`Could not open browser automatically (${err.message}). Open this URL: ${url}\n`);
23
+ });
24
+ }
package/src/cli.js ADDED
@@ -0,0 +1,42 @@
1
+ // Command dispatcher. Each subcommand owns its own argument parsing
2
+ // (via util.parseArgs) so flags can vary cleanly per command.
3
+
4
+ import { backup } from './commands/backup.js';
5
+ import { verify } from './commands/verify.js';
6
+ import { sourcesCmd } from './commands/sources.js';
7
+ import { init } from './commands/init.js';
8
+
9
+ const HELP = `papervault — disaster-recovery kits from your secret sources
10
+
11
+ Usage:
12
+ papervault init Interactive setup → papervault.config.json
13
+ papervault backup [options] Generate a printable kit
14
+ papervault verify <path> Decrypt-check a saved kit
15
+ papervault sources list Show configured backends
16
+ papervault --help
17
+
18
+ Run 'papervault <command> --help' for command-specific options.
19
+ `;
20
+
21
+ export async function main(argv) {
22
+ const [command, ...rest] = argv;
23
+
24
+ if (!command || command === '--help' || command === '-h') {
25
+ process.stdout.write(HELP);
26
+ return;
27
+ }
28
+
29
+ switch (command) {
30
+ case 'init':
31
+ return init(rest);
32
+ case 'backup':
33
+ return backup(rest);
34
+ case 'verify':
35
+ return verify(rest);
36
+ case 'sources':
37
+ return sourcesCmd(rest);
38
+ default:
39
+ process.stderr.write(`Unknown command: ${command}\n\n${HELP}`);
40
+ process.exit(2);
41
+ }
42
+ }
@@ -0,0 +1,217 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { LIMITS } from '@papervault/core';
3
+ import { resolveSource } from '../sources/index.js';
4
+ import { audit } from '../audit.js';
5
+ import { readConfig, CONFIG_FILENAME } from '../config.js';
6
+ import { runKitFlow } from '../kit-runner.js';
7
+
8
+ const HELP = `papervault backup — generate a printable disaster-recovery kit
9
+
10
+ Usage:
11
+ papervault backup [options]
12
+
13
+ Source (one of):
14
+ --source <uri> file://<path>, azure-kv://<vault-name>, or - for stdin.
15
+ Falls back to ${CONFIG_FILENAME}.source if present, then stdin.
16
+ Azure uses your 'az login' cache (no creds handled by us).
17
+ Other clouds (AWS Secrets Manager, GCP Secret Manager,
18
+ HashiCorp Vault, 1Password) are on the roadmap.
19
+
20
+ Required:
21
+ --threshold N Minimum keys required to unlock the vault
22
+ --shares N Total number of key shares to generate
23
+
24
+ Optional:
25
+ --name <text> Vault name shown on the printed kit
26
+ --select <glob,glob> Filter source secrets by glob pattern (comma-separated)
27
+ --names <a,b,c> Custodian names per share (e.g. "alice,bob,carol")
28
+ --max-secrets N Hard cap on number of secrets (default 20)
29
+ --save <dir> Also write HTML files to <dir>/vault-<id>/
30
+ --no-print Skip the ephemeral print server (only useful with --save)
31
+ --yes Skip interactive confirmation (use in scripts)
32
+ --dry-run List what would be backed up, then exit
33
+ --no-config Ignore ${CONFIG_FILENAME} in the current directory
34
+ --help
35
+
36
+ Config: if ${CONFIG_FILENAME} exists in the current directory, its values
37
+ are used as defaults. CLI flags override config. Run \`papervault init\` to
38
+ create one interactively.
39
+
40
+ Defaults: ephemeral print server, no disk writes.
41
+ `;
42
+
43
+ const OPTIONS = {
44
+ source: { type: 'string' },
45
+ threshold: { type: 'string' },
46
+ shares: { type: 'string' },
47
+ name: { type: 'string' },
48
+ select: { type: 'string' },
49
+ names: { type: 'string' },
50
+ 'max-secrets': { type: 'string' },
51
+ save: { type: 'string' },
52
+ 'no-print': { type: 'boolean' },
53
+ yes: { type: 'boolean' },
54
+ 'dry-run': { type: 'boolean' },
55
+ 'no-config': { type: 'boolean' },
56
+ help: { type: 'boolean' },
57
+ };
58
+
59
+ function globToRegex(g) {
60
+ const parts = g.split('*').map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&'));
61
+ return new RegExp('^' + parts.join('.*') + '$');
62
+ }
63
+
64
+ export async function backup(argv) {
65
+ const { values } = parseArgs({ args: argv, options: OPTIONS, allowPositionals: false });
66
+
67
+ if (values.help) {
68
+ process.stdout.write(HELP);
69
+ return;
70
+ }
71
+
72
+ // Layer 1: file config (if present + not --no-config)
73
+ let cfg = null;
74
+ if (!values['no-config']) {
75
+ try { cfg = await readConfig(); }
76
+ catch (err) { process.stderr.write(`Warning: ${err.message}\n`); }
77
+ if (cfg) process.stderr.write(`Using ${CONFIG_FILENAME} (CLI flags override).\n`);
78
+ }
79
+
80
+ // Layer 2: CLI flags override config. Falls back to stdin if nothing set.
81
+ const threshold = Number(values.threshold ?? cfg?.threshold);
82
+ const shares = Number(values.shares ?? cfg?.shares);
83
+ if (!Number.isInteger(threshold) || !Number.isInteger(shares)) {
84
+ throw new Error('--threshold and --shares are required (or set them in ' + CONFIG_FILENAME + '). See --help.');
85
+ }
86
+
87
+ const maxSecrets = Number(values['max-secrets'] ?? cfg?.maxSecrets ?? 20);
88
+ if (!Number.isInteger(maxSecrets) || maxSecrets < 1 || maxSecrets > LIMITS.MAX_KEYS * 5) {
89
+ throw new Error('--max-secrets must be a positive integer.');
90
+ }
91
+
92
+ const sourceUri = values.source ?? cfg?.source ?? '-';
93
+ const src = resolveSource(sourceUri);
94
+ await src.authenticate();
95
+
96
+ // Phase 1: list — see what would be backed up.
97
+ const refs = await src.list();
98
+
99
+ let selectedRefs = refs;
100
+ const effectiveSelect = values.select ?? cfg?.select;
101
+ if (effectiveSelect) {
102
+ const patterns = effectiveSelect.split(',').map(s => globToRegex(s.trim()));
103
+ selectedRefs = refs.filter(r => patterns.some(p => p.test(r.name)));
104
+ }
105
+
106
+ if (selectedRefs.length === 0) {
107
+ throw new Error('No secrets matched the selection.');
108
+ }
109
+ if (selectedRefs.length > maxSecrets) {
110
+ throw new Error(`${selectedRefs.length} secrets matched, but --max-secrets is ${maxSecrets}. ` +
111
+ 'Tighten --select or raise --max-secrets explicitly.');
112
+ }
113
+
114
+ process.stderr.write(`Source: ${src.uri}\n`);
115
+ process.stderr.write(`Selected ${selectedRefs.length} secret${selectedRefs.length === 1 ? '' : 's'}:\n`);
116
+ for (const r of selectedRefs) {
117
+ process.stderr.write(` - ${r.name} [${r.kind}]\n`);
118
+ }
119
+
120
+ if (values['dry-run']) {
121
+ await audit({
122
+ action: 'backup.dryrun',
123
+ sourceUri: src.uri,
124
+ secretCount: selectedRefs.length,
125
+ threshold, shares,
126
+ secretNames: selectedRefs.map(r => r.name),
127
+ outcome: 'success',
128
+ });
129
+ await src.close();
130
+ return;
131
+ }
132
+
133
+ if (!values.yes) {
134
+ process.stderr.write(`\nAbout to encrypt + split into ${shares} keys (threshold ${threshold}). ` +
135
+ `Pass --yes to skip this confirmation.\nContinue? [y/N] `);
136
+ const answer = await readOneLineFromStdin();
137
+ if (!/^y(es)?$/i.test(answer.trim())) {
138
+ process.stderr.write('Aborted.\n');
139
+ await audit({
140
+ action: 'backup',
141
+ sourceUri: src.uri,
142
+ secretCount: selectedRefs.length,
143
+ threshold, shares,
144
+ outcome: 'aborted',
145
+ });
146
+ await src.close();
147
+ return;
148
+ }
149
+ }
150
+
151
+ // Phase 2: fetch only the selected refs. Adapter has already filtered
152
+ // for us, so doc.secrets is exactly what was asked for.
153
+ const doc = await src.fetch(selectedRefs);
154
+ const secrets = doc.secrets;
155
+
156
+ const vaultName = values.name ?? cfg?.vaultName ?? doc.vaultName ?? 'Disaster Recovery Kit';
157
+ const custodianNames = values.names
158
+ ? values.names.split(',').map(s => s.trim())
159
+ : (cfg?.custodianNames ?? undefined);
160
+ if (custodianNames && custodianNames.length !== shares) {
161
+ throw new Error(`names list has ${custodianNames.length} entries but shares is ${shares}.`);
162
+ }
163
+
164
+ let result;
165
+ try {
166
+ result = await runKitFlow({
167
+ secrets,
168
+ freeText: doc.freeText,
169
+ vaultName,
170
+ threshold,
171
+ shares,
172
+ custodianNames,
173
+ savePath: values.save ?? cfg?.savePath,
174
+ print: !values['no-print'],
175
+ });
176
+ } catch (err) {
177
+ await audit({
178
+ action: 'backup',
179
+ sourceUri: src.uri,
180
+ secretCount: selectedRefs.length,
181
+ threshold, shares,
182
+ outcome: 'failed',
183
+ error: err.message,
184
+ });
185
+ await src.close();
186
+ throw err;
187
+ }
188
+ await src.close();
189
+
190
+ await audit({
191
+ action: 'backup',
192
+ sourceUri: src.uri,
193
+ secretCount: selectedRefs.length,
194
+ threshold, shares,
195
+ vaultId: result.vaultId,
196
+ savedTo: result.savedTo,
197
+ printed: result.printed,
198
+ outcome: 'success',
199
+ });
200
+ }
201
+
202
+ function readOneLineFromStdin() {
203
+ return new Promise(resolve => {
204
+ let buf = '';
205
+ const onData = (chunk) => {
206
+ buf += chunk.toString('utf8');
207
+ const nl = buf.indexOf('\n');
208
+ if (nl >= 0) {
209
+ process.stdin.off('data', onData);
210
+ process.stdin.pause();
211
+ resolve(buf.slice(0, nl));
212
+ }
213
+ };
214
+ process.stdin.resume();
215
+ process.stdin.on('data', onData);
216
+ });
217
+ }