@mstone6969/vault 0.2.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.
- package/README.md +198 -6
- package/dist/crypto.d.ts +99 -2
- package/dist/crypto.d.ts.map +1 -1
- package/dist/errors.d.ts +122 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -33
- package/dist/index.js.map +9 -7
- package/dist/providers.d.ts +131 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/stores/file.d.ts +190 -0
- package/dist/stores/file.d.ts.map +1 -0
- package/dist/stores/file.js +1 -0
- package/dist/stores/memory.d.ts +98 -7
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/sqlite.d.ts +140 -9
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +47 -12
- package/dist/stores/sqlite.js.map +3 -3
- package/dist/types.d.ts +408 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/vault.d.ts +554 -19
- package/dist/vault.d.ts.map +1 -1
- package/docs/README.md +10 -0
- package/docs/index/README.md +48 -0
- package/docs/index/classes/FileStore.md +341 -0
- package/docs/index/classes/MemoryStore.md +240 -0
- package/docs/index/classes/Vault.md +805 -0
- package/docs/index/classes/VaultError.md +371 -0
- package/docs/index/classes/VaultKeyError.md +370 -0
- package/docs/index/functions/envKey.md +43 -0
- package/docs/index/functions/fileKey.md +46 -0
- package/docs/index/functions/generateKey.md +39 -0
- package/docs/index/functions/importKey.md +49 -0
- package/docs/index/functions/isKeyProvider.md +43 -0
- package/docs/index/functions/open.md +67 -0
- package/docs/index/functions/randomValue.md +53 -0
- package/docs/index/functions/seal.md +56 -0
- package/docs/index/functions/staticKey.md +39 -0
- package/docs/index/type-aliases/Generator.md +58 -0
- package/docs/index/type-aliases/HistoryEntry.md +65 -0
- package/docs/index/type-aliases/KeyProvider.md +74 -0
- package/docs/index/type-aliases/PutOptions.md +141 -0
- package/docs/index/type-aliases/RekeyReport.md +37 -0
- package/docs/index/type-aliases/RotationContext.md +49 -0
- package/docs/index/type-aliases/RotationPolicy.md +142 -0
- package/docs/index/type-aliases/SecretRecord.md +214 -0
- package/docs/index/type-aliases/SecretSummary.md +56 -0
- package/docs/index/type-aliases/VaultEvent.md +95 -0
- package/docs/index/type-aliases/VaultOptions.md +142 -0
- package/docs/index/type-aliases/VaultStore.md +156 -0
- package/docs/index/variables/DEFAULT_ALPHABET.md +29 -0
- package/docs/index/variables/DEFAULT_HISTORY_LIMIT.md +28 -0
- package/docs/index/variables/DEFAULT_PREFIX.md +23 -0
- package/docs/stores/sqlite/README.md +11 -0
- package/docs/stores/sqlite/classes/SqliteStore.md +307 -0
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -30,6 +30,22 @@ 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
|
+
|
|
33
49
|
## Metadata
|
|
34
50
|
|
|
35
51
|
A value is sealed, but the facts *about* it usually should not be. `put` takes
|
|
@@ -70,9 +86,34 @@ credential is worse than not running it. Change the prefix with
|
|
|
70
86
|
|
|
71
87
|
## Storage
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
76
117
|
|
|
77
118
|
```ts
|
|
78
119
|
import { SqliteStore } from "@mstone6969/vault/stores/sqlite"
|
|
@@ -89,6 +130,7 @@ methods, all scoped by owner, all dealing in sealed strings and never plaintext:
|
|
|
89
130
|
type VaultStore = {
|
|
90
131
|
get(owner: string, name: string): Promise<SecretRecord | null>
|
|
91
132
|
list(owner: string): Promise<SecretRecord[]>
|
|
133
|
+
all(): Promise<SecretRecord[]>
|
|
92
134
|
put(record: {
|
|
93
135
|
owner: string
|
|
94
136
|
name: string
|
|
@@ -107,9 +149,125 @@ decrypting to something wrong — both cases are covered by tests.
|
|
|
107
149
|
|
|
108
150
|
The key never leaves your process, and the package never writes it anywhere.
|
|
109
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
|
+
|
|
110
268
|
> [!WARNING]
|
|
111
|
-
> Losing
|
|
112
|
-
>
|
|
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.
|
|
113
271
|
|
|
114
272
|
## API
|
|
115
273
|
|
|
@@ -117,8 +275,16 @@ The key never leaves your process, and the package never writes it anywhere.
|
|
|
117
275
|
| --- | --- |
|
|
118
276
|
| `new Vault({ key, store, prefix? })` | Key is base64 or an imported `CryptoKey` |
|
|
119
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 |
|
|
120
279
|
| `list(owner)` | Names, dates and metadata, sorted by name |
|
|
121
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 |
|
|
122
288
|
| `has(owner, name)` | Whether it exists |
|
|
123
289
|
| `remove(owner, name)` | `false` if there was nothing to remove |
|
|
124
290
|
| `resolve(owner, values)` | Substitute `@vault:` references |
|
|
@@ -130,6 +296,22 @@ ciphertext problems throw `VaultKeyError`.
|
|
|
130
296
|
|
|
131
297
|
## Versions
|
|
132
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
|
+
|
|
133
315
|
**0.2.0** — `put` takes optional non-secret `metadata`, and `SecretRecord`
|
|
134
316
|
carries it. Existing calls keep working: metadata defaults to `{}`. A
|
|
135
317
|
`SqliteStore` table written by 0.1.0 gains the column on open.
|
|
@@ -139,6 +321,16 @@ carries it. Existing calls keep working: metadata defaults to `{}`. A
|
|
|
139
321
|
## Development
|
|
140
322
|
|
|
141
323
|
```bash
|
|
142
|
-
bun test
|
|
324
|
+
bun test # the suite
|
|
325
|
+
bun run test:coverage # the suite, and fail if anything in src is untested
|
|
143
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
|
|
144
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|