@centient/secrets 0.8.0 → 0.10.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 +70 -2
- package/dist/cli/secrets-cli.d.ts.map +1 -1
- package/dist/cli/secrets-cli.js +191 -32
- package/dist/cli/secrets-cli.js.map +1 -1
- package/dist/crypto/darwin-keychain.d.ts +394 -0
- package/dist/crypto/darwin-keychain.d.ts.map +1 -0
- package/dist/crypto/darwin-keychain.js +793 -0
- package/dist/crypto/darwin-keychain.js.map +1 -0
- package/dist/crypto/vault-common.d.ts +55 -12
- package/dist/crypto/vault-common.d.ts.map +1 -1
- package/dist/crypto/vault-common.js +74 -71
- package/dist/crypto/vault-common.js.map +1 -1
- package/dist/environment/EnvironmentManager.d.ts.map +1 -1
- package/dist/environment/EnvironmentManager.js +5 -10
- package/dist/environment/EnvironmentManager.js.map +1 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/key-providers/keychain-provider.d.ts +30 -2
- package/dist/key-providers/keychain-provider.d.ts.map +1 -1
- package/dist/key-providers/keychain-provider.js +29 -5
- package/dist/key-providers/keychain-provider.js.map +1 -1
- package/dist/key-providers/onepassword-provider.d.ts +65 -5
- package/dist/key-providers/onepassword-provider.d.ts.map +1 -1
- package/dist/key-providers/onepassword-provider.js +159 -67
- package/dist/key-providers/onepassword-provider.js.map +1 -1
- package/dist/key-providers/op-cli.d.ts +78 -0
- package/dist/key-providers/op-cli.d.ts.map +1 -0
- package/dist/key-providers/op-cli.js +123 -0
- package/dist/key-providers/op-cli.js.map +1 -0
- package/dist/key-providers/passphrase-provider.d.ts +20 -0
- package/dist/key-providers/passphrase-provider.d.ts.map +1 -1
- package/dist/key-providers/passphrase-provider.js +49 -0
- package/dist/key-providers/passphrase-provider.js.map +1 -1
- package/dist/key-providers/probe.d.ts +46 -0
- package/dist/key-providers/probe.d.ts.map +1 -0
- package/dist/key-providers/probe.js +70 -0
- package/dist/key-providers/probe.js.map +1 -0
- package/dist/key-providers/types.d.ts +51 -1
- package/dist/key-providers/types.d.ts.map +1 -1
- package/dist/platform/agent-detect.d.ts +39 -3
- package/dist/platform/agent-detect.d.ts.map +1 -1
- package/dist/platform/agent-detect.js +55 -9
- package/dist/platform/agent-detect.js.map +1 -1
- package/dist/platform/session-context.d.ts +77 -0
- package/dist/platform/session-context.d.ts.map +1 -0
- package/dist/platform/session-context.js +83 -0
- package/dist/platform/session-context.js.map +1 -0
- package/dist/secret-state.d.ts +54 -0
- package/dist/secret-state.d.ts.map +1 -0
- package/dist/secret-state.js +2 -0
- package/dist/secret-state.js.map +1 -0
- package/dist/vault/policy.d.ts +35 -4
- package/dist/vault/policy.d.ts.map +1 -1
- package/dist/vault/policy.js +65 -6
- package/dist/vault/policy.js.map +1 -1
- package/dist/vault/session-vault-errors.d.ts +72 -4
- package/dist/vault/session-vault-errors.d.ts.map +1 -1
- package/dist/vault/session-vault-errors.js +95 -4
- package/dist/vault/session-vault-errors.js.map +1 -1
- package/dist/vault/session-vault.d.ts +102 -2
- package/dist/vault/session-vault.d.ts.map +1 -1
- package/dist/vault/session-vault.js +236 -38
- package/dist/vault/session-vault.js.map +1 -1
- package/dist/vault/types.d.ts +42 -1
- package/dist/vault/types.d.ts.map +1 -1
- package/dist/vault/types.js.map +1 -1
- package/dist/vault/vault-env.d.ts +14 -0
- package/dist/vault/vault-env.d.ts.map +1 -1
- package/dist/vault/vault-env.js +15 -0
- package/dist/vault/vault-env.js.map +1 -1
- package/dist/vault/vault-gpg.d.ts +5 -1
- package/dist/vault/vault-gpg.d.ts.map +1 -1
- package/dist/vault/vault-gpg.js +9 -8
- package/dist/vault/vault-gpg.js.map +1 -1
- package/dist/vault/vault-libsecret.d.ts +24 -2
- package/dist/vault/vault-libsecret.d.ts.map +1 -1
- package/dist/vault/vault-libsecret.js +54 -10
- package/dist/vault/vault-libsecret.js.map +1 -1
- package/dist/vault/vault-onepassword.d.ts +118 -0
- package/dist/vault/vault-onepassword.d.ts.map +1 -0
- package/dist/vault/vault-onepassword.js +270 -0
- package/dist/vault/vault-onepassword.js.map +1 -0
- package/dist/vault/vault-utils.d.ts +33 -0
- package/dist/vault/vault-utils.d.ts.map +1 -1
- package/dist/vault/vault-utils.js +61 -0
- package/dist/vault/vault-utils.js.map +1 -1
- package/dist/vault/vault-windows.d.ts +5 -1
- package/dist/vault/vault-windows.d.ts.map +1 -1
- package/dist/vault/vault-windows.js +9 -8
- package/dist/vault/vault-windows.js.map +1 -1
- package/dist/vault/vault.d.ts +116 -4
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +288 -22
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-context detection (#188).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is not platform detection
|
|
5
|
+
*
|
|
6
|
+
* The keychain accessors in this package used to branch on nothing at all:
|
|
7
|
+
* one code path, which implicitly assumed an interactive GUI session. That
|
|
8
|
+
* assumption is wrong in exactly the contexts a credential store matters most
|
|
9
|
+
* — a launchd system-domain daemon and an SSH login both run **without a
|
|
10
|
+
* security session** attached to the user's login keychain:
|
|
11
|
+
*
|
|
12
|
+
* - no consent UI can render, so an item whose ACL is not preauthorized at
|
|
13
|
+
* write time is unreadable and there is nothing to click;
|
|
14
|
+
* - the keychain **search list** cannot be trusted, because it leads with
|
|
15
|
+
* the (locked) login keychain, whose copy of an item shadows a perfectly
|
|
16
|
+
* readable System-keychain copy.
|
|
17
|
+
*
|
|
18
|
+
* `process.platform === "darwin"` answers neither question — a Mac is a Mac in
|
|
19
|
+
* all three contexts. So the branch that matters is *session vs sessionless*,
|
|
20
|
+
* and that is what this module reports.
|
|
21
|
+
*
|
|
22
|
+
* ## The signals, and their limits
|
|
23
|
+
*
|
|
24
|
+
* There is no supported API that says "you are in the launchd system domain",
|
|
25
|
+
* so this is a heuristic and is documented as one. It is deliberately
|
|
26
|
+
* conservative, and the read path is built so that a misclassification is
|
|
27
|
+
* harmless rather than fatal: a sessionless read consults the System keychain
|
|
28
|
+
* *first* and only then falls back to the search list, so classifying an
|
|
29
|
+
* interactive session as sessionless can never lose a credential that was
|
|
30
|
+
* previously readable (see `readKeychainCredential` in
|
|
31
|
+
* `../crypto/darwin-keychain.ts`).
|
|
32
|
+
*
|
|
33
|
+
* The override exists for the case the heuristic cannot see — a daemon whose
|
|
34
|
+
* environment is scrubbed, or a test rig — and it wins over every signal.
|
|
35
|
+
*/
|
|
36
|
+
/** Environment variable that forces the answer. `sessionless` | `interactive`. */
|
|
37
|
+
export declare const SESSION_CONTEXT_OVERRIDE_ENV = "CENTIENT_SECRETS_SESSION_CONTEXT";
|
|
38
|
+
/** Injectable seam. Both fields default to the live process. */
|
|
39
|
+
export interface SessionContextDeps {
|
|
40
|
+
platform: NodeJS.Platform;
|
|
41
|
+
env: NodeJS.ProcessEnv;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Why a context was classified the way it was. Carried so a diagnostic can
|
|
45
|
+
* explain the read path it is about to take instead of asserting it.
|
|
46
|
+
*/
|
|
47
|
+
export type SessionContext = {
|
|
48
|
+
kind: "sessionless";
|
|
49
|
+
/**
|
|
50
|
+
* - `ssh` — an SSH login: `SSH_CONNECTION`/`SSH_TTY`/`SSH_CLIENT` set.
|
|
51
|
+
* - `launchd-daemon` — a launchd-managed job with no controlling
|
|
52
|
+
* terminal (`XPC_SERVICE_NAME` set to a real job label, `TERM` unset).
|
|
53
|
+
* - `forced` — the override said so.
|
|
54
|
+
*/
|
|
55
|
+
reason: "ssh" | "launchd-daemon" | "forced";
|
|
56
|
+
} | {
|
|
57
|
+
kind: "interactive";
|
|
58
|
+
/**
|
|
59
|
+
* - `session` — no sessionless signal was present.
|
|
60
|
+
* - `not-darwin` — the distinction is macOS-specific; elsewhere there is
|
|
61
|
+
* no keychain search list to mistrust.
|
|
62
|
+
* - `forced` — the override said so.
|
|
63
|
+
*/
|
|
64
|
+
reason: "session" | "not-darwin" | "forced";
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Classify the current execution context.
|
|
68
|
+
*
|
|
69
|
+
* Order of precedence: explicit override, then SSH, then launchd-without-a-tty,
|
|
70
|
+
* then interactive. Off darwin the answer is always `interactive` with reason
|
|
71
|
+
* `not-darwin` — the search-list shadowing problem is a macOS keychain problem,
|
|
72
|
+
* and other backends have no equivalent notion.
|
|
73
|
+
*/
|
|
74
|
+
export declare function detectSessionContext(deps?: Partial<SessionContextDeps>): SessionContext;
|
|
75
|
+
/** Convenience predicate over {@link detectSessionContext}. */
|
|
76
|
+
export declare function isSessionlessContext(deps?: Partial<SessionContextDeps>): boolean;
|
|
77
|
+
//# sourceMappingURL=session-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../src/platform/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,kFAAkF;AAClF,eAAO,MAAM,4BAA4B,qCAAqC,CAAC;AAE/E,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,aAAa,CAAC;IACpB;;;;;OAKG;IACH,MAAM,EAAE,KAAK,GAAG,gBAAgB,GAAG,QAAQ,CAAC;CAC7C,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB;;;;;OAKG;IACH,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;CAC7C,CAAC;AAaN;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACrC,cAAc,CAyBhB;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAClC,IAAI,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACrC,OAAO,CAET"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-context detection (#188).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is not platform detection
|
|
5
|
+
*
|
|
6
|
+
* The keychain accessors in this package used to branch on nothing at all:
|
|
7
|
+
* one code path, which implicitly assumed an interactive GUI session. That
|
|
8
|
+
* assumption is wrong in exactly the contexts a credential store matters most
|
|
9
|
+
* — a launchd system-domain daemon and an SSH login both run **without a
|
|
10
|
+
* security session** attached to the user's login keychain:
|
|
11
|
+
*
|
|
12
|
+
* - no consent UI can render, so an item whose ACL is not preauthorized at
|
|
13
|
+
* write time is unreadable and there is nothing to click;
|
|
14
|
+
* - the keychain **search list** cannot be trusted, because it leads with
|
|
15
|
+
* the (locked) login keychain, whose copy of an item shadows a perfectly
|
|
16
|
+
* readable System-keychain copy.
|
|
17
|
+
*
|
|
18
|
+
* `process.platform === "darwin"` answers neither question — a Mac is a Mac in
|
|
19
|
+
* all three contexts. So the branch that matters is *session vs sessionless*,
|
|
20
|
+
* and that is what this module reports.
|
|
21
|
+
*
|
|
22
|
+
* ## The signals, and their limits
|
|
23
|
+
*
|
|
24
|
+
* There is no supported API that says "you are in the launchd system domain",
|
|
25
|
+
* so this is a heuristic and is documented as one. It is deliberately
|
|
26
|
+
* conservative, and the read path is built so that a misclassification is
|
|
27
|
+
* harmless rather than fatal: a sessionless read consults the System keychain
|
|
28
|
+
* *first* and only then falls back to the search list, so classifying an
|
|
29
|
+
* interactive session as sessionless can never lose a credential that was
|
|
30
|
+
* previously readable (see `readKeychainCredential` in
|
|
31
|
+
* `../crypto/darwin-keychain.ts`).
|
|
32
|
+
*
|
|
33
|
+
* The override exists for the case the heuristic cannot see — a daemon whose
|
|
34
|
+
* environment is scrubbed, or a test rig — and it wins over every signal.
|
|
35
|
+
*/
|
|
36
|
+
/** Environment variable that forces the answer. `sessionless` | `interactive`. */
|
|
37
|
+
export const SESSION_CONTEXT_OVERRIDE_ENV = "CENTIENT_SECRETS_SESSION_CONTEXT";
|
|
38
|
+
function resolve(deps) {
|
|
39
|
+
return {
|
|
40
|
+
platform: deps.platform ?? process.platform,
|
|
41
|
+
env: deps.env ?? process.env,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isSet(value) {
|
|
45
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify the current execution context.
|
|
49
|
+
*
|
|
50
|
+
* Order of precedence: explicit override, then SSH, then launchd-without-a-tty,
|
|
51
|
+
* then interactive. Off darwin the answer is always `interactive` with reason
|
|
52
|
+
* `not-darwin` — the search-list shadowing problem is a macOS keychain problem,
|
|
53
|
+
* and other backends have no equivalent notion.
|
|
54
|
+
*/
|
|
55
|
+
export function detectSessionContext(deps = {}) {
|
|
56
|
+
const { platform, env } = resolve(deps);
|
|
57
|
+
const override = env[SESSION_CONTEXT_OVERRIDE_ENV]?.trim();
|
|
58
|
+
if (override === "sessionless")
|
|
59
|
+
return { kind: "sessionless", reason: "forced" };
|
|
60
|
+
if (override === "interactive")
|
|
61
|
+
return { kind: "interactive", reason: "forced" };
|
|
62
|
+
if (platform !== "darwin")
|
|
63
|
+
return { kind: "interactive", reason: "not-darwin" };
|
|
64
|
+
if (isSet(env["SSH_CONNECTION"]) || isSet(env["SSH_TTY"]) || isSet(env["SSH_CLIENT"])) {
|
|
65
|
+
return { kind: "sessionless", reason: "ssh" };
|
|
66
|
+
}
|
|
67
|
+
// launchd exports `XPC_SERVICE_NAME` as the job label for a managed job; a
|
|
68
|
+
// process started from a shell either has it unset or set to the literal
|
|
69
|
+
// `"0"`. A managed job that also has no `TERM` has no controlling terminal
|
|
70
|
+
// and therefore no way to render a consent prompt. Both halves are required:
|
|
71
|
+
// a GUI application is also a launchd job, and demoting one of those to
|
|
72
|
+
// "sessionless" would be a needless change of read path.
|
|
73
|
+
const xpc = env["XPC_SERVICE_NAME"];
|
|
74
|
+
if (isSet(xpc) && xpc?.trim() !== "0" && !isSet(env["TERM"])) {
|
|
75
|
+
return { kind: "sessionless", reason: "launchd-daemon" };
|
|
76
|
+
}
|
|
77
|
+
return { kind: "interactive", reason: "session" };
|
|
78
|
+
}
|
|
79
|
+
/** Convenience predicate over {@link detectSessionContext}. */
|
|
80
|
+
export function isSessionlessContext(deps = {}) {
|
|
81
|
+
return detectSessionContext(deps).kind === "sessionless";
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=session-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-context.js","sourceRoot":"","sources":["../../src/platform/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,4BAA4B,GAAG,kCAAkC,CAAC;AAkC/E,SAAS,OAAO,CAAC,IAAiC;IAChD,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ;QAC3C,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,KAAyB;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAoC,EAAE;IAEtC,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,GAAG,CAAC,4BAA4B,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3D,IAAI,QAAQ,KAAK,aAAa;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACjF,IAAI,QAAQ,KAAK,aAAa;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAEjF,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAEhF,IAAI,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;QACtF,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAChD,CAAC;IAED,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,6EAA6E;IAC7E,wEAAwE;IACxE,yDAAyD;IACzD,MAAM,GAAG,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACpC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC7D,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACpD,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,oBAAoB,CAClC,OAAoC,EAAE;IAEtC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five-way credential state (#188).
|
|
3
|
+
*
|
|
4
|
+
* Every probe in this package answers with this shape, at both credential
|
|
5
|
+
* layers — the `KeyProvider` layer (the vault master key) and the
|
|
6
|
+
* `VaultBackend` cascade (credential values).
|
|
7
|
+
*
|
|
8
|
+
* ## Why five kinds and not a boolean
|
|
9
|
+
*
|
|
10
|
+
* The defect this type exists to kill is the collapse of "there is no such
|
|
11
|
+
* credential" into "I could not read the credential". They have opposite
|
|
12
|
+
* remedies — one says *store it*, the other says *unlock the store / fix the
|
|
13
|
+
* ACL / run me in a context that has a session* — and a caller handed a single
|
|
14
|
+
* `null` cannot tell them apart. A backend that reports `absent` for an item
|
|
15
|
+
* it simply could not open turns a context change into a silent outage: the
|
|
16
|
+
* reader logs "none configured" and everything downstream behaves as though
|
|
17
|
+
* the operator never set it up.
|
|
18
|
+
*
|
|
19
|
+
* So the kinds are deliberately irreducible:
|
|
20
|
+
*
|
|
21
|
+
* - `found` — the credential is there. **Value-free**: a probe never carries
|
|
22
|
+
* secret material, so this variant has no `value` field. Reading the bytes
|
|
23
|
+
* is a separate, explicit call.
|
|
24
|
+
* - `absent` — the store answered, and there is no such item.
|
|
25
|
+
* - `locked` — the item exists but this execution context cannot open it
|
|
26
|
+
* (locked keychain, an ACL that does not preauthorize the reader, no
|
|
27
|
+
* session in which a consent prompt could render).
|
|
28
|
+
* - `not-applicable` — this store cannot exist here at all (a macOS Keychain
|
|
29
|
+
* probe on Linux). Distinct from `failed`: nothing went wrong.
|
|
30
|
+
* - `failed` — the store was contacted and did not give a usable answer.
|
|
31
|
+
* Includes the corrupt case where a successful read yields zero bytes: an
|
|
32
|
+
* empty credential is never reported as `found`.
|
|
33
|
+
*
|
|
34
|
+
* `not-applicable` and `failed` carry a value-free human-readable string for
|
|
35
|
+
* diagnostics. No variant ever carries a credential.
|
|
36
|
+
*
|
|
37
|
+
* See issue #188, ADR-005, and `docs/threat-model.md`.
|
|
38
|
+
*/
|
|
39
|
+
export type SecretState = {
|
|
40
|
+
kind: "found";
|
|
41
|
+
} | {
|
|
42
|
+
kind: "absent";
|
|
43
|
+
} | {
|
|
44
|
+
kind: "locked";
|
|
45
|
+
} | {
|
|
46
|
+
kind: "not-applicable";
|
|
47
|
+
reason: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "failed";
|
|
50
|
+
detail: string;
|
|
51
|
+
};
|
|
52
|
+
/** The discriminant of {@link SecretState}, for exhaustive switches. */
|
|
53
|
+
export type SecretStateKind = SecretState["kind"];
|
|
54
|
+
//# sourceMappingURL=secret-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-state.d.ts","sourceRoot":"","sources":["../src/secret-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-state.js","sourceRoot":"","sources":["../src/secret-state.ts"],"names":[],"mappings":""}
|
package/dist/vault/policy.d.ts
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
* stack described in ADR-002.
|
|
9
9
|
*
|
|
10
10
|
* Execution model:
|
|
11
|
-
* 1. `before` hooks run top-to-bottom. If
|
|
12
|
-
* is aborted and the error propagates to the caller
|
|
11
|
+
* 1. `before` hooks run top-to-bottom. If one throws, the operation
|
|
12
|
+
* is aborted and the error propagates to the caller — but the
|
|
13
|
+
* `after` hooks of already-entered policies (those whose `before`
|
|
14
|
+
* hook already completed) still fire with a `*_rejected` event, so
|
|
15
|
+
* a policy-denied operation is audited rather than vanishing
|
|
16
|
+
* (ADR-002 §1.0.0). The rejecting policy's own `after` hook does
|
|
17
|
+
* not fire — its `before` hook did not complete.
|
|
13
18
|
* 2. The backend operation executes.
|
|
14
19
|
* 3. `after` hooks run bottom-to-top with a structured event
|
|
15
20
|
* describing the outcome. Exceptions in `after` hooks are
|
|
@@ -17,7 +22,7 @@
|
|
|
17
22
|
* failures must never break credential operations.
|
|
18
23
|
*/
|
|
19
24
|
import type { VaultType } from "./types.js";
|
|
20
|
-
export type SecretsEventType = "credential_read" | "credential_read_missing" | "credential_read_failed" | "credential_written" | "credential_write_failed" | "credential_deleted" | "credential_delete_failed" | "credential_enumerated" | "credential_enumerate_failed";
|
|
25
|
+
export type SecretsEventType = "credential_read" | "credential_read_missing" | "credential_read_failed" | "credential_read_rejected" | "credential_written" | "credential_write_failed" | "credential_write_rejected" | "credential_deleted" | "credential_delete_failed" | "credential_delete_rejected" | "credential_enumerated" | "credential_enumerate_failed" | "credential_enumerate_rejected";
|
|
21
26
|
export interface SecretsEvent {
|
|
22
27
|
type: SecretsEventType;
|
|
23
28
|
timestamp: string;
|
|
@@ -33,14 +38,40 @@ export interface SecretsOperation {
|
|
|
33
38
|
key?: string;
|
|
34
39
|
prefix?: string;
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The audit event type emitted when a `before` hook rejects an
|
|
43
|
+
* operation. Single source of truth for the operation→`*_rejected`
|
|
44
|
+
* mapping, shared by the cascade vault and the session vault.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rejectedEventType(op: SecretsOperation): SecretsEventType;
|
|
36
47
|
export interface SecretsPolicy {
|
|
37
48
|
readonly name: string;
|
|
49
|
+
/** `before` hooks may be async; they are awaited before the operation runs. */
|
|
38
50
|
before?(op: SecretsOperation): void | Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* `after` hooks MUST be synchronous (return type is `void`, not
|
|
53
|
+
* `Promise<void>`). The runner invokes them synchronously and catches
|
|
54
|
+
* only synchronous throws — a `Promise` returned from an `after` hook
|
|
55
|
+
* is not awaited, so its rejection would escape as an unhandled
|
|
56
|
+
* rejection rather than being swallowed with the one-time warning. Do
|
|
57
|
+
* audit I/O via a fire-and-forget sink the hook itself owns, or buffer
|
|
58
|
+
* and flush outside the hook.
|
|
59
|
+
*/
|
|
39
60
|
after?(event: SecretsEvent): void;
|
|
40
61
|
}
|
|
41
62
|
export declare function setSecretsPolicies(policies: SecretsPolicy[]): void;
|
|
42
63
|
export declare function getActivePolicies(): readonly SecretsPolicy[];
|
|
43
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Run `before` hooks top-to-bottom. If a hook throws, the operation is
|
|
66
|
+
* rejected: the `after` hooks of the already-entered policies (indices
|
|
67
|
+
* `0..i-1`, bottom-to-top) fire with the event built by
|
|
68
|
+
* `makeRejectionEvent` so the denied operation is still audited, then the
|
|
69
|
+
* original error is re-thrown to abort the operation. The rejecting
|
|
70
|
+
* policy's own `after` hook is intentionally skipped — its `before` hook
|
|
71
|
+
* did not complete, so the policy was not fully entered. See ADR-002
|
|
72
|
+
* §1.0.0.
|
|
73
|
+
*/
|
|
74
|
+
export declare function runBeforeHooks(op: SecretsOperation, makeRejectionEvent: (error: string) => SecretsEvent): Promise<void>;
|
|
44
75
|
export declare function runAfterHooks(event: SecretsEvent): void;
|
|
45
76
|
export interface AuditTrailOptions {
|
|
46
77
|
sink: (event: SecretsEvent) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/vault/policy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/vault/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,yBAAyB,GACzB,wBAAwB,GACxB,0BAA0B,GAC1B,oBAAoB,GACpB,yBAAyB,GACzB,2BAA2B,GAC3B,oBAAoB,GACpB,0BAA0B,GAC1B,4BAA4B,GAC5B,uBAAuB,GACvB,6BAA6B,GAC7B,+BAA+B,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,SAAS,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,gBAAgB,GAAG,gBAAgB,CAiBxE;AAMD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,MAAM,CAAC,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD;;;;;;;;OAQG;IACH,KAAK,CAAC,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACnC;AASD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAGlE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,aAAa,EAAE,CAE5D;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,gBAAgB,EACpB,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,GAClD,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAEvD;AAkCD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACpC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CAejE"}
|
package/dist/vault/policy.js
CHANGED
|
@@ -8,14 +8,42 @@
|
|
|
8
8
|
* stack described in ADR-002.
|
|
9
9
|
*
|
|
10
10
|
* Execution model:
|
|
11
|
-
* 1. `before` hooks run top-to-bottom. If
|
|
12
|
-
* is aborted and the error propagates to the caller
|
|
11
|
+
* 1. `before` hooks run top-to-bottom. If one throws, the operation
|
|
12
|
+
* is aborted and the error propagates to the caller — but the
|
|
13
|
+
* `after` hooks of already-entered policies (those whose `before`
|
|
14
|
+
* hook already completed) still fire with a `*_rejected` event, so
|
|
15
|
+
* a policy-denied operation is audited rather than vanishing
|
|
16
|
+
* (ADR-002 §1.0.0). The rejecting policy's own `after` hook does
|
|
17
|
+
* not fire — its `before` hook did not complete.
|
|
13
18
|
* 2. The backend operation executes.
|
|
14
19
|
* 3. `after` hooks run bottom-to-top with a structured event
|
|
15
20
|
* describing the outcome. Exceptions in `after` hooks are
|
|
16
21
|
* swallowed with a one-time stderr warning — audit infrastructure
|
|
17
22
|
* failures must never break credential operations.
|
|
18
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* The audit event type emitted when a `before` hook rejects an
|
|
26
|
+
* operation. Single source of truth for the operation→`*_rejected`
|
|
27
|
+
* mapping, shared by the cascade vault and the session vault.
|
|
28
|
+
*/
|
|
29
|
+
export function rejectedEventType(op) {
|
|
30
|
+
switch (op.type) {
|
|
31
|
+
case "read":
|
|
32
|
+
return "credential_read_rejected";
|
|
33
|
+
case "write":
|
|
34
|
+
return "credential_write_rejected";
|
|
35
|
+
case "delete":
|
|
36
|
+
return "credential_delete_rejected";
|
|
37
|
+
case "enumerate":
|
|
38
|
+
return "credential_enumerate_rejected";
|
|
39
|
+
default: {
|
|
40
|
+
// Exhaustiveness guard: if a new SecretsOperation["type"] is added
|
|
41
|
+
// without a case here, this assignment fails to compile.
|
|
42
|
+
const unreachable = op.type;
|
|
43
|
+
throw new Error(`unhandled operation type: ${String(unreachable)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
19
47
|
// =============================================================================
|
|
20
48
|
// Policy registry
|
|
21
49
|
// =============================================================================
|
|
@@ -28,14 +56,45 @@ export function setSecretsPolicies(policies) {
|
|
|
28
56
|
export function getActivePolicies() {
|
|
29
57
|
return activePolicies;
|
|
30
58
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Run `before` hooks top-to-bottom. If a hook throws, the operation is
|
|
61
|
+
* rejected: the `after` hooks of the already-entered policies (indices
|
|
62
|
+
* `0..i-1`, bottom-to-top) fire with the event built by
|
|
63
|
+
* `makeRejectionEvent` so the denied operation is still audited, then the
|
|
64
|
+
* original error is re-thrown to abort the operation. The rejecting
|
|
65
|
+
* policy's own `after` hook is intentionally skipped — its `before` hook
|
|
66
|
+
* did not complete, so the policy was not fully entered. See ADR-002
|
|
67
|
+
* §1.0.0.
|
|
68
|
+
*/
|
|
69
|
+
export async function runBeforeHooks(op, makeRejectionEvent) {
|
|
70
|
+
for (let i = 0; i < activePolicies.length; i++) {
|
|
71
|
+
const policy = activePolicies[i];
|
|
72
|
+
if (!policy.before)
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
34
75
|
await policy.before(op);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
79
|
+
runAfterHooksForRange(makeRejectionEvent(msg), i - 1);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
35
82
|
}
|
|
36
83
|
}
|
|
37
84
|
export function runAfterHooks(event) {
|
|
38
|
-
|
|
85
|
+
runAfterHooksForRange(event, activePolicies.length - 1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Run `after` hooks for policies `fromIndex..0` (bottom-to-top). A
|
|
89
|
+
* negative `fromIndex` is a no-op (no policy was entered). Synchronous
|
|
90
|
+
* hook exceptions are swallowed best-effort with a one-time stderr
|
|
91
|
+
* warning — audit-infrastructure failures must never break credential
|
|
92
|
+
* operations. `after` hooks are synchronous by contract (see
|
|
93
|
+
* `SecretsPolicy.after`); a hook that returns a rejected `Promise` is
|
|
94
|
+
* not awaited here, so it would surface as an unhandled rejection.
|
|
95
|
+
*/
|
|
96
|
+
function runAfterHooksForRange(event, fromIndex) {
|
|
97
|
+
for (let i = fromIndex; i >= 0; i--) {
|
|
39
98
|
const policy = activePolicies[i];
|
|
40
99
|
if (policy.after) {
|
|
41
100
|
try {
|
package/dist/vault/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/vault/policy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/vault/policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* sidecar) can throw
|
|
4
|
-
* session-vault.ts.
|
|
2
|
+
* Vault error taxonomy — the package's one `VaultError` hierarchy, extracted
|
|
3
|
+
* so helper modules (file-lock, sidecar, vault-utils) can throw these without
|
|
4
|
+
* creating an import cycle back into session-vault.ts.
|
|
5
|
+
*
|
|
6
|
+
* Most members are SessionVault-specific; `InvalidCredentialKeyError` is
|
|
7
|
+
* raised by the credential cascade (`vault.ts`) and its backends. They share a
|
|
8
|
+
* base class deliberately — a caller wrapping vault work in
|
|
9
|
+
* `catch (e) { if (e instanceof VaultError) … }` should not have to know which
|
|
10
|
+
* storage path produced the failure.
|
|
11
|
+
*
|
|
12
|
+
* This module has no imports on purpose; keep it that way so any vault module
|
|
13
|
+
* can depend on it.
|
|
5
14
|
*
|
|
6
15
|
* Each subclass restores the prototype chain with `Object.setPrototypeOf`
|
|
7
16
|
* so `instanceof VaultError` etc. remains robust when the class is consumed
|
|
8
17
|
* across an ES transpile boundary (L2 — matches the EngramError pattern in
|
|
9
18
|
* `packages/sdk/src/errors.ts`).
|
|
10
19
|
*/
|
|
11
|
-
/** Base class for
|
|
20
|
+
/** Base class for vault errors. */
|
|
12
21
|
export declare class VaultError extends Error {
|
|
13
22
|
readonly code: string;
|
|
14
23
|
constructor(code: string, message: string);
|
|
@@ -31,6 +40,65 @@ export declare class VaultRollbackError extends VaultError {
|
|
|
31
40
|
export declare class VaultClosedError extends VaultError {
|
|
32
41
|
constructor();
|
|
33
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when a rekey's external commit failed AND restoring the prior
|
|
45
|
+
* ciphertext then failed too.
|
|
46
|
+
*
|
|
47
|
+
* This is the one indeterminate state the rekey path can reach: the vault file
|
|
48
|
+
* may still hold the ciphertext written under the NEW key, so neither key can
|
|
49
|
+
* be assumed to be the right one — and, critically, **no key material for
|
|
50
|
+
* either may be discarded**. Callers that clean up on failure (e.g. deleting
|
|
51
|
+
* freshly written passphrase metadata) must special-case this error; deleting
|
|
52
|
+
* the new key's material here can leave the vault permanently unopenable.
|
|
53
|
+
*/
|
|
54
|
+
export declare class VaultRestoreError extends VaultError {
|
|
55
|
+
/** Absolute path of the vault whose state is indeterminate. */
|
|
56
|
+
readonly path: string;
|
|
57
|
+
/** Version the new ciphertext carries, if it is the one on disk. */
|
|
58
|
+
readonly attemptedVersion: number;
|
|
59
|
+
/** The `commit` failure that triggered the rollback attempt. */
|
|
60
|
+
readonly commitCause: unknown;
|
|
61
|
+
/** The failure of the rollback write itself. */
|
|
62
|
+
readonly restoreCause: unknown;
|
|
63
|
+
constructor(
|
|
64
|
+
/** Absolute path of the vault whose state is indeterminate. */
|
|
65
|
+
path: string,
|
|
66
|
+
/** Version the new ciphertext carries, if it is the one on disk. */
|
|
67
|
+
attemptedVersion: number,
|
|
68
|
+
/** The `commit` failure that triggered the rollback attempt. */
|
|
69
|
+
commitCause: unknown,
|
|
70
|
+
/** The failure of the rollback write itself. */
|
|
71
|
+
restoreCause: unknown);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Thrown when a credential key (or a `listCredentials` prefix) does not match
|
|
75
|
+
* the key grammar every vault backend is required to accept.
|
|
76
|
+
*
|
|
77
|
+
* **Why this throws rather than returning a falsy value (#168).** The backends
|
|
78
|
+
* disagree about non-conforming keys: macOS Keychain and the env fallback
|
|
79
|
+
* store anything, while the 1Password, GPG, libsecret and Windows backends
|
|
80
|
+
* cannot address such a key at all. A key that violates the grammar therefore
|
|
81
|
+
* writes successfully on a Mac and is unreadable everywhere else — a silent
|
|
82
|
+
* write/read asymmetry (P2). Returning `false`/`null` would express that as
|
|
83
|
+
* "the write failed" or, worse on the read path, as "no such credential",
|
|
84
|
+
* which is indistinguishable from a genuine miss. The key is malformed, not
|
|
85
|
+
* missing, and only a distinguishable failure says so.
|
|
86
|
+
*/
|
|
87
|
+
export declare class InvalidCredentialKeyError extends VaultError {
|
|
88
|
+
/** The offending key or prefix. A key name, never credential material. */
|
|
89
|
+
readonly key: string;
|
|
90
|
+
/** The operation that was refused — "read", "write", "delete", "enumerate". */
|
|
91
|
+
readonly operation: string;
|
|
92
|
+
/** Whether `key` was supplied as a full key or as a `listCredentials` prefix. */
|
|
93
|
+
readonly kind: "key" | "prefix";
|
|
94
|
+
constructor(
|
|
95
|
+
/** The offending key or prefix. A key name, never credential material. */
|
|
96
|
+
key: string,
|
|
97
|
+
/** The operation that was refused — "read", "write", "delete", "enumerate". */
|
|
98
|
+
operation: string,
|
|
99
|
+
/** Whether `key` was supplied as a full key or as a `listCredentials` prefix. */
|
|
100
|
+
kind?: "key" | "prefix");
|
|
101
|
+
}
|
|
34
102
|
/** Thrown when the write-path file lock can't be acquired within the timeout. */
|
|
35
103
|
export declare class VaultLockError extends VaultError {
|
|
36
104
|
constructor(message: string);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-vault-errors.d.ts","sourceRoot":"","sources":["../../src/vault/session-vault-errors.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session-vault-errors.d.ts","sourceRoot":"","sources":["../../src/vault/session-vault-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,mCAAmC;AACnC,qBAAa,UAAW,SAAQ,KAAK;aACP,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1D;AAED,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,UAAU;gBAClC,OAAO,EAAE,MAAM;CAK5B;AAED,iFAAiF;AACjF,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAK5B;AAED,oEAAoE;AACpE,qBAAa,kBAAmB,SAAQ,UAAU;aAE9B,QAAQ,EAAE,MAAM;aAChB,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAWjC;AAED,8DAA8D;AAC9D,qBAAa,gBAAiB,SAAQ,UAAU;;CAM/C;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,SAAQ,UAAU;IAE7C,+DAA+D;aAC/C,IAAI,EAAE,MAAM;IAC5B,oEAAoE;aACpD,gBAAgB,EAAE,MAAM;IACxC,gEAAgE;aAChD,WAAW,EAAE,OAAO;IACpC,gDAAgD;aAChC,YAAY,EAAE,OAAO;;IAPrC,+DAA+D;IAC/C,IAAI,EAAE,MAAM;IAC5B,oEAAoE;IACpD,gBAAgB,EAAE,MAAM;IACxC,gEAAgE;IAChD,WAAW,EAAE,OAAO;IACpC,gDAAgD;IAChC,YAAY,EAAE,OAAO;CAexC;AAMD;;;;;;;;;;;;;GAaG;AACH,qBAAa,yBAA0B,SAAQ,UAAU;IAErD,0EAA0E;aAC1D,GAAG,EAAE,MAAM;IAC3B,+EAA+E;aAC/D,SAAS,EAAE,MAAM;IACjC,iFAAiF;aACjE,IAAI,EAAE,KAAK,GAAG,QAAQ;;IALtC,0EAA0E;IAC1D,GAAG,EAAE,MAAM;IAC3B,+EAA+E;IAC/D,SAAS,EAAE,MAAM;IACjC,iFAAiF;IACjE,IAAI,GAAE,KAAK,GAAG,QAAgB;CAejD;AAED,iFAAiF;AACjF,qBAAa,cAAe,SAAQ,UAAU;gBAChC,OAAO,EAAE,MAAM;CAK5B"}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* sidecar) can throw
|
|
4
|
-
* session-vault.ts.
|
|
2
|
+
* Vault error taxonomy — the package's one `VaultError` hierarchy, extracted
|
|
3
|
+
* so helper modules (file-lock, sidecar, vault-utils) can throw these without
|
|
4
|
+
* creating an import cycle back into session-vault.ts.
|
|
5
|
+
*
|
|
6
|
+
* Most members are SessionVault-specific; `InvalidCredentialKeyError` is
|
|
7
|
+
* raised by the credential cascade (`vault.ts`) and its backends. They share a
|
|
8
|
+
* base class deliberately — a caller wrapping vault work in
|
|
9
|
+
* `catch (e) { if (e instanceof VaultError) … }` should not have to know which
|
|
10
|
+
* storage path produced the failure.
|
|
11
|
+
*
|
|
12
|
+
* This module has no imports on purpose; keep it that way so any vault module
|
|
13
|
+
* can depend on it.
|
|
5
14
|
*
|
|
6
15
|
* Each subclass restores the prototype chain with `Object.setPrototypeOf`
|
|
7
16
|
* so `instanceof VaultError` etc. remains robust when the class is consumed
|
|
8
17
|
* across an ES transpile boundary (L2 — matches the EngramError pattern in
|
|
9
18
|
* `packages/sdk/src/errors.ts`).
|
|
10
19
|
*/
|
|
11
|
-
/** Base class for
|
|
20
|
+
/** Base class for vault errors. */
|
|
12
21
|
export class VaultError extends Error {
|
|
13
22
|
code;
|
|
14
23
|
constructor(code, message) {
|
|
@@ -56,6 +65,88 @@ export class VaultClosedError extends VaultError {
|
|
|
56
65
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when a rekey's external commit failed AND restoring the prior
|
|
70
|
+
* ciphertext then failed too.
|
|
71
|
+
*
|
|
72
|
+
* This is the one indeterminate state the rekey path can reach: the vault file
|
|
73
|
+
* may still hold the ciphertext written under the NEW key, so neither key can
|
|
74
|
+
* be assumed to be the right one — and, critically, **no key material for
|
|
75
|
+
* either may be discarded**. Callers that clean up on failure (e.g. deleting
|
|
76
|
+
* freshly written passphrase metadata) must special-case this error; deleting
|
|
77
|
+
* the new key's material here can leave the vault permanently unopenable.
|
|
78
|
+
*/
|
|
79
|
+
export class VaultRestoreError extends VaultError {
|
|
80
|
+
path;
|
|
81
|
+
attemptedVersion;
|
|
82
|
+
commitCause;
|
|
83
|
+
restoreCause;
|
|
84
|
+
constructor(
|
|
85
|
+
/** Absolute path of the vault whose state is indeterminate. */
|
|
86
|
+
path,
|
|
87
|
+
/** Version the new ciphertext carries, if it is the one on disk. */
|
|
88
|
+
attemptedVersion,
|
|
89
|
+
/** The `commit` failure that triggered the rollback attempt. */
|
|
90
|
+
commitCause,
|
|
91
|
+
/** The failure of the rollback write itself. */
|
|
92
|
+
restoreCause) {
|
|
93
|
+
super("VAULT_RESTORE_FAILED", `Rekey of ${path} could not be completed OR undone. The caller's commit ` +
|
|
94
|
+
`failed (${describeCause(commitCause)}) and restoring the prior ` +
|
|
95
|
+
`ciphertext then failed (${describeCause(restoreCause)}). The vault ` +
|
|
96
|
+
`file may still hold the ciphertext written under the NEW key at ` +
|
|
97
|
+
`version ${attemptedVersion}. Do NOT discard either key: try the new ` +
|
|
98
|
+
`key first, then the old one. If neither opens it, restore a backup of ` +
|
|
99
|
+
`the vault file and its sidecar.`);
|
|
100
|
+
this.path = path;
|
|
101
|
+
this.attemptedVersion = attemptedVersion;
|
|
102
|
+
this.commitCause = commitCause;
|
|
103
|
+
this.restoreCause = restoreCause;
|
|
104
|
+
this.name = "VaultRestoreError";
|
|
105
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function describeCause(cause) {
|
|
109
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Thrown when a credential key (or a `listCredentials` prefix) does not match
|
|
113
|
+
* the key grammar every vault backend is required to accept.
|
|
114
|
+
*
|
|
115
|
+
* **Why this throws rather than returning a falsy value (#168).** The backends
|
|
116
|
+
* disagree about non-conforming keys: macOS Keychain and the env fallback
|
|
117
|
+
* store anything, while the 1Password, GPG, libsecret and Windows backends
|
|
118
|
+
* cannot address such a key at all. A key that violates the grammar therefore
|
|
119
|
+
* writes successfully on a Mac and is unreadable everywhere else — a silent
|
|
120
|
+
* write/read asymmetry (P2). Returning `false`/`null` would express that as
|
|
121
|
+
* "the write failed" or, worse on the read path, as "no such credential",
|
|
122
|
+
* which is indistinguishable from a genuine miss. The key is malformed, not
|
|
123
|
+
* missing, and only a distinguishable failure says so.
|
|
124
|
+
*/
|
|
125
|
+
export class InvalidCredentialKeyError extends VaultError {
|
|
126
|
+
key;
|
|
127
|
+
operation;
|
|
128
|
+
kind;
|
|
129
|
+
constructor(
|
|
130
|
+
/** The offending key or prefix. A key name, never credential material. */
|
|
131
|
+
key,
|
|
132
|
+
/** The operation that was refused — "read", "write", "delete", "enumerate". */
|
|
133
|
+
operation,
|
|
134
|
+
/** Whether `key` was supplied as a full key or as a `listCredentials` prefix. */
|
|
135
|
+
kind = "key") {
|
|
136
|
+
super("VAULT_INVALID_CREDENTIAL_KEY", `Vault ${operation} refused: credential ${kind} ${JSON.stringify(key)} ` +
|
|
137
|
+
`does not match the required key grammar. Keys must be 2-64 characters ` +
|
|
138
|
+
`of lowercase alphanumerics with '-' or '.' as separators, beginning ` +
|
|
139
|
+
`and ending with an alphanumeric` +
|
|
140
|
+
(kind === "prefix" ? " (a prefix may end with a separator)" : "") +
|
|
141
|
+
`. Non-conforming keys store on some backends and are unaddressable on ` +
|
|
142
|
+
`others, so the value would be written and never read back.`);
|
|
143
|
+
this.key = key;
|
|
144
|
+
this.operation = operation;
|
|
145
|
+
this.kind = kind;
|
|
146
|
+
this.name = "InvalidCredentialKeyError";
|
|
147
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
59
150
|
/** Thrown when the write-path file lock can't be acquired within the timeout. */
|
|
60
151
|
export class VaultLockError extends VaultError {
|
|
61
152
|
constructor(message) {
|