@centient/secrets 0.9.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 (93) 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 +20 -6
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +23 -5
  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/session-vault-errors.d.ts +72 -4
  55. package/dist/vault/session-vault-errors.d.ts.map +1 -1
  56. package/dist/vault/session-vault-errors.js +95 -4
  57. package/dist/vault/session-vault-errors.js.map +1 -1
  58. package/dist/vault/session-vault.d.ts +102 -2
  59. package/dist/vault/session-vault.d.ts.map +1 -1
  60. package/dist/vault/session-vault.js +225 -36
  61. package/dist/vault/session-vault.js.map +1 -1
  62. package/dist/vault/types.d.ts +42 -1
  63. package/dist/vault/types.d.ts.map +1 -1
  64. package/dist/vault/types.js.map +1 -1
  65. package/dist/vault/vault-env.d.ts +14 -0
  66. package/dist/vault/vault-env.d.ts.map +1 -1
  67. package/dist/vault/vault-env.js +15 -0
  68. package/dist/vault/vault-env.js.map +1 -1
  69. package/dist/vault/vault-gpg.d.ts +5 -1
  70. package/dist/vault/vault-gpg.d.ts.map +1 -1
  71. package/dist/vault/vault-gpg.js +9 -8
  72. package/dist/vault/vault-gpg.js.map +1 -1
  73. package/dist/vault/vault-libsecret.d.ts +5 -1
  74. package/dist/vault/vault-libsecret.d.ts.map +1 -1
  75. package/dist/vault/vault-libsecret.js +9 -8
  76. package/dist/vault/vault-libsecret.js.map +1 -1
  77. package/dist/vault/vault-onepassword.d.ts +118 -0
  78. package/dist/vault/vault-onepassword.d.ts.map +1 -0
  79. package/dist/vault/vault-onepassword.js +270 -0
  80. package/dist/vault/vault-onepassword.js.map +1 -0
  81. package/dist/vault/vault-utils.d.ts +33 -0
  82. package/dist/vault/vault-utils.d.ts.map +1 -1
  83. package/dist/vault/vault-utils.js +61 -0
  84. package/dist/vault/vault-utils.js.map +1 -1
  85. package/dist/vault/vault-windows.d.ts +5 -1
  86. package/dist/vault/vault-windows.d.ts.map +1 -1
  87. package/dist/vault/vault-windows.js +9 -8
  88. package/dist/vault/vault-windows.js.map +1 -1
  89. package/dist/vault/vault.d.ts +116 -4
  90. package/dist/vault/vault.d.ts.map +1 -1
  91. package/dist/vault/vault.js +251 -12
  92. package/dist/vault/vault.js.map +1 -1
  93. 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":""}
@@ -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) {
@@ -1 +1 @@
1
- {"version":3,"file":"session-vault-errors.js","sourceRoot":"","sources":["../../src/vault/session-vault-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,0CAA0C;AAC1C,MAAM,OAAO,UAAW,SAAQ,KAAK;IACP;IAA5B,YAA4B,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QADW,SAAI,GAAJ,IAAI,CAAQ;QAEtC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;QACzB,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,kFAAkF;AAClF,MAAM,OAAO,gBAAiB,SAAQ,UAAU;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,OAAO,iBAAkB,SAAQ,UAAU;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,oEAAoE;AACpE,MAAM,OAAO,kBAAmB,SAAQ,UAAU;IAE9B;IACA;IAFlB,YACkB,QAAgB,EAChB,MAAc;QAE9B,KAAK,CACH,iCAAiC,EACjC,+DAA+D,QAAQ,IAAI;YACzE,kCAAkC,MAAM,8BAA8B;YACtE,wDAAwD,CAC3D,CAAC;QARc,aAAQ,GAAR,QAAQ,CAAQ;QAChB,WAAM,GAAN,MAAM,CAAQ;QAQ9B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,8DAA8D;AAC9D,MAAM,OAAO,gBAAiB,SAAQ,UAAU;IAC9C;QACE,KAAK,CAAC,cAAc,EAAE,6DAA6D,CAAC,CAAC;QACrF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,OAAO,cAAe,SAAQ,UAAU;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF"}
1
+ {"version":3,"file":"session-vault-errors.js","sourceRoot":"","sources":["../../src/vault/session-vault-errors.ts"],"names":[],"mappings":"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"}
@@ -39,8 +39,8 @@
39
39
  */
40
40
  import type { KeychainProviderOptions } from "../key-providers/keychain-provider.js";
41
41
  import type { KeyProvider, KeyProviderType } from "../key-providers/types.js";
42
- import { VaultError, VaultUnlockError, VaultDecryptError, VaultRollbackError, VaultClosedError, VaultLockError } from "./session-vault-errors.js";
43
- export { VaultError, VaultUnlockError, VaultDecryptError, VaultRollbackError, VaultClosedError, VaultLockError, };
42
+ import { VaultError, VaultUnlockError, VaultDecryptError, VaultRollbackError, VaultClosedError, VaultLockError, VaultRestoreError, InvalidCredentialKeyError } from "./session-vault-errors.js";
43
+ export { VaultError, VaultUnlockError, VaultDecryptError, VaultRollbackError, VaultClosedError, VaultLockError, VaultRestoreError, InvalidCredentialKeyError, };
44
44
  /** Current payload schema version — bump requires a compat migration. */
45
45
  export declare const VAULT_SCHEMA_VERSION = 1;
46
46
  /** Default vault file location — same path the CLI uses, so they share state. */
@@ -174,4 +174,104 @@ export interface SessionVault {
174
174
  * ```
175
175
  */
176
176
  export declare function openVault(opts?: OpenVaultOptions): Promise<SessionVault>;
177
+ /** Options for {@link rekeyVault}. */
178
+ export interface RekeyVaultOptions {
179
+ /** Vault file path. Defaults to the same path the CLI uses. */
180
+ path?: string;
181
+ /** Sidecar path. Defaults to vault directory + `vault.seen-version`. */
182
+ sidecarPath?: string;
183
+ /** The key the vault is currently encrypted under. */
184
+ currentKey: Buffer;
185
+ /** The key the vault should be re-encrypted under. */
186
+ nextKey: Buffer;
187
+ /**
188
+ * Opt-in acceptance of a detected rollback (sidecar version > vault
189
+ * version), mirroring {@link OpenVaultOptions.acceptRollback}. Unlike
190
+ * `openVault`, the sidecar is never lowered to match — the rekeyed vault
191
+ * lands above the recorded high-water mark either way, so accepting a
192
+ * rollback here does not also discard the mark.
193
+ */
194
+ acceptRollback?: boolean;
195
+ /**
196
+ * Opt-in acceptance of a missing sidecar, mirroring
197
+ * {@link OpenVaultOptions.acceptMissingSidecar}. Legacy (pre-`openVault`,
198
+ * AAD-less) vaults never had a sidecar by construction and are exempt.
199
+ */
200
+ acceptMissingSidecar?: boolean;
201
+ /**
202
+ * Invoked once the re-encrypted vault has landed on disk, while the write
203
+ * lock is still held and before the sidecar high-water mark is published.
204
+ *
205
+ * This is the seam that makes an *external* commitment part of the same
206
+ * all-or-nothing step. `migrate --to passphrase` uses it to write
207
+ * `secrets.provider` — a config that names a provider which cannot open the
208
+ * vault (or a vault no configured provider can open) is exactly the
209
+ * half-migrated state this exists to prevent. Throwing restores the prior
210
+ * ciphertext byte-for-byte and rethrows; the sidecar is never advanced on
211
+ * that path, so the restored vault opens cleanly.
212
+ */
213
+ commit?: () => void | Promise<void>;
214
+ }
215
+ /** Outcome of a successful {@link rekeyVault}. */
216
+ export interface RekeyVaultResult {
217
+ /** Number of secrets carried across to the new ciphertext. */
218
+ secretCount: number;
219
+ /** The vault version after the rekey. */
220
+ vaultVersion: number;
221
+ /** True when the source vault was in the pre-`openVault` AAD-less shape. */
222
+ upgradedFromLegacy: boolean;
223
+ /**
224
+ * False when the rekey committed but the sidecar version bump could not be
225
+ * written (warned on stderr). The rekey still succeeded — see the note on
226
+ * the sidecar write in {@link rekeyVault} — but rollback protection is
227
+ * degraded until the next successful vault write catches the sidecar up.
228
+ */
229
+ sidecarPublished: boolean;
230
+ }
231
+ /**
232
+ * Re-encrypt a vault in place under a different master key.
233
+ *
234
+ * Needed whenever the new key cannot be *chosen* — a `PassphraseProvider`
235
+ * derives its key from what the operator types and cannot store a
236
+ * caller-supplied one, so moving a vault to it means rewriting the ciphertext
237
+ * rather than copying the key to a new home. Provider-to-provider moves that
238
+ * only relocate the same key do not need this.
239
+ *
240
+ * The AAD binds the vault's resolved real path and schema, not the key, so the
241
+ * rekeyed ciphertext stays bound to the same file identity. Legacy AAD-less
242
+ * vaults are upgraded to schema 1 in the same step. The new version is one
243
+ * above both the payload's version and the sidecar's high-water mark, so a
244
+ * rekeyed vault can never land below a version already seen.
245
+ *
246
+ * Any process holding this vault open under the old key will fail its next
247
+ * decrypt with {@link VaultDecryptError} — an honest failure, by design: the
248
+ * old key genuinely no longer opens this vault.
249
+ *
250
+ * **Post-commit guarantee.** With one named exception, a throw means `commit`
251
+ * did not succeed and the prior ciphertext is back in place — nothing after a
252
+ * successful `commit` can throw, because the only step that follows it (the
253
+ * sidecar bump) is reported via {@link RekeyVaultResult.sidecarPublished}
254
+ * rather than thrown. Callers may therefore treat a throw as "nothing was
255
+ * committed" and clean up accordingly; that is what makes it safe for the CLI
256
+ * to delete freshly written passphrase metadata on failure.
257
+ *
258
+ * **The exception: {@link VaultRestoreError}.** If `commit` throws *and* the
259
+ * rollback write then fails, the file on disk may still be the new ciphertext.
260
+ * That state is neither committed nor undone, so it gets its own error type —
261
+ * callers MUST special-case it and must NOT discard key material for either
262
+ * key. Every other throw keeps the plain guarantee.
263
+ *
264
+ * @param opts - {@link RekeyVaultOptions}. `currentKey`/`nextKey` are required;
265
+ * both remain owned by the caller and are never zeroed here.
266
+ * @returns {@link RekeyVaultResult}.
267
+ * @throws {@link VaultError} `VAULT_NOT_FOUND` when the vault file is absent.
268
+ * @throws {@link VaultDecryptError} when `currentKey` does not open the vault.
269
+ * @throws {@link VaultError} `VAULT_SIDECAR_MISSING` when the sidecar is absent
270
+ * and neither `acceptMissingSidecar` nor the legacy exemption applies.
271
+ * @throws {@link VaultRollbackError} when the vault version trails the sidecar
272
+ * and `acceptRollback` is not set.
273
+ * @throws {@link VaultError} `VAULT_ENCRYPT_FAILED` when re-encryption fails.
274
+ * @throws Whatever `commit` throws, after restoring the prior ciphertext.
275
+ */
276
+ export declare function rekeyVault(opts: RekeyVaultOptions): Promise<RekeyVaultResult>;
177
277
  //# sourceMappingURL=session-vault.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-vault.d.ts","sourceRoot":"","sources":["../../src/vault/session-vault.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAkBH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAgB9E,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACf,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,GACf,CAAC;AAMF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,QAAuD,CAAC;AAEvF,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,QAKhC,CAAC;AAKF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AA2BzD;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEzE,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,uBAAuB,CAAC;CACpC;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,2EAA2E;IAC3E,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,uEAAuE;IACvE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,yEAAyE;IACzE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,4EAA4E;IAC5E,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,4EAA4E;IAC5E,KAAK,IAAI,IAAI,CAAC;IACd,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AA2ED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,SAAS,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmKlF"}
1
+ {"version":3,"file":"session-vault.d.ts","sourceRoot":"","sources":["../../src/vault/session-vault.ts"],"names":[],"mappings":"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"}