@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.
Files changed (97) hide show
  1. package/README.md +70 -2
  2. package/dist/cli/secrets-cli.d.ts.map +1 -1
  3. package/dist/cli/secrets-cli.js +191 -32
  4. package/dist/cli/secrets-cli.js.map +1 -1
  5. package/dist/crypto/darwin-keychain.d.ts +394 -0
  6. package/dist/crypto/darwin-keychain.d.ts.map +1 -0
  7. package/dist/crypto/darwin-keychain.js +793 -0
  8. package/dist/crypto/darwin-keychain.js.map +1 -0
  9. package/dist/crypto/vault-common.d.ts +55 -12
  10. package/dist/crypto/vault-common.d.ts.map +1 -1
  11. package/dist/crypto/vault-common.js +74 -71
  12. package/dist/crypto/vault-common.js.map +1 -1
  13. package/dist/environment/EnvironmentManager.d.ts.map +1 -1
  14. package/dist/environment/EnvironmentManager.js +5 -10
  15. package/dist/environment/EnvironmentManager.js.map +1 -1
  16. package/dist/index.d.ts +21 -7
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +24 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/key-providers/keychain-provider.d.ts +30 -2
  21. package/dist/key-providers/keychain-provider.d.ts.map +1 -1
  22. package/dist/key-providers/keychain-provider.js +29 -5
  23. package/dist/key-providers/keychain-provider.js.map +1 -1
  24. package/dist/key-providers/onepassword-provider.d.ts +65 -5
  25. package/dist/key-providers/onepassword-provider.d.ts.map +1 -1
  26. package/dist/key-providers/onepassword-provider.js +159 -67
  27. package/dist/key-providers/onepassword-provider.js.map +1 -1
  28. package/dist/key-providers/op-cli.d.ts +78 -0
  29. package/dist/key-providers/op-cli.d.ts.map +1 -0
  30. package/dist/key-providers/op-cli.js +123 -0
  31. package/dist/key-providers/op-cli.js.map +1 -0
  32. package/dist/key-providers/passphrase-provider.d.ts +20 -0
  33. package/dist/key-providers/passphrase-provider.d.ts.map +1 -1
  34. package/dist/key-providers/passphrase-provider.js +49 -0
  35. package/dist/key-providers/passphrase-provider.js.map +1 -1
  36. package/dist/key-providers/probe.d.ts +46 -0
  37. package/dist/key-providers/probe.d.ts.map +1 -0
  38. package/dist/key-providers/probe.js +70 -0
  39. package/dist/key-providers/probe.js.map +1 -0
  40. package/dist/key-providers/types.d.ts +51 -1
  41. package/dist/key-providers/types.d.ts.map +1 -1
  42. package/dist/platform/agent-detect.d.ts +39 -3
  43. package/dist/platform/agent-detect.d.ts.map +1 -1
  44. package/dist/platform/agent-detect.js +55 -9
  45. package/dist/platform/agent-detect.js.map +1 -1
  46. package/dist/platform/session-context.d.ts +77 -0
  47. package/dist/platform/session-context.d.ts.map +1 -0
  48. package/dist/platform/session-context.js +83 -0
  49. package/dist/platform/session-context.js.map +1 -0
  50. package/dist/secret-state.d.ts +54 -0
  51. package/dist/secret-state.d.ts.map +1 -0
  52. package/dist/secret-state.js +2 -0
  53. package/dist/secret-state.js.map +1 -0
  54. package/dist/vault/policy.d.ts +35 -4
  55. package/dist/vault/policy.d.ts.map +1 -1
  56. package/dist/vault/policy.js +65 -6
  57. package/dist/vault/policy.js.map +1 -1
  58. package/dist/vault/session-vault-errors.d.ts +72 -4
  59. package/dist/vault/session-vault-errors.d.ts.map +1 -1
  60. package/dist/vault/session-vault-errors.js +95 -4
  61. package/dist/vault/session-vault-errors.js.map +1 -1
  62. package/dist/vault/session-vault.d.ts +102 -2
  63. package/dist/vault/session-vault.d.ts.map +1 -1
  64. package/dist/vault/session-vault.js +236 -38
  65. package/dist/vault/session-vault.js.map +1 -1
  66. package/dist/vault/types.d.ts +42 -1
  67. package/dist/vault/types.d.ts.map +1 -1
  68. package/dist/vault/types.js.map +1 -1
  69. package/dist/vault/vault-env.d.ts +14 -0
  70. package/dist/vault/vault-env.d.ts.map +1 -1
  71. package/dist/vault/vault-env.js +15 -0
  72. package/dist/vault/vault-env.js.map +1 -1
  73. package/dist/vault/vault-gpg.d.ts +5 -1
  74. package/dist/vault/vault-gpg.d.ts.map +1 -1
  75. package/dist/vault/vault-gpg.js +9 -8
  76. package/dist/vault/vault-gpg.js.map +1 -1
  77. package/dist/vault/vault-libsecret.d.ts +24 -2
  78. package/dist/vault/vault-libsecret.d.ts.map +1 -1
  79. package/dist/vault/vault-libsecret.js +54 -10
  80. package/dist/vault/vault-libsecret.js.map +1 -1
  81. package/dist/vault/vault-onepassword.d.ts +118 -0
  82. package/dist/vault/vault-onepassword.d.ts.map +1 -0
  83. package/dist/vault/vault-onepassword.js +270 -0
  84. package/dist/vault/vault-onepassword.js.map +1 -0
  85. package/dist/vault/vault-utils.d.ts +33 -0
  86. package/dist/vault/vault-utils.d.ts.map +1 -1
  87. package/dist/vault/vault-utils.js +61 -0
  88. package/dist/vault/vault-utils.js.map +1 -1
  89. package/dist/vault/vault-windows.d.ts +5 -1
  90. package/dist/vault/vault-windows.d.ts.map +1 -1
  91. package/dist/vault/vault-windows.js +9 -8
  92. package/dist/vault/vault-windows.js.map +1 -1
  93. package/dist/vault/vault.d.ts +116 -4
  94. package/dist/vault/vault.d.ts.map +1 -1
  95. package/dist/vault/vault.js +288 -22
  96. package/dist/vault/vault.js.map +1 -1
  97. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=secret-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-state.js","sourceRoot":"","sources":["../src/secret-state.ts"],"names":[],"mappings":""}
@@ -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 any throws, the operation
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
- export declare function runBeforeHooks(op: SecretsOperation): Promise<void>;
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;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,yBAAyB,GACzB,oBAAoB,GACpB,0BAA0B,GAC1B,uBAAuB,GACvB,6BAA6B,CAAC;AAElC,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;AAMD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,CAAC,EAAE,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,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,wBAAsB,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAiBvD;AAMD,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"}
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"}
@@ -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 any throws, the operation
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
- export async function runBeforeHooks(op) {
32
- for (const policy of activePolicies) {
33
- if (policy.before)
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
- for (let i = activePolicies.length - 1; i >= 0; i--) {
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 {
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/vault/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAkDH,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF,IAAI,cAAc,GAAoB,EAAE,CAAC;AACzC,IAAI,mBAAmB,GAAG,KAAK,CAAC;AAEhC,MAAM,UAAU,kBAAkB,CAAC,QAAyB;IAC1D,cAAc,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;IAC/B,mBAAmB,GAAG,KAAK,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAoB;IACvD,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;QACpC,IAAI,MAAM,CAAC,MAAM;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,KAAK,IAAI,CAAC,GAAG,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,cAAc,CAAC,CAAC,CAAE,CAAC;QAClC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBACzB,mBAAmB,GAAG,IAAI,CAAC;oBAC3B,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;oBAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qBAAqB,MAAM,CAAC,IAAI,mCAAmC,GAAG,IAAI,CAC3E,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAWD,MAAM,UAAU,UAAU,CAAC,IAAuB;IAChD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC;IAC/C,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,KAAK,CAAC,KAAmB;YACvB,IACE,CAAC,YAAY;gBACb,CAAC,KAAK,CAAC,IAAI,KAAK,iBAAiB;oBAC/B,KAAK,CAAC,IAAI,KAAK,yBAAyB,CAAC,EAC3C,CAAC;gBACD,OAAO;YACT,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
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
- * SessionVault error classesextracted so helper modules (file-lock,
3
- * sidecar) can throw them without creating an import cycle back into
4
- * session-vault.ts.
2
+ * Vault error taxonomythe 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 SessionVault errors. */
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;;;;;;;;;GASG;AAEH,0CAA0C;AAC1C,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,iFAAiF;AACjF,qBAAa,cAAe,SAAQ,UAAU;gBAChC,OAAO,EAAE,MAAM;CAK5B"}
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
- * SessionVault error classesextracted so helper modules (file-lock,
3
- * sidecar) can throw them without creating an import cycle back into
4
- * session-vault.ts.
2
+ * Vault error taxonomythe 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 SessionVault errors. */
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) {