@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.
- package/README.md +39 -1
- package/dist/cli/hidden-input.d.ts +52 -0
- package/dist/cli/hidden-input.d.ts.map +1 -0
- package/dist/cli/hidden-input.js +109 -0
- package/dist/cli/hidden-input.js.map +1 -0
- package/dist/cli/hidden-prompt.d.ts +48 -0
- package/dist/cli/hidden-prompt.d.ts.map +1 -0
- package/dist/cli/hidden-prompt.js +127 -0
- package/dist/cli/hidden-prompt.js.map +1 -0
- package/dist/cli/secrets-cli.d.ts +9 -1
- package/dist/cli/secrets-cli.d.ts.map +1 -1
- package/dist/cli/secrets-cli.js +272 -175
- package/dist/cli/secrets-cli.js.map +1 -1
- package/dist/crypto/vault-common.d.ts +17 -4
- package/dist/crypto/vault-common.d.ts.map +1 -1
- package/dist/crypto/vault-common.js +23 -6
- package/dist/crypto/vault-common.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/key-providers/index.d.ts +4 -1
- package/dist/key-providers/index.d.ts.map +1 -1
- package/dist/key-providers/index.js +1 -0
- package/dist/key-providers/index.js.map +1 -1
- package/dist/key-providers/passphrase-provider.d.ts +79 -0
- package/dist/key-providers/passphrase-provider.d.ts.map +1 -0
- package/dist/key-providers/passphrase-provider.js +298 -0
- package/dist/key-providers/passphrase-provider.js.map +1 -0
- package/dist/key-providers/resolve.d.ts +8 -3
- package/dist/key-providers/resolve.d.ts.map +1 -1
- package/dist/key-providers/resolve.js +34 -10
- package/dist/key-providers/resolve.js.map +1 -1
- package/dist/key-providers/types.d.ts +20 -2
- package/dist/key-providers/types.d.ts.map +1 -1
- package/dist/key-providers/types.js +1 -1
- package/dist/vault/file-lock.d.ts +33 -0
- package/dist/vault/file-lock.d.ts.map +1 -0
- package/dist/vault/file-lock.js +143 -0
- package/dist/vault/file-lock.js.map +1 -0
- package/dist/vault/session-vault-errors.d.ts +38 -0
- package/dist/vault/session-vault-errors.d.ts.map +1 -0
- package/dist/vault/session-vault-errors.js +67 -0
- package/dist/vault/session-vault-errors.js.map +1 -0
- package/dist/vault/session-vault.d.ts +147 -0
- package/dist/vault/session-vault.d.ts.map +1 -0
- package/dist/vault/session-vault.js +669 -0
- package/dist/vault/session-vault.js.map +1 -0
- package/dist/vault/sidecar.d.ts +37 -0
- package/dist/vault/sidecar.d.ts.map +1 -0
- package/dist/vault/sidecar.js +84 -0
- package/dist/vault/sidecar.js.map +1 -0
- package/dist/vault/types.d.ts +1 -1
- package/dist/vault/types.d.ts.map +1 -1
- 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
|
|
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
|
|
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"}
|