@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/dist/index.js
CHANGED
|
@@ -46,47 +46,269 @@ async function open(key, sealed) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// src/providers.ts
|
|
50
|
+
function staticKey(key) {
|
|
51
|
+
return { key: () => key };
|
|
52
|
+
}
|
|
53
|
+
function envKey(name) {
|
|
54
|
+
return {
|
|
55
|
+
key() {
|
|
56
|
+
const value = process.env[name];
|
|
57
|
+
if (!value) {
|
|
58
|
+
throw new VaultKeyError(`${name} is not set, so there is no key to open with.`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function fileKey(path) {
|
|
65
|
+
return {
|
|
66
|
+
async key() {
|
|
67
|
+
const file = Bun.file(path);
|
|
68
|
+
if (!await file.exists()) {
|
|
69
|
+
throw new VaultKeyError(`No key file at ${path}.`);
|
|
70
|
+
}
|
|
71
|
+
const contents = (await file.text()).trim();
|
|
72
|
+
if (!contents)
|
|
73
|
+
throw new VaultKeyError(`The key file at ${path} is empty.`);
|
|
74
|
+
return contents;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function isKeyProvider(value) {
|
|
79
|
+
return typeof value?.key === "function";
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
// src/vault.ts
|
|
50
83
|
var DEFAULT_PREFIX = "@vault:";
|
|
84
|
+
var DEFAULT_HISTORY_LIMIT = 5;
|
|
51
85
|
var NAME_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
86
|
+
var DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
87
|
+
function randomValue(length = 32, alphabet = DEFAULT_ALPHABET) {
|
|
88
|
+
if (length < 1)
|
|
89
|
+
throw new VaultError("A generated value needs at least one character.");
|
|
90
|
+
if (alphabet.length < 2) {
|
|
91
|
+
throw new VaultError("A generated value needs an alphabet of at least two characters.");
|
|
92
|
+
}
|
|
93
|
+
const ceiling = Math.floor(256 / alphabet.length) * alphabet.length;
|
|
94
|
+
let value = "";
|
|
95
|
+
while (value.length < length) {
|
|
96
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length));
|
|
97
|
+
for (const byte of bytes) {
|
|
98
|
+
if (byte >= ceiling)
|
|
99
|
+
continue;
|
|
100
|
+
value += alphabet[byte % alphabet.length];
|
|
101
|
+
if (value.length === length)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
function toKey(key) {
|
|
108
|
+
const provider = isKeyProvider(key) ? key : staticKey(key);
|
|
109
|
+
return Promise.resolve(provider.key()).then((resolved) => typeof resolved === "string" ? importKey(resolved) : resolved);
|
|
110
|
+
}
|
|
111
|
+
function summarise(record) {
|
|
112
|
+
const { sealed: _sealed, sealedKey: _key, plain, history, ...rest } = record;
|
|
113
|
+
const summary = { ...rest, versions: history.length };
|
|
114
|
+
if (!record.isSealed && plain !== null)
|
|
115
|
+
summary.value = plain;
|
|
116
|
+
return summary;
|
|
117
|
+
}
|
|
118
|
+
function isExpired(record, now) {
|
|
119
|
+
return record.expiresAt !== null && record.expiresAt.getTime() <= now.getTime();
|
|
120
|
+
}
|
|
52
121
|
|
|
53
122
|
class Vault {
|
|
54
|
-
|
|
123
|
+
keySource;
|
|
124
|
+
previousSources;
|
|
125
|
+
keyCache = null;
|
|
126
|
+
previousCache = null;
|
|
55
127
|
store;
|
|
128
|
+
historyLimit;
|
|
129
|
+
generators;
|
|
130
|
+
onAccess;
|
|
56
131
|
prefix;
|
|
57
|
-
constructor({
|
|
58
|
-
|
|
132
|
+
constructor({
|
|
133
|
+
key,
|
|
134
|
+
store,
|
|
135
|
+
prefix = DEFAULT_PREFIX,
|
|
136
|
+
previousKeys = [],
|
|
137
|
+
historyLimit = DEFAULT_HISTORY_LIMIT,
|
|
138
|
+
generators = {},
|
|
139
|
+
onAccess
|
|
140
|
+
}) {
|
|
141
|
+
this.keySource = key;
|
|
142
|
+
this.previousSources = previousKeys;
|
|
59
143
|
this.store = store;
|
|
60
144
|
this.prefix = prefix;
|
|
145
|
+
this.historyLimit = historyLimit;
|
|
146
|
+
this.generators = generators;
|
|
147
|
+
this.onAccess = onAccess;
|
|
148
|
+
}
|
|
149
|
+
master() {
|
|
150
|
+
this.keyCache ??= toKey(this.keySource);
|
|
151
|
+
return this.keyCache;
|
|
152
|
+
}
|
|
153
|
+
retired() {
|
|
154
|
+
this.previousCache ??= this.previousSources.map(toKey);
|
|
155
|
+
return this.previousCache;
|
|
156
|
+
}
|
|
157
|
+
record(event) {
|
|
158
|
+
if (!this.onAccess)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
this.onAccess({ ...event, at: new Date });
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
async unsealWithMaster(sealed) {
|
|
165
|
+
try {
|
|
166
|
+
return await open(await this.master(), sealed);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
for (const previous of this.retired()) {
|
|
169
|
+
try {
|
|
170
|
+
return await open(await previous, sealed);
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async unseal(sealed, sealedKey) {
|
|
177
|
+
if (!sealedKey) {
|
|
178
|
+
return this.unsealWithMaster(sealed);
|
|
179
|
+
}
|
|
180
|
+
const dataKey = await importKey(await this.unsealWithMaster(sealedKey));
|
|
181
|
+
return open(dataKey, sealed);
|
|
182
|
+
}
|
|
183
|
+
async enseal(value) {
|
|
184
|
+
const material = generateKey();
|
|
185
|
+
const dataKey = await importKey(material);
|
|
186
|
+
return {
|
|
187
|
+
sealed: await seal(dataKey, value),
|
|
188
|
+
sealedKey: await seal(await this.master(), material)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
checkName(name) {
|
|
192
|
+
const clean = String(name ?? "").trim();
|
|
193
|
+
if (!NAME_PATTERN.test(clean)) {
|
|
194
|
+
throw new VaultError("A name can be up to 64 characters: letters, numbers, dot, dash or underscore.");
|
|
195
|
+
}
|
|
196
|
+
return clean;
|
|
61
197
|
}
|
|
62
198
|
async list(owner) {
|
|
63
199
|
const records = await this.store.list(owner);
|
|
64
|
-
return records.map(
|
|
200
|
+
return records.map(summarise).sort((a, b) => a.name.localeCompare(b.name));
|
|
65
201
|
}
|
|
66
|
-
async put(owner, name, value,
|
|
202
|
+
async put(owner, name, value, options = {}) {
|
|
67
203
|
const clean = this.checkName(name);
|
|
68
204
|
if (!value)
|
|
69
205
|
throw new VaultError("A secret needs a value.");
|
|
70
|
-
const
|
|
71
|
-
|
|
206
|
+
const existing = await this.store.get(owner, clean);
|
|
207
|
+
if (existing?.isFinal) {
|
|
208
|
+
this.record({ action: "denied", owner, name: clean, detail: "final" });
|
|
209
|
+
throw new VaultError(`"${clean}" is final: it cannot be changed, only deleted.`, 409);
|
|
210
|
+
}
|
|
211
|
+
const now = new Date;
|
|
212
|
+
const isSealed = options.open === undefined ? existing?.isSealed ?? true : !options.open;
|
|
213
|
+
const history = options.keepHistory && existing && existing.sealed ? [
|
|
214
|
+
{
|
|
215
|
+
sealed: existing.sealed,
|
|
216
|
+
sealedKey: existing.sealedKey,
|
|
217
|
+
createdAt: existing.updatedAt
|
|
218
|
+
},
|
|
219
|
+
...existing.history
|
|
220
|
+
].slice(0, this.historyLimit) : existing?.history ?? [];
|
|
221
|
+
const body = isSealed ? { ...await this.enseal(value), plain: null } : { sealed: "", sealedKey: null, plain: value };
|
|
222
|
+
const stored = await this.store.put({
|
|
72
223
|
owner,
|
|
73
224
|
name: clean,
|
|
74
|
-
|
|
75
|
-
|
|
225
|
+
...body,
|
|
226
|
+
isSealed,
|
|
227
|
+
isFinal: options.final === true,
|
|
228
|
+
rotation: options.rotation === undefined ? existing?.rotation ?? null : options.rotation,
|
|
229
|
+
rotatedAt: existing?.rotatedAt ?? null,
|
|
230
|
+
expiresAt: options.expiresAt === undefined ? existing?.expiresAt ?? null : options.expiresAt,
|
|
231
|
+
history,
|
|
232
|
+
metadata: options.metadata ?? existing?.metadata ?? {},
|
|
233
|
+
createdAt: existing?.createdAt ?? now,
|
|
234
|
+
updatedAt: now
|
|
76
235
|
});
|
|
77
|
-
|
|
236
|
+
this.record({ action: "put", owner, name: clean });
|
|
237
|
+
return summarise(stored);
|
|
238
|
+
}
|
|
239
|
+
async rotate(owner, name, value, options = {}) {
|
|
240
|
+
const clean = this.checkName(name);
|
|
241
|
+
const next = value ?? await this.generate(owner, clean);
|
|
242
|
+
this.record({ action: "rotate", owner, name: clean });
|
|
243
|
+
const summary = await this.put(owner, clean, next, { ...options, keepHistory: true });
|
|
244
|
+
const stored = await this.store.get(owner, clean);
|
|
245
|
+
if (stored)
|
|
246
|
+
await this.store.put({ ...stored, rotatedAt: new Date });
|
|
247
|
+
return { ...summary, rotatedAt: new Date };
|
|
248
|
+
}
|
|
249
|
+
async generate(owner, name) {
|
|
250
|
+
const record = await this.require(owner, name);
|
|
251
|
+
const policy = record.rotation;
|
|
252
|
+
if (!policy) {
|
|
253
|
+
throw new VaultError(`"${name}" has no rotation policy, so there is nothing to make the next value with.`);
|
|
254
|
+
}
|
|
255
|
+
if (policy.kind === "random") {
|
|
256
|
+
return randomValue(policy.length, policy.alphabet);
|
|
257
|
+
}
|
|
258
|
+
const generator = policy.generator ? this.generators[policy.generator] : undefined;
|
|
259
|
+
if (!generator) {
|
|
260
|
+
throw new VaultError(`"${name}" wants the "${policy.generator ?? "unnamed"}" generator, which this vault does not have.`, 501);
|
|
261
|
+
}
|
|
262
|
+
return generator({ owner, name, arguments: policy.arguments ?? {} });
|
|
263
|
+
}
|
|
264
|
+
async rotationDue(now = new Date) {
|
|
265
|
+
const records = await this.store.all();
|
|
266
|
+
return records.filter((record) => {
|
|
267
|
+
const every = record.rotation?.every;
|
|
268
|
+
if (!every)
|
|
269
|
+
return false;
|
|
270
|
+
const last = (record.rotatedAt ?? record.createdAt).getTime();
|
|
271
|
+
return now.getTime() - last >= every * 1000;
|
|
272
|
+
}).map(summarise);
|
|
273
|
+
}
|
|
274
|
+
async versions(owner, name) {
|
|
275
|
+
const record = await this.require(owner, name);
|
|
276
|
+
return Promise.all(record.history.map((entry) => this.unseal(entry.sealed, entry.sealedKey)));
|
|
78
277
|
}
|
|
79
278
|
async has(owner, name) {
|
|
80
279
|
return await this.store.get(owner, this.checkName(name)) !== null;
|
|
81
280
|
}
|
|
82
|
-
remove(owner, name) {
|
|
83
|
-
|
|
281
|
+
async remove(owner, name) {
|
|
282
|
+
const removed = await this.store.remove(owner, this.checkName(name));
|
|
283
|
+
if (removed)
|
|
284
|
+
this.record({ action: "remove", owner, name });
|
|
285
|
+
return removed;
|
|
84
286
|
}
|
|
85
|
-
async
|
|
86
|
-
const
|
|
287
|
+
async require(owner, name, now = new Date) {
|
|
288
|
+
const clean = this.checkName(name);
|
|
289
|
+
const record = await this.store.get(owner, clean);
|
|
87
290
|
if (!record)
|
|
88
|
-
throw new VaultError(`No secret named "${
|
|
89
|
-
|
|
291
|
+
throw new VaultError(`No secret named "${clean}" in the vault.`, 404);
|
|
292
|
+
if (isExpired(record, now)) {
|
|
293
|
+
this.record({ action: "denied", owner, name: clean, detail: "expired" });
|
|
294
|
+
throw new VaultError(`"${clean}" expired and can no longer be used.`, 410);
|
|
295
|
+
}
|
|
296
|
+
return record;
|
|
297
|
+
}
|
|
298
|
+
async open(owner, name) {
|
|
299
|
+
const record = await this.require(owner, name);
|
|
300
|
+
const value = record.isSealed ? await this.unseal(record.sealed, record.sealedKey) : record.plain ?? "";
|
|
301
|
+
this.record({ action: "open", owner, name });
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
async read(owner, name) {
|
|
305
|
+
const record = await this.require(owner, name);
|
|
306
|
+
if (record.isSealed) {
|
|
307
|
+
this.record({ action: "denied", owner, name, detail: "sealed" });
|
|
308
|
+
throw new VaultError(`"${name}" is sealed and cannot be read back.`, 403);
|
|
309
|
+
}
|
|
310
|
+
this.record({ action: "read", owner, name });
|
|
311
|
+
return record.plain ?? "";
|
|
90
312
|
}
|
|
91
313
|
async resolve(owner, values) {
|
|
92
314
|
const references = Object.entries(values).filter(([, value]) => value.startsWith(this.prefix));
|
|
@@ -98,17 +320,83 @@ class Vault {
|
|
|
98
320
|
}
|
|
99
321
|
return resolved;
|
|
100
322
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
323
|
+
async reseal(owner) {
|
|
324
|
+
const records = owner ? await this.store.list(owner) : await this.store.all();
|
|
325
|
+
let resealed = 0;
|
|
326
|
+
for (const record of records) {
|
|
327
|
+
if (!record.isSealed || !record.sealed)
|
|
328
|
+
continue;
|
|
329
|
+
const value = await this.unseal(record.sealed, record.sealedKey);
|
|
330
|
+
await this.store.put({ ...record, ...await this.enseal(value) });
|
|
331
|
+
resealed += 1;
|
|
105
332
|
}
|
|
106
|
-
return
|
|
333
|
+
return resealed;
|
|
334
|
+
}
|
|
335
|
+
async rekey(next) {
|
|
336
|
+
const nextKey = await toKey(next);
|
|
337
|
+
const report = { rekeyed: 0, failed: [] };
|
|
338
|
+
for (const record of await this.store.all()) {
|
|
339
|
+
if (!record.isSealed)
|
|
340
|
+
continue;
|
|
341
|
+
try {
|
|
342
|
+
const history = await this.rekeyHistory(record, nextKey);
|
|
343
|
+
if (record.sealedKey) {
|
|
344
|
+
const material = await this.unsealWithMaster(record.sealedKey);
|
|
345
|
+
await this.store.put({
|
|
346
|
+
...record,
|
|
347
|
+
sealedKey: await seal(nextKey, material),
|
|
348
|
+
history
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
const value = await this.unsealWithMaster(record.sealed);
|
|
352
|
+
const material = generateKey();
|
|
353
|
+
const dataKey = await importKey(material);
|
|
354
|
+
await this.store.put({
|
|
355
|
+
...record,
|
|
356
|
+
sealed: await seal(dataKey, value),
|
|
357
|
+
sealedKey: await seal(nextKey, material),
|
|
358
|
+
history
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
report.rekeyed += 1;
|
|
362
|
+
} catch {
|
|
363
|
+
report.failed.push(`${record.owner}/${record.name}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.previousSources = [this.keySource, ...this.previousSources];
|
|
367
|
+
this.keySource = nextKey;
|
|
368
|
+
this.keyCache = Promise.resolve(nextKey);
|
|
369
|
+
this.previousCache = null;
|
|
370
|
+
this.record({
|
|
371
|
+
action: "rekey",
|
|
372
|
+
owner: "",
|
|
373
|
+
name: null,
|
|
374
|
+
detail: `${report.rekeyed} re-sealed, ${report.failed.length} failed`
|
|
375
|
+
});
|
|
376
|
+
return report;
|
|
377
|
+
}
|
|
378
|
+
async rekeyHistory(record, nextKey) {
|
|
379
|
+
return Promise.all(record.history.map(async (entry) => {
|
|
380
|
+
if (!entry.sealedKey)
|
|
381
|
+
return entry;
|
|
382
|
+
const material = await this.unsealWithMaster(entry.sealedKey);
|
|
383
|
+
return { ...entry, sealedKey: await seal(nextKey, material) };
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
async purgeExpired(now = new Date) {
|
|
387
|
+
const expired = (await this.store.all()).filter((record) => isExpired(record, now));
|
|
388
|
+
for (const record of expired) {
|
|
389
|
+
await this.store.remove(record.owner, record.name);
|
|
390
|
+
}
|
|
391
|
+
return expired.length;
|
|
107
392
|
}
|
|
108
393
|
}
|
|
109
394
|
// src/stores/memory.ts
|
|
110
395
|
class MemoryStore {
|
|
111
|
-
records
|
|
396
|
+
records;
|
|
397
|
+
constructor() {
|
|
398
|
+
this.records = new Map;
|
|
399
|
+
}
|
|
112
400
|
static key(owner, name) {
|
|
113
401
|
return `${owner} ${name}`;
|
|
114
402
|
}
|
|
@@ -118,32 +406,158 @@ class MemoryStore {
|
|
|
118
406
|
async list(owner) {
|
|
119
407
|
return [...this.records.values()].filter((record) => record.owner === owner);
|
|
120
408
|
}
|
|
409
|
+
async all() {
|
|
410
|
+
return [...this.records.values()];
|
|
411
|
+
}
|
|
121
412
|
async put(record) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const stored = {
|
|
125
|
-
...record,
|
|
126
|
-
createdAt: this.records.get(key)?.createdAt ?? now,
|
|
127
|
-
updatedAt: now
|
|
128
|
-
};
|
|
129
|
-
this.records.set(key, stored);
|
|
130
|
-
return stored;
|
|
413
|
+
this.records.set(MemoryStore.key(record.owner, record.name), record);
|
|
414
|
+
return record;
|
|
131
415
|
}
|
|
132
416
|
async remove(owner, name) {
|
|
133
417
|
return this.records.delete(MemoryStore.key(owner, name));
|
|
134
418
|
}
|
|
135
419
|
}
|
|
420
|
+
// src/stores/file.ts
|
|
421
|
+
import { open as openFile, readFile, rename, unlink } from "node:fs/promises";
|
|
422
|
+
var MAGIC = "VAULT1";
|
|
423
|
+
|
|
424
|
+
class FileStore {
|
|
425
|
+
path;
|
|
426
|
+
keySource;
|
|
427
|
+
keyCache = null;
|
|
428
|
+
records = null;
|
|
429
|
+
constructor(path, key) {
|
|
430
|
+
this.path = path;
|
|
431
|
+
this.keySource = key;
|
|
432
|
+
}
|
|
433
|
+
key() {
|
|
434
|
+
this.keyCache ??= (async () => {
|
|
435
|
+
const provider = isKeyProvider(this.keySource) ? this.keySource : staticKey(this.keySource);
|
|
436
|
+
const resolved = await provider.key();
|
|
437
|
+
return typeof resolved === "string" ? importKey(resolved) : resolved;
|
|
438
|
+
})();
|
|
439
|
+
return this.keyCache;
|
|
440
|
+
}
|
|
441
|
+
static id(owner, name) {
|
|
442
|
+
return `${owner} ${name}`;
|
|
443
|
+
}
|
|
444
|
+
async load() {
|
|
445
|
+
if (this.records)
|
|
446
|
+
return this.records;
|
|
447
|
+
let contents;
|
|
448
|
+
try {
|
|
449
|
+
contents = (await readFile(this.path, "utf8")).trim();
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error.code !== "ENOENT")
|
|
452
|
+
throw error;
|
|
453
|
+
this.records = new Map;
|
|
454
|
+
return this.records;
|
|
455
|
+
}
|
|
456
|
+
const [magic, payload] = contents.split(`
|
|
457
|
+
`);
|
|
458
|
+
if (magic !== MAGIC || !payload) {
|
|
459
|
+
throw new VaultKeyError(`${this.path} is not a vault file.`);
|
|
460
|
+
}
|
|
461
|
+
const opened = await open(await this.key(), payload);
|
|
462
|
+
const parsed = JSON.parse(opened);
|
|
463
|
+
this.records = new Map(parsed.map((record) => [
|
|
464
|
+
FileStore.id(record.owner, record.name),
|
|
465
|
+
{
|
|
466
|
+
...record,
|
|
467
|
+
expiresAt: record.expiresAt === null ? null : new Date(record.expiresAt),
|
|
468
|
+
history: record.history.map((entry) => ({
|
|
469
|
+
...entry,
|
|
470
|
+
createdAt: new Date(entry.createdAt)
|
|
471
|
+
})),
|
|
472
|
+
rotatedAt: record.rotatedAt === null ? null : new Date(record.rotatedAt),
|
|
473
|
+
createdAt: new Date(record.createdAt),
|
|
474
|
+
updatedAt: new Date(record.updatedAt)
|
|
475
|
+
}
|
|
476
|
+
]));
|
|
477
|
+
return this.records;
|
|
478
|
+
}
|
|
479
|
+
async save() {
|
|
480
|
+
const records = await this.load();
|
|
481
|
+
const sealed = await seal(await this.key(), JSON.stringify([...records.values()]));
|
|
482
|
+
const temporary = `${this.path}.${process.pid}.${Date.now()}.writing`;
|
|
483
|
+
const handle = await openFile(temporary, "w");
|
|
484
|
+
try {
|
|
485
|
+
await handle.writeFile(`${MAGIC}
|
|
486
|
+
${sealed}
|
|
487
|
+
`);
|
|
488
|
+
await handle.sync();
|
|
489
|
+
} finally {
|
|
490
|
+
await handle.close();
|
|
491
|
+
}
|
|
492
|
+
await rename(temporary, this.path);
|
|
493
|
+
}
|
|
494
|
+
async get(owner, name) {
|
|
495
|
+
return (await this.load()).get(FileStore.id(owner, name)) ?? null;
|
|
496
|
+
}
|
|
497
|
+
async list(owner) {
|
|
498
|
+
return [...(await this.load()).values()].filter((record) => record.owner === owner);
|
|
499
|
+
}
|
|
500
|
+
async all() {
|
|
501
|
+
return [...(await this.load()).values()];
|
|
502
|
+
}
|
|
503
|
+
async put(record) {
|
|
504
|
+
const records = await this.load();
|
|
505
|
+
const id = FileStore.id(record.owner, record.name);
|
|
506
|
+
const displaced = records.get(id);
|
|
507
|
+
records.set(id, record);
|
|
508
|
+
try {
|
|
509
|
+
await this.save();
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (displaced)
|
|
512
|
+
records.set(id, displaced);
|
|
513
|
+
else
|
|
514
|
+
records.delete(id);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
return record;
|
|
518
|
+
}
|
|
519
|
+
async remove(owner, name) {
|
|
520
|
+
const records = await this.load();
|
|
521
|
+
const id = FileStore.id(owner, name);
|
|
522
|
+
const removed = records.get(id);
|
|
523
|
+
if (!removed)
|
|
524
|
+
return false;
|
|
525
|
+
records.delete(id);
|
|
526
|
+
try {
|
|
527
|
+
await this.save();
|
|
528
|
+
} catch (error) {
|
|
529
|
+
records.set(id, removed);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
forget() {
|
|
535
|
+
this.records = null;
|
|
536
|
+
}
|
|
537
|
+
async destroy() {
|
|
538
|
+
this.records = new Map;
|
|
539
|
+
await unlink(this.path).catch(() => {});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
136
542
|
export {
|
|
543
|
+
staticKey,
|
|
137
544
|
seal,
|
|
545
|
+
randomValue,
|
|
138
546
|
open,
|
|
547
|
+
isKeyProvider,
|
|
139
548
|
importKey,
|
|
140
549
|
generateKey,
|
|
550
|
+
fileKey,
|
|
551
|
+
envKey,
|
|
141
552
|
VaultKeyError,
|
|
142
553
|
VaultError,
|
|
143
554
|
Vault,
|
|
144
555
|
MemoryStore,
|
|
145
|
-
|
|
556
|
+
FileStore,
|
|
557
|
+
DEFAULT_PREFIX,
|
|
558
|
+
DEFAULT_HISTORY_LIMIT,
|
|
559
|
+
DEFAULT_ALPHABET
|
|
146
560
|
};
|
|
147
561
|
|
|
148
|
-
//# debugId=
|
|
562
|
+
//# debugId=D639A370455C926764756E2164756E21
|
|
149
563
|
//# sourceMappingURL=index.js.map
|