@centient/secrets 0.5.0 → 0.7.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.
Files changed (55) hide show
  1. package/README.md +39 -1
  2. package/dist/cli/hidden-input.d.ts +52 -0
  3. package/dist/cli/hidden-input.d.ts.map +1 -0
  4. package/dist/cli/hidden-input.js +109 -0
  5. package/dist/cli/hidden-input.js.map +1 -0
  6. package/dist/cli/hidden-prompt.d.ts +48 -0
  7. package/dist/cli/hidden-prompt.d.ts.map +1 -0
  8. package/dist/cli/hidden-prompt.js +127 -0
  9. package/dist/cli/hidden-prompt.js.map +1 -0
  10. package/dist/cli/secrets-cli.d.ts +9 -1
  11. package/dist/cli/secrets-cli.d.ts.map +1 -1
  12. package/dist/cli/secrets-cli.js +272 -175
  13. package/dist/cli/secrets-cli.js.map +1 -1
  14. package/dist/crypto/vault-common.d.ts +17 -4
  15. package/dist/crypto/vault-common.d.ts.map +1 -1
  16. package/dist/crypto/vault-common.js +23 -6
  17. package/dist/crypto/vault-common.js.map +1 -1
  18. package/dist/index.d.ts +7 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/key-providers/index.d.ts +4 -1
  23. package/dist/key-providers/index.d.ts.map +1 -1
  24. package/dist/key-providers/index.js +1 -0
  25. package/dist/key-providers/index.js.map +1 -1
  26. package/dist/key-providers/passphrase-provider.d.ts +79 -0
  27. package/dist/key-providers/passphrase-provider.d.ts.map +1 -0
  28. package/dist/key-providers/passphrase-provider.js +298 -0
  29. package/dist/key-providers/passphrase-provider.js.map +1 -0
  30. package/dist/key-providers/resolve.d.ts +8 -3
  31. package/dist/key-providers/resolve.d.ts.map +1 -1
  32. package/dist/key-providers/resolve.js +34 -10
  33. package/dist/key-providers/resolve.js.map +1 -1
  34. package/dist/key-providers/types.d.ts +20 -2
  35. package/dist/key-providers/types.d.ts.map +1 -1
  36. package/dist/key-providers/types.js +1 -1
  37. package/dist/vault/file-lock.d.ts +33 -0
  38. package/dist/vault/file-lock.d.ts.map +1 -0
  39. package/dist/vault/file-lock.js +143 -0
  40. package/dist/vault/file-lock.js.map +1 -0
  41. package/dist/vault/session-vault-errors.d.ts +38 -0
  42. package/dist/vault/session-vault-errors.d.ts.map +1 -0
  43. package/dist/vault/session-vault-errors.js +67 -0
  44. package/dist/vault/session-vault-errors.js.map +1 -0
  45. package/dist/vault/session-vault.d.ts +147 -0
  46. package/dist/vault/session-vault.d.ts.map +1 -0
  47. package/dist/vault/session-vault.js +669 -0
  48. package/dist/vault/session-vault.js.map +1 -0
  49. package/dist/vault/sidecar.d.ts +37 -0
  50. package/dist/vault/sidecar.d.ts.map +1 -0
  51. package/dist/vault/sidecar.js +84 -0
  52. package/dist/vault/sidecar.js.map +1 -0
  53. package/dist/vault/types.d.ts +1 -1
  54. package/dist/vault/types.d.ts.map +1 -1
  55. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Cross-platform secrets vault with AES-256-GCM encryption and platform-native key storage.
4
4
 
5
+ > **Daemons / long-running processes:** see [Session-backed vault (`openVault`)](./docs/session-vault.md) for the recommended API — single master-key unlock per session, in-memory cached reads, mtime-check coherence with the CLI, rollback protection via monotonic version + sidecar.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -18,7 +20,7 @@ pnpm add @centient/secrets
18
20
 
19
21
  - AES-256-GCM authenticated encryption for secrets at rest
20
22
  - Platform-native key storage (macOS Keychain, Linux secret-service)
21
- - Pluggable key providers (Keychain, 1Password)
23
+ - Pluggable key providers (Keychain, 1Password, passphrase)
22
24
  - Credential vault with session management
23
25
  - Environment detection (CI, Docker, SSH, headless, agent)
24
26
  - Built-in CLI for interactive secret management
@@ -64,6 +66,42 @@ if (isCIEnvironment()) {
64
66
  |----------|----------|-------------|
65
67
  | `KeychainProvider` | macOS/Linux | Uses OS keychain (Keychain Access / secret-service) |
66
68
  | `OnePasswordProvider` | Any | Uses 1Password CLI for team secret sharing |
69
+ | `PassphraseProvider` | Any (interactive TTY) | Derives the vault key from a typed passphrase via scrypt — no OS keychain required |
70
+
71
+ Provider auto-detection prefers OS-backed storage: 1Password, then Keychain,
72
+ then passphrase as the last fallback. Set `secrets.provider: "passphrase"` in
73
+ `~/.centient/config.json` to select it explicitly.
74
+
75
+ ### Passphrase provider
76
+
77
+ For hosts without an OS keychain or 1Password CLI (e.g. a headless Linux box
78
+ over SSH), the vault key is derived from a passphrase typed at an interactive
79
+ terminal using scrypt (`N=2^17, r=8, p=1`, 32-byte key — ~128 MB memory cost
80
+ per derivation, in line with current OWASP guidance). The passphrase and the
81
+ derived key are never persisted. A sidecar file (`vault.passphrase.json`,
82
+ mode `0600`, beside the vault) stores only the salt, the KDF parameters, and
83
+ an HMAC-SHA256 verifier used to detect a wrong passphrase without revealing
84
+ the key.
85
+
86
+ Security tradeoffs vs OS-backed providers — choose deliberately:
87
+
88
+ - **Passphrase strength is the security ceiling.** Keychain keys are random
89
+ 256-bit values guarded by the OS; a passphrase-derived key is only as strong
90
+ as the passphrase. The scrypt cost is the sole brake on brute force.
91
+ - **The sidecar enables offline guessing if exfiltrated.** An attacker holding
92
+ `vault.passphrase.json` (or the vault file) can test candidate passphrases
93
+ offline at ~one guess per 128 MB-scrypt derivation. Use a long, high-entropy
94
+ passphrase.
95
+ - **No human-presence guarantee.** Unlike Keychain with Touch ID, typing a
96
+ passphrase proves knowledge, not presence; it cannot satisfy policies that
97
+ require fresh per-operation human auth.
98
+ - **Interactive TTY required — fails closed otherwise.** In CI, agent, or
99
+ other non-interactive contexts the provider refuses to prompt and unlock
100
+ fails with an actionable error. Configure keychain/1Password for
101
+ non-interactive use.
102
+ - **Unlock blocks the event loop.** Key derivation is synchronous (~hundreds
103
+ of ms); daemons should call `openVault()` once at startup, before entering
104
+ their hot loop.
67
105
 
68
106
  ## License
69
107
 
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Hidden-Input State Machine
3
+ *
4
+ * Pure character-stream parser used by the secrets CLI's hidden-input prompt
5
+ * (password / secret entry). Extracted from `secrets-cli.ts` so the parsing
6
+ * rules can be exercised directly in tests without stubbing `process.stdin`.
7
+ *
8
+ * Regression: issue #37 — the previous prompt resolved on the first `\n`
9
+ * from a terminal paste and silently truncated multi-line secrets (PEM keys,
10
+ * multi-line config blobs) to their first line.
11
+ */
12
+ /** VT100 bracketed-paste sentinels — wraps pasted content from the terminal. */
13
+ export declare const PASTE_START = "\u001B[200~";
14
+ export declare const PASTE_END = "\u001B[201~";
15
+ /** Enable / disable bracketed paste mode on the current terminal. */
16
+ export declare const ENABLE_BRACKETED_PASTE = "\u001B[?2004h";
17
+ export declare const DISABLE_BRACKETED_PASTE = "\u001B[?2004l";
18
+ /** Control characters recognized by the hidden-input state machine. */
19
+ export declare const CTRL_C = "\u0003";
20
+ export declare const CTRL_D = "\u0004";
21
+ export declare const BACKSPACE = "";
22
+ export declare const BS = "\b";
23
+ /** Signal emitted by {@link advanceHiddenInput} to indicate why input stopped. */
24
+ export type HiddenInputSignal = "submit" | "ctrl-c" | "continue";
25
+ /**
26
+ * Mutable state for the hidden-input parser. Callers should construct a fresh
27
+ * instance per prompt and feed chunks via {@link advanceHiddenInput}.
28
+ */
29
+ export interface HiddenInputState {
30
+ input: string;
31
+ inPaste: boolean;
32
+ escBuf: string;
33
+ /**
34
+ * True when the state machine has recognized an escape sequence that is not
35
+ * a paste-marker prefix and is swallowing characters until the sequence's
36
+ * terminator byte arrives. Prevents stray `~` / digits from unrelated CSI
37
+ * sequences (e.g. Delete `\x1b[3~`, arrow keys) from leaking into `input`.
38
+ */
39
+ swallowingCsi: boolean;
40
+ }
41
+ export declare function createHiddenInputState(): HiddenInputState;
42
+ /**
43
+ * Advance the state machine with a single chunk from stdin. Returns `"submit"`
44
+ * when the caller should resolve the prompt with `state.input`, `"ctrl-c"`
45
+ * when the user interrupted, or `"continue"` when more input is expected.
46
+ *
47
+ * Character-by-character processing ensures escape-sequence state survives
48
+ * across chunk boundaries (a fast paste may arrive split across many chunks,
49
+ * and a single chunk may contain both paste markers and regular keystrokes).
50
+ */
51
+ export declare function advanceHiddenInput(state: HiddenInputState, chunk: string): HiddenInputSignal;
52
+ //# sourceMappingURL=hidden-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-input.d.ts","sourceRoot":"","sources":["../../src/cli/hidden-input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,eAAO,MAAM,WAAW,gBAAc,CAAC;AACvC,eAAO,MAAM,SAAS,gBAAc,CAAC;AACrC,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,kBAAgB,CAAC;AACpD,eAAO,MAAM,uBAAuB,kBAAgB,CAAC;AAErD,uEAAuE;AACvE,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,SAAS,MAAW,CAAC;AAClC,eAAO,MAAM,EAAE,OAAO,CAAC;AAEvB,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAEzD;AAeD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,MAAM,GACZ,iBAAiB,CAmEnB"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Hidden-Input State Machine
3
+ *
4
+ * Pure character-stream parser used by the secrets CLI's hidden-input prompt
5
+ * (password / secret entry). Extracted from `secrets-cli.ts` so the parsing
6
+ * rules can be exercised directly in tests without stubbing `process.stdin`.
7
+ *
8
+ * Regression: issue #37 — the previous prompt resolved on the first `\n`
9
+ * from a terminal paste and silently truncated multi-line secrets (PEM keys,
10
+ * multi-line config blobs) to their first line.
11
+ */
12
+ /** VT100 bracketed-paste sentinels — wraps pasted content from the terminal. */
13
+ export const PASTE_START = "\x1b[200~";
14
+ export const PASTE_END = "\x1b[201~";
15
+ /** Enable / disable bracketed paste mode on the current terminal. */
16
+ export const ENABLE_BRACKETED_PASTE = "\x1b[?2004h";
17
+ export const DISABLE_BRACKETED_PASTE = "\x1b[?2004l";
18
+ /** Control characters recognized by the hidden-input state machine. */
19
+ export const CTRL_C = "\u0003";
20
+ export const CTRL_D = "\u0004";
21
+ export const BACKSPACE = "\u007F";
22
+ export const BS = "\b";
23
+ export function createHiddenInputState() {
24
+ return { input: "", inPaste: false, escBuf: "", swallowingCsi: false };
25
+ }
26
+ /**
27
+ * CSI final-byte range per ECMA-48: `\x40`-`\x7E` (i.e. `@` through `~`). When
28
+ * we're swallowing a CSI we know isn't a paste marker, we stop at the first
29
+ * byte in this range that isn't a parameter byte (`0`-`9`, `;`, `:`, `<`-`?`).
30
+ */
31
+ function isCsiTerminator(char) {
32
+ const code = char.charCodeAt(0);
33
+ // Parameter / intermediate bytes — not terminators.
34
+ if ((code >= 0x30 && code <= 0x3f) || (code >= 0x20 && code <= 0x2f))
35
+ return false;
36
+ // Final byte range.
37
+ return code >= 0x40 && code <= 0x7e;
38
+ }
39
+ /**
40
+ * Advance the state machine with a single chunk from stdin. Returns `"submit"`
41
+ * when the caller should resolve the prompt with `state.input`, `"ctrl-c"`
42
+ * when the user interrupted, or `"continue"` when more input is expected.
43
+ *
44
+ * Character-by-character processing ensures escape-sequence state survives
45
+ * across chunk boundaries (a fast paste may arrive split across many chunks,
46
+ * and a single chunk may contain both paste markers and regular keystrokes).
47
+ */
48
+ export function advanceHiddenInput(state, chunk) {
49
+ for (let i = 0; i < chunk.length; i++) {
50
+ const char = chunk[i];
51
+ // Swallowing a non-paste CSI sequence: drop characters until the CSI
52
+ // terminator byte arrives, then resume normal processing.
53
+ if (state.swallowingCsi) {
54
+ if (isCsiTerminator(char))
55
+ state.swallowingCsi = false;
56
+ continue;
57
+ }
58
+ // Accumulate potential bracketed-paste marker.
59
+ if (state.escBuf || char === "\x1b") {
60
+ state.escBuf += char;
61
+ if (state.escBuf === PASTE_START) {
62
+ state.inPaste = true;
63
+ state.escBuf = "";
64
+ continue;
65
+ }
66
+ if (state.escBuf === PASTE_END) {
67
+ state.inPaste = false;
68
+ state.escBuf = "";
69
+ continue;
70
+ }
71
+ // If escBuf is no longer a prefix of either paste marker, it's a
72
+ // different escape sequence (arrow keys, Delete, F-keys, etc.). Switch
73
+ // to swallow-until-terminator mode so we don't leak the sequence's
74
+ // final byte (e.g. `~` from `\x1b[3~`) into the input buffer.
75
+ if (!PASTE_START.startsWith(state.escBuf) &&
76
+ !PASTE_END.startsWith(state.escBuf)) {
77
+ // If the current char is already a terminator, we've consumed the
78
+ // entire sequence; no need to swallow more.
79
+ state.swallowingCsi = !isCsiTerminator(char);
80
+ state.escBuf = "";
81
+ }
82
+ continue;
83
+ }
84
+ // Inside a bracketed paste, newlines are literal content.
85
+ if (state.inPaste) {
86
+ state.input += char;
87
+ continue;
88
+ }
89
+ if (char === CTRL_C)
90
+ return "ctrl-c";
91
+ if (char === CTRL_D) {
92
+ // Explicit end-of-input (multi-line escape hatch for terminals without
93
+ // bracketed-paste support).
94
+ return "submit";
95
+ }
96
+ if (char === "\n" || char === "\r") {
97
+ // Single-line submit — preserves the old single-password UX.
98
+ return "submit";
99
+ }
100
+ if (char === BACKSPACE || char === BS) {
101
+ if (state.input.length > 0)
102
+ state.input = state.input.slice(0, -1);
103
+ continue;
104
+ }
105
+ state.input += char;
106
+ }
107
+ return "continue";
108
+ }
109
+ //# sourceMappingURL=hidden-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-input.js","sourceRoot":"","sources":["../../src/cli/hidden-input.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Synchronous hidden TTY prompt.
3
+ *
4
+ * KeyProvider implementations are synchronous today, so passphrase unlock
5
+ * needs a sync wrapper around the hidden-input state machine. The parser stays
6
+ * in hidden-input.ts; this file only owns terminal raw-mode and fd reads.
7
+ */
8
+ interface PromptInput {
9
+ readonly fd?: number;
10
+ readonly isRaw?: boolean;
11
+ readonly isTTY?: boolean;
12
+ pause?(): unknown;
13
+ resume?(): unknown;
14
+ setRawMode?(mode: boolean): unknown;
15
+ }
16
+ interface PromptOutput {
17
+ write(chunk: string): unknown;
18
+ }
19
+ export interface HiddenPromptOptions {
20
+ input?: PromptInput;
21
+ output?: PromptOutput;
22
+ }
23
+ export declare class HiddenPromptError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ export declare const NON_INTERACTIVE_HIDDEN_PROMPT_MESSAGE = "Passphrase provider requires an interactive TTY. Run this command from a terminal so the passphrase can be typed, or configure keychain/1password for non-interactive use.";
27
+ /**
28
+ * Upper bound on accumulated hidden input. Interactive passphrases and pasted
29
+ * multi-line secrets (PEM keys, config blobs — issue #37) fit comfortably;
30
+ * the cap exists so a hostile or broken input source feeding endless bytes
31
+ * without a submit cannot grow `state.input` without bound.
32
+ */
33
+ export declare const MAX_HIDDEN_INPUT_LENGTH = 65536;
34
+ export declare const HIDDEN_INPUT_TOO_LONG_MESSAGE = "Hidden input exceeded 65536 characters without a submit; aborting prompt.";
35
+ /**
36
+ * Prompt for a hidden single-line value from a real TTY.
37
+ *
38
+ * Returns null when the user sends Ctrl-C. Throws HiddenPromptError when stdin
39
+ * is not an interactive TTY; callers should fail closed.
40
+ *
41
+ * Known limitation: the submitted value is returned as an immutable JS string,
42
+ * which cannot be zeroed and lingers in the heap until GC — same residue class
43
+ * as the session-vault key-in-RAM caveat. The transient read buffer IS zeroed
44
+ * before return.
45
+ */
46
+ export declare function promptHiddenSync(message: string, options?: HiddenPromptOptions): string | null;
47
+ export {};
48
+ //# sourceMappingURL=hidden-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-prompt.d.ts","sourceRoot":"","sources":["../../src/cli/hidden-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,IAAI,OAAO,CAAC;IAClB,MAAM,CAAC,IAAI,OAAO,CAAC;IACnB,UAAU,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CACrC;AAED,UAAU,YAAY;IACpB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,eAAO,MAAM,qCAAqC,+KAC4H,CAAC;AAE/K;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,eAAO,MAAM,6BAA6B,8EAAmG,CAAC;AAQ9I;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,MAAM,GAAG,IAAI,CAqFf"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Synchronous hidden TTY prompt.
3
+ *
4
+ * KeyProvider implementations are synchronous today, so passphrase unlock
5
+ * needs a sync wrapper around the hidden-input state machine. The parser stays
6
+ * in hidden-input.ts; this file only owns terminal raw-mode and fd reads.
7
+ */
8
+ import { readSync } from "node:fs";
9
+ import { StringDecoder } from "node:string_decoder";
10
+ import { advanceHiddenInput, createHiddenInputState, DISABLE_BRACKETED_PASTE, ENABLE_BRACKETED_PASTE, } from "./hidden-input.js";
11
+ export class HiddenPromptError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "HiddenPromptError";
15
+ }
16
+ }
17
+ export const NON_INTERACTIVE_HIDDEN_PROMPT_MESSAGE = "Passphrase provider requires an interactive TTY. Run this command from a terminal so the passphrase can be typed, or configure keychain/1password for non-interactive use.";
18
+ /**
19
+ * Upper bound on accumulated hidden input. Interactive passphrases and pasted
20
+ * multi-line secrets (PEM keys, config blobs — issue #37) fit comfortably;
21
+ * the cap exists so a hostile or broken input source feeding endless bytes
22
+ * without a submit cannot grow `state.input` without bound.
23
+ */
24
+ export const MAX_HIDDEN_INPUT_LENGTH = 65_536;
25
+ export const HIDDEN_INPUT_TOO_LONG_MESSAGE = `Hidden input exceeded ${MAX_HIDDEN_INPUT_LENGTH} characters without a submit; aborting prompt.`;
26
+ const PROMPT_SIGNALS = [
27
+ "SIGINT",
28
+ "SIGTERM",
29
+ "SIGHUP",
30
+ ];
31
+ /**
32
+ * Prompt for a hidden single-line value from a real TTY.
33
+ *
34
+ * Returns null when the user sends Ctrl-C. Throws HiddenPromptError when stdin
35
+ * is not an interactive TTY; callers should fail closed.
36
+ *
37
+ * Known limitation: the submitted value is returned as an immutable JS string,
38
+ * which cannot be zeroed and lingers in the heap until GC — same residue class
39
+ * as the session-vault key-in-RAM caveat. The transient read buffer IS zeroed
40
+ * before return.
41
+ */
42
+ export function promptHiddenSync(message, options = {}) {
43
+ const input = options.input ?? process.stdin;
44
+ const output = options.output ?? process.stdout;
45
+ if (input.isTTY !== true || typeof input.fd !== "number") {
46
+ throw new HiddenPromptError(NON_INTERACTIVE_HIDDEN_PROMPT_MESSAGE);
47
+ }
48
+ const state = createHiddenInputState();
49
+ const decoder = new StringDecoder("utf8");
50
+ const buffer = Buffer.allocUnsafe(256);
51
+ const wasRaw = input.isRaw === true;
52
+ let restored = false;
53
+ const restoreTerminal = () => {
54
+ if (restored)
55
+ return;
56
+ restored = true;
57
+ output.write(DISABLE_BRACKETED_PASTE);
58
+ input.setRawMode?.(wasRaw);
59
+ input.pause?.();
60
+ output.write("\n");
61
+ };
62
+ // A signal's default disposition terminates the process without unwinding
63
+ // the stack, so `finally` would never run and the terminal would be left in
64
+ // raw mode with echo disabled. The terminal's own Ctrl-C arrives in-band as
65
+ // \x03 under raw mode, so these handlers only matter for external signals
66
+ // (`kill`, hangup).
67
+ //
68
+ // Timing: this entire prompt is synchronous, so the event loop cannot
69
+ // dispatch a signal's JS callback while it runs — an external signal
70
+ // received here is queued by libuv and dispatched only after the prompt
71
+ // unwinds. Installing the listeners suppresses the immediate default kill
72
+ // (protecting raw mode); the deferred handler then re-raises so the default
73
+ // exit behavior is preserved. That is also why removal is deferred one tick
74
+ // in `finally` below: removing the listeners synchronously would drop the
75
+ // queued dispatch and the process would silently ignore a kill received
76
+ // during the prompt.
77
+ const onSignal = (signal) => {
78
+ // Remove ALL prompt listeners (not just this signal's spent `once`
79
+ // wrapper) before re-raising, so the re-raised signal hits the default
80
+ // disposition and sibling handlers cannot fire after the prompt is done.
81
+ removeSignalHandlers();
82
+ restoreTerminal();
83
+ process.kill(process.pid, signal);
84
+ };
85
+ const removeSignalHandlers = () => {
86
+ for (const signal of PROMPT_SIGNALS) {
87
+ process.removeListener(signal, onSignal);
88
+ }
89
+ };
90
+ for (const signal of PROMPT_SIGNALS)
91
+ process.once(signal, onSignal);
92
+ output.write(message);
93
+ input.setRawMode?.(true);
94
+ output.write(ENABLE_BRACKETED_PASTE);
95
+ input.resume?.();
96
+ try {
97
+ while (true) {
98
+ const bytesRead = readSync(input.fd, buffer, 0, buffer.length, null);
99
+ if (bytesRead === 0)
100
+ return null;
101
+ const chunk = decoder.write(buffer.subarray(0, bytesRead));
102
+ const signal = advanceHiddenInput(state, chunk);
103
+ if (signal === "ctrl-c")
104
+ return null;
105
+ if (signal === "submit")
106
+ return state.input;
107
+ if (state.input.length > MAX_HIDDEN_INPUT_LENGTH) {
108
+ // Drop the reference before throwing; the string itself is immutable
109
+ // and lingers until GC (same residue class as the documented caveat).
110
+ state.input = "";
111
+ throw new HiddenPromptError(HIDDEN_INPUT_TOO_LONG_MESSAGE);
112
+ }
113
+ }
114
+ }
115
+ finally {
116
+ // The read buffer held raw passphrase bytes — zero it before returning.
117
+ buffer.fill(0);
118
+ restoreTerminal();
119
+ // Deferred (see the comment on onSignal): a signal received while
120
+ // readSync blocked has not been dispatched yet — removing the listeners
121
+ // now would swallow it. One tick later either the queued handler has
122
+ // re-raised (process is exiting) or there was no signal and this is a
123
+ // plain cleanup.
124
+ setImmediate(removeSignalHandlers);
125
+ }
126
+ }
127
+ //# sourceMappingURL=hidden-prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hidden-prompt.js","sourceRoot":"","sources":["../../src/cli/hidden-prompt.ts"],"names":[],"mappings":"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"}
@@ -25,8 +25,16 @@
25
25
  * Security:
26
26
  * - All commands check for AI agent environment and refuse to run
27
27
  * - Vault is encrypted with AES-256-GCM
28
- * - Key stored via pluggable provider (macOS Keychain or 1Password)
28
+ * - Key stored or derived via pluggable provider (macOS Keychain, 1Password,
29
+ * or passphrase)
29
30
  * - 4-hour session timeout
31
+ *
32
+ * Internals:
33
+ * Vault mutations flow through `openVault` (session-vault.ts) so the CLI
34
+ * and the library-facing API share one on-disk format (AAD-bound, schema 1).
35
+ * Pre-`openVault` CLI-written vaults (AAD-less, legacy flat format) are
36
+ * handled transparently by session-vault's legacy-read path and upgraded to
37
+ * schema 1 on the next successful write.
30
38
  */
31
39
  export interface SecretsOptions {
32
40
  command: "init" | "unlock" | "lock" | "list" | "list-backend-keys" | "set" | "get" | "delete" | "status" | "migrate" | "env-list" | "env-switch" | "env-create" | "env-current";
@@ -1 +1 @@
1
- {"version":3,"file":"secrets-cli.d.ts","sourceRoot":"","sources":["../../src/cli/secrets-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAWH,MAAM,WAAW,cAAc;IAC7B,OAAO,EACH,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,mBAAmB,GACnB,KAAK,GACL,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAirBD;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDvE"}
1
+ {"version":3,"file":"secrets-cli.d.ts","sourceRoot":"","sources":["../../src/cli/secrets-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAkBH,MAAM,WAAW,cAAc;IAC7B,OAAO,EACH,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,mBAAmB,GACnB,KAAK,GACL,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAwwBD;;GAEG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDvE"}