@mstone6969/vault 0.1.0 → 0.5.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 (58) hide show
  1. package/README.md +236 -10
  2. package/dist/crypto.d.ts +99 -2
  3. package/dist/crypto.d.ts.map +1 -1
  4. package/dist/errors.d.ts +122 -4
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +447 -32
  9. package/dist/index.js.map +9 -7
  10. package/dist/providers.d.ts +131 -0
  11. package/dist/providers.d.ts.map +1 -0
  12. package/dist/stores/file.d.ts +190 -0
  13. package/dist/stores/file.d.ts.map +1 -0
  14. package/dist/stores/file.js +1 -0
  15. package/dist/stores/memory.d.ts +98 -6
  16. package/dist/stores/memory.d.ts.map +1 -1
  17. package/dist/stores/sqlite.d.ts +140 -8
  18. package/dist/stores/sqlite.d.ts.map +1 -1
  19. package/dist/stores/sqlite.js +49 -7
  20. package/dist/stores/sqlite.js.map +3 -3
  21. package/dist/types.d.ts +411 -13
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/vault.d.ts +561 -20
  24. package/dist/vault.d.ts.map +1 -1
  25. package/docs/README.md +10 -0
  26. package/docs/index/README.md +48 -0
  27. package/docs/index/classes/FileStore.md +341 -0
  28. package/docs/index/classes/MemoryStore.md +240 -0
  29. package/docs/index/classes/Vault.md +805 -0
  30. package/docs/index/classes/VaultError.md +371 -0
  31. package/docs/index/classes/VaultKeyError.md +370 -0
  32. package/docs/index/functions/envKey.md +43 -0
  33. package/docs/index/functions/fileKey.md +46 -0
  34. package/docs/index/functions/generateKey.md +39 -0
  35. package/docs/index/functions/importKey.md +49 -0
  36. package/docs/index/functions/isKeyProvider.md +43 -0
  37. package/docs/index/functions/open.md +67 -0
  38. package/docs/index/functions/randomValue.md +53 -0
  39. package/docs/index/functions/seal.md +56 -0
  40. package/docs/index/functions/staticKey.md +39 -0
  41. package/docs/index/type-aliases/Generator.md +58 -0
  42. package/docs/index/type-aliases/HistoryEntry.md +65 -0
  43. package/docs/index/type-aliases/KeyProvider.md +74 -0
  44. package/docs/index/type-aliases/PutOptions.md +141 -0
  45. package/docs/index/type-aliases/RekeyReport.md +37 -0
  46. package/docs/index/type-aliases/RotationContext.md +49 -0
  47. package/docs/index/type-aliases/RotationPolicy.md +142 -0
  48. package/docs/index/type-aliases/SecretRecord.md +214 -0
  49. package/docs/index/type-aliases/SecretSummary.md +56 -0
  50. package/docs/index/type-aliases/VaultEvent.md +95 -0
  51. package/docs/index/type-aliases/VaultOptions.md +142 -0
  52. package/docs/index/type-aliases/VaultStore.md +156 -0
  53. package/docs/index/variables/DEFAULT_ALPHABET.md +29 -0
  54. package/docs/index/variables/DEFAULT_HISTORY_LIMIT.md +28 -0
  55. package/docs/index/variables/DEFAULT_PREFIX.md +23 -0
  56. package/docs/stores/sqlite/README.md +11 -0
  57. package/docs/stores/sqlite/classes/SqliteStore.md +307 -0
  58. package/package.json +15 -5
package/README.md CHANGED
@@ -21,7 +21,7 @@ const vault = new Vault({
21
21
  await vault.put("alice", "stripe_key", "sk_live_…")
22
22
 
23
23
  await vault.list("alice")
24
- // [{ owner: "alice", name: "stripe_key", createdAt: …, updatedAt: … }]
24
+ // [{ owner: "alice", name: "stripe_key", metadata: {}, createdAt: …, updatedAt: … }]
25
25
  // — no value, ever
26
26
 
27
27
  await vault.open("alice", "stripe_key") // "sk_live_…"
@@ -30,6 +30,43 @@ await vault.open("alice", "stripe_key") // "sk_live_…"
30
30
  Everything is scoped by an owner, so one vault serves many accounts and two
31
31
  people can both keep a `token` without seeing each other's.
32
32
 
33
+ ## How a value is sealed
34
+
35
+ Every value gets its own **data key**. The value is sealed under that, and only
36
+ the data key is sealed under your master key:
37
+
38
+ ```
39
+ value ──sealed under──▶ data key ──sealed under──▶ master key
40
+ ```
41
+
42
+ Two things follow. Changing the master key re-seals a handful of bytes per
43
+ entry rather than every value, so `rekey` is cheap whatever you keep in there.
44
+ And a data key that leaks opens one value, not all of them.
45
+
46
+ Values written by earlier versions are sealed under the master key directly;
47
+ they still open, and `rekey` gives them an envelope on the way past.
48
+
49
+ ## Metadata
50
+
51
+ A value is sealed, but the facts *about* it usually should not be. `put` takes
52
+ a map of non-secret strings that `list` returns as-is:
53
+
54
+ ```ts
55
+ await vault.put("alice", "deploy", privateKey, {
56
+ kind: "ssh",
57
+ publicKey: "ssh-ed25519 AAAA…",
58
+ })
59
+
60
+ await vault.list("alice")
61
+ // [{ name: "deploy", metadata: { kind: "ssh", publicKey: "ssh-ed25519 AAAA…" }, … }]
62
+ ```
63
+
64
+ That is what lets a listing say what something is — which login a password
65
+ belongs to, which public key pairs with a sealed private one — without opening
66
+ anything. Replacing a value replaces its metadata too.
67
+
68
+ It is stored in the clear. Put nothing in it you would not show.
69
+
33
70
  ## References
34
71
 
35
72
  Configuration can name a secret instead of holding one. `resolve` swaps
@@ -49,9 +86,34 @@ credential is worse than not running it. Change the prefix with
49
86
 
50
87
  ## Storage
51
88
 
52
- `MemoryStore` ships in the main entry. `SqliteStore` is Bun-only — it imports
53
- `bun:sqlite`, so it lives behind a subpath and never loads unless you ask for
54
- it:
89
+ Three stores ship with the package.
90
+
91
+ **`FileStore`** keeps everything in one encrypted file. The other stores seal
92
+ values and leave the rest in the open — SQLite has an `owner` column and a
93
+ `name` column, so anyone who can read the file learns what you keep even if
94
+ they cannot read it. Here the whole index is inside a single envelope, and what
95
+ leaks at rest is the file's size:
96
+
97
+ ```ts
98
+ import { FileStore } from "@mstone6969/vault/stores/file"
99
+
100
+ const store = new FileStore("./secrets.vault", fileKey("/etc/vault.key"))
101
+ ```
102
+
103
+ Give the file a key of its own, or hand it the vault's — sharing means one key
104
+ opens both layers. It is loaded and written whole, so it suits hundreds of
105
+ secrets and one writer, not millions and many.
106
+
107
+ Writes go to a temporary file, are flushed to disk, and are renamed into place,
108
+ so neither a crash nor a power loss leaves a half-written index — which matters
109
+ more here than elsewhere, because the whole index is one envelope and a torn
110
+ file would lose every record rather than one. Nothing locks the file, so two
111
+ writers on the same path are still last-write-wins. Unlike `SqliteStore`, it
112
+ uses only `node:fs`, so it runs on Node as well as Bun.
113
+
114
+ **`MemoryStore`** ships in the main entry. **`SqliteStore`** is Bun-only — it
115
+ imports `bun:sqlite`, so it lives behind a subpath and never loads unless you
116
+ ask for it:
55
117
 
56
118
  ```ts
57
119
  import { SqliteStore } from "@mstone6969/vault/stores/sqlite"
@@ -68,7 +130,13 @@ methods, all scoped by owner, all dealing in sealed strings and never plaintext:
68
130
  type VaultStore = {
69
131
  get(owner: string, name: string): Promise<SecretRecord | null>
70
132
  list(owner: string): Promise<SecretRecord[]>
71
- put(record: { owner: string; name: string; sealed: string }): Promise<SecretRecord>
133
+ all(): Promise<SecretRecord[]>
134
+ put(record: {
135
+ owner: string
136
+ name: string
137
+ sealed: string
138
+ metadata: Record<string, string>
139
+ }): Promise<SecretRecord>
72
140
  remove(owner: string, name: string): Promise<boolean>
73
141
  }
74
142
  ```
@@ -81,18 +149,142 @@ decrypting to something wrong — both cases are covered by tests.
81
149
 
82
150
  The key never leaves your process, and the package never writes it anywhere.
83
151
 
152
+ ## Lifecycle
153
+
154
+ An entry can be more than a value:
155
+
156
+ ```ts
157
+ await vault.put("alice", "region", "eu-west-1", { open: true }) // readable
158
+ await vault.put("alice", "root_ca", pem, { final: true }) // written once
159
+ await vault.put("alice", "token", value, { expiresAt: tomorrow }) // stops working
160
+ await vault.rotate("alice", "deploy", next) // keeps the old one
161
+ ```
162
+
163
+ - **`open`** stores the value in the clear, and `read()` gives it back. For
164
+ configuration rather than credentials; a sealed entry answers 403.
165
+ - **`final`** refuses every future replacement — delete it or live with it.
166
+ - **`expiresAt`** stops the entry resolving once it passes. `purgeExpired()`
167
+ clears them out when you are ready.
168
+ - **`rotate`** keeps what it replaced, up to `historyLimit` (5 by default), and
169
+ `versions()` opens them. A job that read the credential moments before a
170
+ rotation can still finish on what it was given.
171
+
172
+ ## Rotating without knowing the value
173
+
174
+ An entry can carry a **rotation policy**: how to make its next value. That is a
175
+ recipe, never a value, so it is stored in the open beside the metadata — and
176
+ whatever runs the rotation is told how to make the next password without being
177
+ told the current one.
178
+
179
+ ```ts
180
+ await vault.put("alice", "db", firstPassword, {
181
+ metadata: { username: "ada" },
182
+ rotation: { kind: "random", length: 24, every: 86_400 },
183
+ })
184
+
185
+ await vault.rotate("alice", "db") // no value: the policy makes one
186
+ ```
187
+
188
+ For a credential only the far end can mint, name a generator instead. The vault
189
+ stores the *name*; the function stays in your process:
190
+
191
+ ```ts
192
+ const vault = new Vault({
193
+ key,
194
+ store,
195
+ generators: {
196
+ provider: async ({ arguments: args }) => api.mintKey(args.account),
197
+ },
198
+ })
199
+
200
+ await vault.put("alice", "api", currentKey, {
201
+ rotation: { kind: "generator", generator: "provider", arguments: { account: "acct_123" } },
202
+ })
203
+ ```
204
+
205
+ A generator is told which entry is being rotated and its policy's arguments —
206
+ deliberately not the value it is replacing. One that needs the old value can
207
+ ask the vault for it.
208
+
209
+ `rotationDue(now?)` reports entries whose `every` has elapsed since they were
210
+ last rotated. Nothing rotates them for you; schedule it and act on the list.
211
+
212
+ Rotating keeps everything the entry already had — its metadata, its policy, its
213
+ expiry — and records `rotatedAt`. Only the value changes.
214
+
215
+ Anything you leave out of `put` stays as it was, so rotating a credential does
216
+ not quietly forget what kind it is or when it expires.
217
+
218
+ `reseal()` re-seals values under fresh data keys without changing the master
219
+ key — cheap hygiene, so the ciphertext of an unchanged secret stops being
220
+ comparable between two copies of the database.
221
+
222
+ ## Where the key comes from
223
+
224
+ ```ts
225
+ new Vault({ key: envKey("VAULT_KEY"), store }) // an environment variable
226
+ new Vault({ key: fileKey("/etc/vault.key"), store }) // a file
227
+ new Vault({ key: staticKey(material), store }) // one you already have
228
+ ```
229
+
230
+ A provider is one method — write your own for a KMS or anything else. It is
231
+ called the first time a key is actually needed, not when the vault is built, so
232
+ a vault nobody uses never reaches for one.
233
+
234
+ ## Watching what happens
235
+
236
+ ```ts
237
+ new Vault({
238
+ key,
239
+ store,
240
+ onAccess: (event) => log(event), // put, open, read, remove, rotate, rekey, denied
241
+ })
242
+ ```
243
+
244
+ Every refusal is reported too, with the reason — `final`, `sealed`, `expired`.
245
+ The hook is never awaited and its failures are swallowed: an audit trail that
246
+ throws must not take the vault with it.
247
+
248
+ ### Changing the key
249
+
250
+ `rekey` opens every value with the current key and re-seals it under a new one:
251
+
252
+ ```ts
253
+ const report = await vault.rekey(nextKey)
254
+ // { rekeyed: 128, failed: [] }
255
+ ```
256
+
257
+ The old key stays readable for the life of that vault, so a run that stops
258
+ halfway leaves a mix that still opens. Construct the next one with both until
259
+ you are sure:
260
+
261
+ ```ts
262
+ new Vault({ key: nextKey, previousKeys: [oldKey], store })
263
+ ```
264
+
265
+ A value that will not open under any key it holds is **left exactly as it was**
266
+ and named in `failed` — re-sealing what cannot be read would only destroy it.
267
+
84
268
  > [!WARNING]
85
- > Losing the key loses every value stored under it. There is no recovery path,
86
- > by design. Back it up where you would back up a password.
269
+ > Losing every key loses every value sealed under them. Rekey before you retire
270
+ > a key, and back the current one up where you would back up a password.
87
271
 
88
272
  ## API
89
273
 
90
274
  | | |
91
275
  | --- | --- |
92
276
  | `new Vault({ key, store, prefix? })` | Key is base64 or an imported `CryptoKey` |
93
- | `put(owner, name, value)` | Store or replace; returns a summary, no value |
94
- | `list(owner)` | Names and dates, sorted by name |
277
+ | `put(owner, name, value, metadata?)` | Store or replace; returns a summary, no value |
278
+ | `rekey(nextKey)` | Re-seal everything under a new key; returns a report |
279
+ | `list(owner)` | Names, dates and metadata, sorted by name |
95
280
  | `open(owner, name)` | The plaintext — keep it in memory |
281
+ | `read(owner, name)` | The value of an entry stored in the open |
282
+ | `rotate(owner, name, value?, options?)` | Replace, keeping the old value; without one, the policy makes it |
283
+ | `rotationDue(now?)` | Entries whose rotation interval has elapsed |
284
+ | `randomValue(length?, alphabet?)` | An unbiased random string |
285
+ | `versions(owner, name)` | Previous values, newest first |
286
+ | `reseal(owner?)` | Fresh data keys, same master key |
287
+ | `purgeExpired(now?)` | Delete entries whose time is up |
96
288
  | `has(owner, name)` | Whether it exists |
97
289
  | `remove(owner, name)` | `false` if there was nothing to remove |
98
290
  | `resolve(owner, values)` | Substitute `@vault:` references |
@@ -102,9 +294,43 @@ Names are up to 64 characters of letters, numbers, dot, dash or underscore.
102
294
  Bad input throws `VaultError` (with a suggested HTTP `status`); key and
103
295
  ciphertext problems throw `VaultKeyError`.
104
296
 
297
+ ## Versions
298
+
299
+ **0.5.0** — envelope encryption (each value gets its own data key), entry
300
+ lifecycle (`open`, `final`, `expiresAt`, `rotate`/`versions`, `reseal`,
301
+ `purgeExpired`), rotation policies that say how to make the next value without
302
+ storing it, key providers (`envKey`, `fileKey`, `staticKey`), an `onAccess`
303
+ audit hook, `FileStore` — an encrypted single-file store that hides names as
304
+ well as values — and a generated API reference in `docs/`.
305
+
306
+ Breaking: `put`'s fourth argument is now an options object — `put(owner, name,
307
+ value, { metadata })` rather than `put(owner, name, value, metadata)` — and
308
+ `VaultStore.put` takes a whole record. Values written by 0.2.0 and earlier open
309
+ unchanged.
310
+
311
+ **0.3.0** — `rekey(nextKey)` re-seals every value under a new key, and
312
+ `previousKeys` lets a vault open values sealed under keys it has retired.
313
+ `VaultStore` gains `all()`, which a custom store must implement.
314
+
315
+ **0.2.0** — `put` takes optional non-secret `metadata`, and `SecretRecord`
316
+ carries it. Existing calls keep working: metadata defaults to `{}`. A
317
+ `SqliteStore` table written by 0.1.0 gains the column on open.
318
+
319
+ **0.1.0** — first release.
320
+
105
321
  ## Development
106
322
 
107
323
  ```bash
108
- bun test
324
+ bun test # the suite
325
+ bun run test:coverage # the suite, and fail if anything in src is untested
109
326
  bun run typecheck
327
+ bun run docs # generate docs/ from the TSDoc comments
328
+ bun run docs:check # fail if any exported member is undocumented
110
329
  ```
330
+
331
+ Every line and function in `src` is covered, and every exported member carries
332
+ TSDoc; `test:coverage` and `docs:check` enforce both, and `prepublishOnly` runs
333
+ them. Bun accepts `coverageThreshold` in bunfig.toml but does not act on it, so
334
+ the coverage check reads the lcov report itself and exits non-zero on a gap.
335
+
336
+ The generated reference lives in `docs/` and ships with the package.
package/dist/crypto.d.ts CHANGED
@@ -1,7 +1,104 @@
1
- /** A new random key, base64 encoded — store it somewhere safe. */
1
+ /**
2
+ * A new random key, base64 encoded — store it somewhere safe.
3
+ *
4
+ * @returns 32 random bytes, base64. Nothing keeps a copy, so a key that is lost
5
+ * takes every value sealed under it with it.
6
+ * @remarks
7
+ * Used for master keys you generate once and keep, and — inside the vault — for
8
+ * the throwaway data key minted per written value.
9
+ * @example
10
+ * ```ts
11
+ * import { generateKey, importKey, seal } from "@mstone6969/vault"
12
+ *
13
+ * const material = generateKey()
14
+ * const key = await importKey(material)
15
+ * const sealed = await seal(key, "hunter2")
16
+ * ```
17
+ * @see {@link importKey} to turn the string back into a usable key.
18
+ */
2
19
  export declare function generateKey(): string;
3
- /** Imports a base64 key produced by `generateKey`. */
20
+ /**
21
+ * Imports a base64 key produced by {@link generateKey}.
22
+ *
23
+ * @param base64Key The key material, base64 encoded, decoding to exactly 32
24
+ * bytes.
25
+ * @returns A key usable with {@link seal} and {@link open}.
26
+ * @throws {@link VaultKeyError} When the decoded material is not 32 bytes. Note
27
+ * that base64 decoding is lenient: rubbish that is not base64 at all decodes
28
+ * to too few bytes and surfaces here as a length complaint rather than a
29
+ * parse error.
30
+ * @remarks
31
+ * The imported key is not extractable, so the raw bytes cannot be read back out
32
+ * of it — a value only ever leaves via {@link open}.
33
+ * @example
34
+ * ```ts
35
+ * import { importKey, MemoryStore, Vault } from "@mstone6969/vault"
36
+ *
37
+ * const key = await importKey(process.env.VAULT_KEY!)
38
+ * const vault = new Vault({ key, store: new MemoryStore() })
39
+ * ```
40
+ */
4
41
  export declare function importKey(base64Key: string): Promise<CryptoKey>;
42
+ /**
43
+ * Seals a value under a key.
44
+ *
45
+ * @param key The key to seal under.
46
+ * @param plaintext What to seal.
47
+ * @returns `iv:payload`, both base64. A fresh IV every time, so the same value
48
+ * sealed twice gives two different results.
49
+ * @remarks
50
+ * That the output differs every time is the point: an observer with the store
51
+ * in front of them cannot tell that two entries hold the same password, nor
52
+ * that a value was replaced with itself. It also means a sealed string is no
53
+ * good as a cache key or an equality check.
54
+ *
55
+ * Nothing about the key is written into the output, so the caller must
56
+ * remember which key sealed what — the vault does that by keeping each value's
57
+ * data key beside it in {@link SecretRecord.sealedKey}.
58
+ * @example
59
+ * ```ts
60
+ * import { generateKey, importKey, open, seal } from "@mstone6969/vault"
61
+ *
62
+ * const key = await importKey(generateKey())
63
+ * const sealed = await seal(key, "s3cret")
64
+ * sealed.split(":").length // 2
65
+ * await open(key, sealed) // "s3cret"
66
+ * ```
67
+ */
5
68
  export declare function seal(key: CryptoKey, plaintext: string): Promise<string>;
69
+ /**
70
+ * Opens a value sealed by {@link seal}.
71
+ *
72
+ * @param key The key it was sealed under.
73
+ * @param sealed The `iv:payload` string to open.
74
+ * @returns The plaintext.
75
+ * @throws {@link VaultKeyError} When the key is wrong, the value has been
76
+ * altered, or it is not in `iv:payload` form. A wrong key fails rather than
77
+ * returning nonsense, because GCM authenticates what it decrypts.
78
+ * @remarks
79
+ * That failure mode is worth relying on. A caller does not need to check
80
+ * whether what came back looks plausible: if this returns at all, the value is
81
+ * byte for byte what was sealed, under the key that sealed it. It is also why
82
+ * {@link Vault.rekey} can try each key in turn and know which one was right,
83
+ * and why a store that silently corrupts a record produces an error rather
84
+ * than a credential that fails somewhere far away.
85
+ *
86
+ * The error deliberately does not say which of the three went wrong: telling
87
+ * an attacker apart from a typo is not worth telling an attacker anything.
88
+ * @example
89
+ * ```ts
90
+ * import { generateKey, importKey, open, seal, VaultKeyError } from "@mstone6969/vault"
91
+ *
92
+ * const key = await importKey(generateKey())
93
+ * const other = await importKey(generateKey())
94
+ * const sealed = await seal(key, "s3cret")
95
+ *
96
+ * try {
97
+ * await open(other, sealed)
98
+ * } catch (error) {
99
+ * error instanceof VaultKeyError // true — never a wrong plaintext
100
+ * }
101
+ * ```
102
+ */
6
103
  export declare function open(key: CryptoKey, sealed: string): Promise<string>;
7
104
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAUA,kEAAkE;AAClE,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAQrE;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ7E;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1E"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAQrE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAsB,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB1E"}
package/dist/errors.d.ts CHANGED
@@ -1,13 +1,131 @@
1
- /** A caller mistake: a bad name, an empty value, a missing secret. */
1
+ /**
2
+ * A caller mistake: a bad name, an empty value, a missing secret.
3
+ *
4
+ * @remarks
5
+ * Everything the vault throws on purpose is a `VaultError`, so a caller can
6
+ * tell "you asked for something that cannot be done" apart from a bug, and
7
+ * answer accordingly, with one `instanceof`.
8
+ *
9
+ * @example Turning a vault call into an HTTP response
10
+ * ```ts
11
+ * import { Vault, VaultError, MemoryStore, generateKey } from "@mstone6969/vault"
12
+ *
13
+ * const vault = new Vault({ key: generateKey(), store: new MemoryStore() })
14
+ *
15
+ * try {
16
+ * return new Response(await vault.open("alice", "stripe"))
17
+ * } catch (error) {
18
+ * if (error instanceof VaultError) {
19
+ * return new Response(error.message, { status: error.status })
20
+ * }
21
+ * throw error
22
+ * }
23
+ * ```
24
+ *
25
+ * @see {@link VaultKeyError} for the key and ciphertext failures.
26
+ */
2
27
  export declare class VaultError extends Error {
3
- /** Suggested HTTP status, for callers putting this behind an API. */
28
+ /**
29
+ * Suggested HTTP status, for callers putting this behind an API.
30
+ *
31
+ * @remarks
32
+ * It is a suggestion, not a promise about transport: nothing in the
33
+ * vault speaks HTTP. It exists so a handler can map a failure to a
34
+ * response without knowing which check inside the vault failed.
35
+ *
36
+ * The statuses actually thrown:
37
+ *
38
+ * - `422` — bad input: a name that is not 1–64 characters of letters,
39
+ * numbers, dot, dash or underscore; an empty value; a `randomValue`
40
+ * length below one or an alphabet under two characters; a rotation
41
+ * asked of an entry with no rotation policy.
42
+ * - `404` — no secret under that name for that owner.
43
+ * - `409` — the entry is `final`, so it can be deleted but not
44
+ * replaced.
45
+ * - `403` — the entry is sealed, so `read` will not hand it back.
46
+ * `open` is the only way out.
47
+ * - `410` — the entry's `expiresAt` has passed. The record is still
48
+ * there; it just cannot be used.
49
+ * - `501` — the entry's rotation policy names a generator this vault
50
+ * was not constructed with.
51
+ * - `500` — {@link VaultKeyError}'s default: a key or ciphertext
52
+ * problem, which is the operator's fault rather than the caller's.
53
+ *
54
+ * @defaultValue 422
55
+ */
4
56
  readonly status: number;
57
+ /**
58
+ * @param message What the caller did that the vault would not do. Names
59
+ * and owners appear in it; secret values never do, so it is safe to log.
60
+ * @param status Suggested HTTP status. See {@link VaultError.status}.
61
+ */
5
62
  constructor(message: string,
6
- /** Suggested HTTP status, for callers putting this behind an API. */
63
+ /**
64
+ * Suggested HTTP status, for callers putting this behind an API.
65
+ *
66
+ * @remarks
67
+ * It is a suggestion, not a promise about transport: nothing in the
68
+ * vault speaks HTTP. It exists so a handler can map a failure to a
69
+ * response without knowing which check inside the vault failed.
70
+ *
71
+ * The statuses actually thrown:
72
+ *
73
+ * - `422` — bad input: a name that is not 1–64 characters of letters,
74
+ * numbers, dot, dash or underscore; an empty value; a `randomValue`
75
+ * length below one or an alphabet under two characters; a rotation
76
+ * asked of an entry with no rotation policy.
77
+ * - `404` — no secret under that name for that owner.
78
+ * - `409` — the entry is `final`, so it can be deleted but not
79
+ * replaced.
80
+ * - `403` — the entry is sealed, so `read` will not hand it back.
81
+ * `open` is the only way out.
82
+ * - `410` — the entry's `expiresAt` has passed. The record is still
83
+ * there; it just cannot be used.
84
+ * - `501` — the entry's rotation policy names a generator this vault
85
+ * was not constructed with.
86
+ * - `500` — {@link VaultKeyError}'s default: a key or ciphertext
87
+ * problem, which is the operator's fault rather than the caller's.
88
+ *
89
+ * @defaultValue 422
90
+ */
7
91
  status?: number);
8
92
  }
9
- /** The key is the wrong shape, or cannot open what it was given. */
93
+ /**
94
+ * The key is the wrong shape, or cannot open what it was given.
95
+ *
96
+ * @remarks
97
+ * Thrown for a base64 key that is not 32 bytes, a sealed value not in
98
+ * `iv:payload` form, and a value that will not open — which covers both the
99
+ * wrong key and a value someone has altered, since GCM authenticates what it
100
+ * decrypts and cannot tell you which it was. The key providers throw it too,
101
+ * when the environment variable is unset or the key file is missing or empty.
102
+ *
103
+ * Its status is 500 rather than a 4xx because a request that reaches this did
104
+ * nothing wrong: the vault is misconfigured, or its data no longer matches its
105
+ * key.
106
+ *
107
+ * @example Distinguishing a key problem from a caller problem
108
+ * ```ts
109
+ * import { importKey, VaultKeyError } from "@mstone6969/vault"
110
+ *
111
+ * try {
112
+ * await importKey(process.env.VAULT_KEY!)
113
+ * } catch (error) {
114
+ * if (error instanceof VaultKeyError) {
115
+ * console.error("vault key is unusable:", error.message)
116
+ * process.exit(1)
117
+ * }
118
+ * throw error
119
+ * }
120
+ * ```
121
+ *
122
+ * @see {@link VaultError} for the mistakes callers can fix themselves.
123
+ */
10
124
  export declare class VaultKeyError extends VaultError {
125
+ /**
126
+ * @param message What was wrong with the key or the sealed value. It never
127
+ * says which key was tried or what the value held.
128
+ */
11
129
  constructor(message: string);
12
130
  }
13
131
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,qBAAa,UAAW,SAAQ,KAAK;IAG7B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFvB,OAAO,EAAE,MAAM;IACf,qEAAqE;IAC5D,MAAM,GAAE,MAAY;CAKpC;AAED,oEAAoE;AACpE,qBAAa,aAAc,SAAQ,UAAU;gBAC7B,OAAO,EAAE,MAAM;CAI9B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,UAAW,SAAQ,KAAK;IAQ7B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM;IAnC3B;;;;OAIG;gBAEC,OAAO,EAAE,MAAM;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACM,MAAM,GAAE,MAAY;CAKpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,aAAc,SAAQ,UAAU;IACzC;;;OAGG;gBACS,OAAO,EAAE,MAAM;CAI9B"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./vault";
2
2
  export * from "./crypto";
3
3
  export * from "./errors";
4
+ export * from "./providers";
4
5
  export * from "./stores/memory";
6
+ export * from "./stores/file";
5
7
  export * from "./types";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,SAAS,CAAA"}