@cloudbitmaps/core 0.22.0 → 0.23.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 (53) hide show
  1. package/README.md +8 -4
  2. package/dist/chunk-7GT632UF.js +641 -0
  3. package/dist/chunk-7GT632UF.js.map +7 -0
  4. package/dist/chunk-CGILDXGP.js +73 -0
  5. package/dist/chunk-CGILDXGP.js.map +7 -0
  6. package/dist/chunk-GKHERVVQ.js +121 -0
  7. package/dist/chunk-GKHERVVQ.js.map +7 -0
  8. package/dist/{chunk-OSU7W2Y3.js → chunk-MVUD4R72.js} +200 -756
  9. package/dist/chunk-MVUD4R72.js.map +7 -0
  10. package/dist/core/audit.d.ts +53 -15
  11. package/dist/core/bit-route.d.ts +2 -0
  12. package/dist/core/blob.d.ts +4 -0
  13. package/dist/core/budget.d.ts +8 -3
  14. package/dist/core/check.d.ts +49 -0
  15. package/dist/core/combine-many.d.ts +11 -0
  16. package/dist/core/consistency.d.ts +11 -2
  17. package/dist/core/cooperative.d.ts +7 -0
  18. package/dist/core/crbm-storage-source.d.ts +7 -4
  19. package/dist/core/crypto.d.ts +5 -0
  20. package/dist/core/discover.d.ts +5 -3
  21. package/dist/core/engine.d.ts +4 -4
  22. package/dist/core/erase-id.d.ts +35 -12
  23. package/dist/core/erasure.d.ts +61 -27
  24. package/dist/core/errors.d.ts +30 -0
  25. package/dist/core/leases.d.ts +2 -0
  26. package/dist/core/load-input.d.ts +48 -0
  27. package/dist/core/load.d.ts +23 -2
  28. package/dist/core/metrics.d.ts +21 -0
  29. package/dist/core/pinned-storage-source.d.ts +27 -8
  30. package/dist/core/pointer-id.d.ts +9 -0
  31. package/dist/core/ports.d.ts +100 -7
  32. package/dist/core/preflight.d.ts +80 -0
  33. package/dist/core/retention-sweep.d.ts +60 -15
  34. package/dist/core/retention.d.ts +17 -6
  35. package/dist/core/retry.d.ts +1 -2
  36. package/dist/core/rollback.d.ts +22 -12
  37. package/dist/core/validate.d.ts +33 -2
  38. package/dist/driver-kit.d.ts +3 -3
  39. package/dist/driver-kit.js +61 -20
  40. package/dist/driver-kit.js.map +2 -2
  41. package/dist/drivers/_shared/object-registry.d.ts +24 -1
  42. package/dist/drivers/_shared/options.d.ts +15 -7
  43. package/dist/drivers/backends.d.ts +17 -2
  44. package/dist/drivers/crypto.d.ts +17 -2
  45. package/dist/export/index.d.ts +16 -2
  46. package/dist/index.d.ts +10 -9
  47. package/dist/index.js +1743 -1701
  48. package/dist/index.js.map +4 -4
  49. package/dist/node.d.ts +14 -0
  50. package/dist/node.js +1064 -0
  51. package/dist/node.js.map +7 -0
  52. package/package.json +11 -2
  53. package/dist/chunk-OSU7W2Y3.js.map +0 -7
package/dist/node.js ADDED
@@ -0,0 +1,1064 @@
1
+ import {
2
+ BufferSink,
3
+ checkTailLength,
4
+ segmentKey
5
+ } from "./chunk-GKHERVVQ.js";
6
+ import {
7
+ DEFAULT_NAMESPACE,
8
+ applyRegistryPatch,
9
+ checkDeleteOptions,
10
+ checkNow,
11
+ checkRegistryWriteOptions,
12
+ drawIncarnation,
13
+ drawWrite,
14
+ entropyIsAvailable,
15
+ namespacePathPart,
16
+ newIncarnationToken,
17
+ nextRegistryToken,
18
+ parseRegistryEnvelope,
19
+ recordFromNew,
20
+ serializeRegistryEnvelope,
21
+ validateNewRegistryRecord,
22
+ validateRegistryPatch,
23
+ webCryptoEntropy
24
+ } from "./chunk-7GT632UF.js";
25
+ import {
26
+ IntegrityError,
27
+ KeyUnavailableError,
28
+ NotFoundError,
29
+ TransientError,
30
+ ValidationError,
31
+ WriteConflictError,
32
+ brandAsBackend,
33
+ decodeNameFromPath,
34
+ encodeNameForPath,
35
+ incarnationOf,
36
+ refuseUnknownOptions,
37
+ requireInteger,
38
+ validateSegmentRef
39
+ } from "./chunk-MVUD4R72.js";
40
+
41
+ // src/drivers/backends.ts
42
+ import { join as join5 } from "node:path";
43
+
44
+ // src/drivers/memory.ts
45
+ import { createHash } from "node:crypto";
46
+ var MemoryRegistryDriver = class {
47
+ rows = /* @__PURE__ */ new Map();
48
+ now;
49
+ entropy;
50
+ seq = 0;
51
+ constructor(options = {}) {
52
+ this.now = options.now ?? (() => Date.now());
53
+ this.entropy = options.entropy ?? webCryptoEntropy;
54
+ }
55
+ nextToken(incarnation) {
56
+ this.seq += 1;
57
+ return `${incarnation}.${this.seq}.${drawWrite(this.entropy)}`;
58
+ }
59
+ /** `conditionalDelete`: a delete checks the token and removes the row in one step, with no await between. */
60
+ capabilities() {
61
+ return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete: true } : { strongRead: true, canWrite: false, conditionalDelete: true };
62
+ }
63
+ /** Every write reports its token to `onToken` before it is applied. */
64
+ get reportsWriteTokens() {
65
+ return true;
66
+ }
67
+ async get(ref) {
68
+ validateSegmentRef(ref);
69
+ const row = this.rows.get(segmentKey(ref));
70
+ return row ? structuredClone(row) : null;
71
+ }
72
+ async create(ref, record, options) {
73
+ checkRegistryWriteOptions(options);
74
+ validateSegmentRef(ref);
75
+ const checked = validateNewRegistryRecord(record);
76
+ const key = segmentKey(ref);
77
+ if (this.rows.has(key)) {
78
+ throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
79
+ }
80
+ const token = this.nextToken(drawIncarnation(this.entropy));
81
+ options?.onToken?.(token);
82
+ this.rows.set(key, structuredClone(recordFromNew(ref, checked, this.now(), token)));
83
+ return { token };
84
+ }
85
+ /**
86
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the token is compared with the stored row in the same step
87
+ * as the write, in memory, so a stale one fails exactly as a lost race does.
88
+ */
89
+ async compareAndSwap(ref, expected, patch, options) {
90
+ checkRegistryWriteOptions(options);
91
+ validateSegmentRef(ref);
92
+ const checked = validateRegistryPatch(patch);
93
+ const key = segmentKey(ref);
94
+ const existing = this.rows.get(key);
95
+ if (!existing || existing.token !== expected) {
96
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
97
+ }
98
+ const token = this.nextToken(incarnationOf(existing.token));
99
+ options?.onToken?.(token);
100
+ this.rows.set(key, structuredClone(applyRegistryPatch(existing, checked, this.now(), token)));
101
+ return { token };
102
+ }
103
+ async *list(namespace) {
104
+ if (namespace !== void 0) validateSegmentRef({ segment: "x", namespace });
105
+ for (const row of [...this.rows.values()]) {
106
+ if (namespace === void 0 || row.namespace === namespace) yield structuredClone(row);
107
+ }
108
+ }
109
+ async delete(ref, expected) {
110
+ validateSegmentRef(ref);
111
+ const key = segmentKey(ref);
112
+ if (expected !== void 0 && this.rows.get(key)?.token !== expected) {
113
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
114
+ }
115
+ this.rows.delete(key);
116
+ }
117
+ };
118
+ var MemoryStorageDriver = class {
119
+ objects = /* @__PURE__ */ new Map();
120
+ stored = 0;
121
+ capabilities() {
122
+ return {
123
+ rangeRead: true,
124
+ maxObjectBytes: Number.MAX_SAFE_INTEGER,
125
+ conditionalPut: true,
126
+ conditionalDelete: true
127
+ };
128
+ }
129
+ async putImmutable(key, write) {
130
+ const k = genObjectKey(key);
131
+ const sink = new BufferSink();
132
+ await write(sink);
133
+ const body = sink.bytes();
134
+ if (this.objects.has(k)) {
135
+ throw new WriteConflictError(
136
+ `generation already exists (write-once): ${key.segment}.${key.generation}`
137
+ );
138
+ }
139
+ this.stored += 1;
140
+ this.objects.set(k, { body, version: String(this.stored) });
141
+ return { size: body.length, sha256: createHash("sha256").update(body).digest("hex") };
142
+ }
143
+ async getRange(key, offset, length) {
144
+ requireInteger(offset, { min: 0, what: "range offset" });
145
+ requireInteger(length, { min: 0, what: "range length" });
146
+ const { body } = this.require(key);
147
+ if (offset + length > body.length) {
148
+ throw new ValidationError(
149
+ `range [${offset}, ${offset + length}) out of bounds for ${body.length}B`
150
+ );
151
+ }
152
+ return body.slice(offset, offset + length);
153
+ }
154
+ async getTail(key, maxBytes) {
155
+ checkTailLength(maxBytes);
156
+ const { body, version } = this.require(key);
157
+ const take = Math.min(Math.max(maxBytes, 0), body.length);
158
+ return { bytes: body.slice(body.length - take), size: body.length, version };
159
+ }
160
+ async delete(key, options) {
161
+ const ifVersion = checkDeleteOptions(options);
162
+ const k = genObjectKey(key);
163
+ const stored = this.objects.get(k);
164
+ if (ifVersion !== void 0 && stored !== void 0 && stored.version !== ifVersion) {
165
+ throw new WriteConflictError(
166
+ `generation ${key.segment}.${key.generation} is another object than the version given; not deleted`
167
+ );
168
+ }
169
+ this.objects.delete(k);
170
+ }
171
+ async *list(ref) {
172
+ validateSegmentRef(ref);
173
+ const prefix = `${segmentKey(ref)} `;
174
+ for (const k of [...this.objects.keys()]) {
175
+ if (k.startsWith(prefix)) {
176
+ yield {
177
+ namespace: ref.namespace,
178
+ segment: ref.segment,
179
+ generation: Number(k.slice(prefix.length))
180
+ };
181
+ }
182
+ }
183
+ }
184
+ require(key) {
185
+ const stored = this.objects.get(genObjectKey(key));
186
+ if (stored === void 0) {
187
+ throw new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
188
+ }
189
+ return stored;
190
+ }
191
+ };
192
+ function genObjectKey(key) {
193
+ validateSegmentRef(key);
194
+ requireInteger(key.generation, { min: 0, what: "generation" });
195
+ return `${segmentKey(key)} ${key.generation}`;
196
+ }
197
+
198
+ // src/drivers/localfs/storage.ts
199
+ import { constants as FS2 } from "node:fs";
200
+ import { createHash as createHash2 } from "node:crypto";
201
+ import { link, open as open2, readdir as readdir2, unlink as unlink2 } from "node:fs/promises";
202
+ import { dirname as dirname3 } from "node:path";
203
+
204
+ // src/drivers/localfs/paths.ts
205
+ import { join } from "node:path";
206
+ var MAX_COMPONENT_BYTES = 255;
207
+ var TEMP_SUFFIX_BYTES = 1 + 36 + 4;
208
+ function assertFits(component, extraBytes) {
209
+ if (component.length + extraBytes > MAX_COMPONENT_BYTES) {
210
+ throw new ValidationError(
211
+ `a name is too long for a file name on this backend: the namespace, or the segment with its generation and the temp file's suffix, must fit in ${MAX_COMPONENT_BYTES} bytes once encoded`
212
+ );
213
+ }
214
+ }
215
+ function assertStorageNamesFit(key) {
216
+ validateSegmentRef(key);
217
+ assertFits(namespacePathPart(key.namespace), 0);
218
+ assertFits(storageObjectFilename(key.segment, key.generation), TEMP_SUFFIX_BYTES);
219
+ }
220
+ function assertRegistryNamesFit(ref) {
221
+ validateSegmentRef(ref);
222
+ assertFits(namespacePathPart(ref.namespace), 0);
223
+ assertFits(`${encodeNameForPath(ref.segment)}${REGISTRY_SUFFIX}`, TEMP_SUFFIX_BYTES);
224
+ }
225
+ function segmentsDir(root, ref) {
226
+ validateSegmentRef(ref);
227
+ return join(root, namespacePathPart(ref.namespace), "segments");
228
+ }
229
+ function storageObjectPath(root, key) {
230
+ validateSegmentRef(key);
231
+ requireInteger(key.generation, { min: 0, what: "generation" });
232
+ return join(
233
+ segmentsDir(root, { segment: key.segment, namespace: key.namespace }),
234
+ storageObjectFilename(key.segment, key.generation)
235
+ );
236
+ }
237
+ function storageObjectFilename(segment, generation) {
238
+ return `${encodeNameForPath(segment)}.${generation}.crbm`;
239
+ }
240
+ function parseGeneration(segment, filename) {
241
+ const prefix = `${encodeNameForPath(segment)}.`;
242
+ const suffix = ".crbm";
243
+ if (!filename.startsWith(prefix) || !filename.endsWith(suffix)) return null;
244
+ const middle = filename.slice(prefix.length, filename.length - suffix.length);
245
+ if (!/^(0|[1-9]\d*)$/.test(middle)) return null;
246
+ const gen = Number(middle);
247
+ return Number.isSafeInteger(gen) ? gen : null;
248
+ }
249
+ var REGISTRY_SUFFIX = ".reg";
250
+ function registryDir(root, namespace) {
251
+ if (namespace !== void 0) validateSegmentRef({ segment: "x", namespace });
252
+ return join(root, namespacePathPart(namespace), "registry");
253
+ }
254
+ function registryRowPath(root, ref) {
255
+ validateSegmentRef(ref);
256
+ return join(
257
+ registryDir(root, ref.namespace),
258
+ `${encodeNameForPath(ref.segment)}${REGISTRY_SUFFIX}`
259
+ );
260
+ }
261
+ function parseRegistryRow(filename) {
262
+ if (!filename.endsWith(REGISTRY_SUFFIX)) return null;
263
+ const stem = filename.slice(0, filename.length - REGISTRY_SUFFIX.length);
264
+ const segment = decodeNameFromPath(stem);
265
+ if (encodeNameForPath(segment) !== stem) return null;
266
+ try {
267
+ validateSegmentRef({ segment });
268
+ } catch {
269
+ return null;
270
+ }
271
+ return segment;
272
+ }
273
+ function parseNamespaceDir(entry) {
274
+ if (entry === DEFAULT_NAMESPACE) return { namespace: void 0 };
275
+ const namespace = decodeNameFromPath(entry);
276
+ if (encodeNameForPath(namespace) !== entry) return null;
277
+ try {
278
+ validateSegmentRef({ segment: "x", namespace });
279
+ } catch {
280
+ return null;
281
+ }
282
+ return { namespace };
283
+ }
284
+
285
+ // src/drivers/localfs/exact-case.ts
286
+ import { readdir, realpath, stat } from "node:fs/promises";
287
+ import { dirname as dirname2, join as join3, parse, relative, resolve, sep } from "node:path";
288
+
289
+ // src/drivers/localfs/fs-util.ts
290
+ import { constants as FS } from "node:fs";
291
+ import { randomUUID } from "node:crypto";
292
+ import { lstat, mkdir, opendir, open, unlink } from "node:fs/promises";
293
+ import { dirname, join as join2 } from "node:path";
294
+ var isCode = (err, code) => err instanceof Error && err.code === code;
295
+ var isAbsent = (err) => isCode(err, "ENOENT") || isCode(err, "ENOTDIR") || isCode(err, "ENAMETOOLONG");
296
+ var TRANSIENT_FS_CODES = /* @__PURE__ */ new Set(["EBUSY", "EAGAIN", "EMFILE", "ENFILE", "ETIMEDOUT"]);
297
+ var isFsTransient = (err) => err instanceof Error && TRANSIENT_FS_CODES.has(err.code ?? "");
298
+ var mapFsError = (err) => isFsTransient(err) ? new TransientError(`transient filesystem fault: ${err.code}`, { cause: err }) : err;
299
+ var O_NOFOLLOW = FS.O_NOFOLLOW ?? 0;
300
+ var FILE_MODE = 384;
301
+ var DIR_MODE = 448;
302
+ async function fsyncDir(dir) {
303
+ try {
304
+ const handle = await open(dir, "r");
305
+ try {
306
+ await handle.sync();
307
+ } finally {
308
+ await handle.close();
309
+ }
310
+ } catch {
311
+ }
312
+ }
313
+ async function writeAll(handle, bytes) {
314
+ let offset = 0;
315
+ while (offset < bytes.length) {
316
+ const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
317
+ if (bytesWritten <= 0) {
318
+ throw Object.assign(new Error("a write made no progress: the device is full"), {
319
+ code: "ENOSPC"
320
+ });
321
+ }
322
+ offset += bytesWritten;
323
+ }
324
+ }
325
+ async function makeDirs(dir, top) {
326
+ if (!parentsMade.has(top)) {
327
+ await mkdir(dirname(top), { recursive: true });
328
+ parentsMade.add(top);
329
+ }
330
+ await mkdir(dir, { recursive: true, mode: DIR_MODE });
331
+ }
332
+ var parentsMade = /* @__PURE__ */ new Set();
333
+ var tempPathFor = (finalPath) => `${finalPath}.${randomUUID()}.tmp`;
334
+ var TEMP_NAME = /\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/;
335
+ var ORPHAN_TEMP_AGE_MS = 24 * 60 * 60 * 1e3;
336
+ var SWEEP_INTERVAL_MS = 60 * 60 * 1e3;
337
+ var lastSwept = /* @__PURE__ */ new Map();
338
+ async function sweepOrphanTemps(dir, now = Date.now()) {
339
+ const last = lastSwept.get(dir);
340
+ if (last !== void 0 && now - last < SWEEP_INTERVAL_MS) return;
341
+ lastSwept.set(dir, now);
342
+ try {
343
+ const entries = await opendir(dir);
344
+ for await (const entry of entries) {
345
+ if (!TEMP_NAME.test(entry.name)) continue;
346
+ const path = join2(dir, entry.name);
347
+ const info = await lstat(path).catch(() => void 0);
348
+ if (info?.isFile() === true && info.mtimeMs < now - ORPHAN_TEMP_AGE_MS) {
349
+ await unlink(path).catch(() => {
350
+ });
351
+ }
352
+ }
353
+ } catch {
354
+ }
355
+ }
356
+
357
+ // src/drivers/localfs/exact-case.ts
358
+ function firstCaseDifference(expected, actual) {
359
+ const shared = Math.min(expected.length, actual.length);
360
+ for (let i = 0; i < shared; i++) {
361
+ const want = expected[i];
362
+ const have = actual[i];
363
+ if (want !== have) return want.toLowerCase() === have.toLowerCase() ? i : -1;
364
+ }
365
+ return -1;
366
+ }
367
+ var swapCase = (s) => [...s].map((c) => c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase()).join("");
368
+ var components = (path) => path.split(sep).filter((c) => c !== "");
369
+ function caseVariantError() {
370
+ return new ValidationError(
371
+ "names differ only by case on a case-insensitive filesystem: a name that differs only by case from one already stored is the same file here, so it is refused rather than written over"
372
+ );
373
+ }
374
+ var ExactCase = class {
375
+ constructor(root) {
376
+ this.root = root;
377
+ }
378
+ root;
379
+ /** `undefined` until the root holds an entry to try; then fixed, as a filesystem does not change its mind. */
380
+ insensitive;
381
+ /**
382
+ * Whether the root's filesystem folds case, learned from an entry already in the root: the entry's name with its
383
+ * case swapped is the same file or it is not. An empty or absent root holds nothing to alias, so it is not
384
+ * insensitive yet, and is looked at again on the next call.
385
+ */
386
+ async foldsCase() {
387
+ if (this.insensitive !== void 0) return this.insensitive;
388
+ let names;
389
+ try {
390
+ names = await readdir(this.root);
391
+ } catch (err) {
392
+ if (isAbsent(err)) return this.foldsAtAncestor();
393
+ throw err;
394
+ }
395
+ for (const name of names) {
396
+ const swapped = swapCase(name);
397
+ if (swapped === name) continue;
398
+ const own = await stat(join3(this.root, name), { bigint: true }).catch(() => void 0);
399
+ if (own === void 0) continue;
400
+ const other = await stat(join3(this.root, swapped), { bigint: true }).catch(() => void 0);
401
+ this.insensitive = other !== void 0 && other.ino === own.ino && other.dev === own.dev;
402
+ return this.insensitive;
403
+ }
404
+ return this.foldsAtAncestor();
405
+ }
406
+ /**
407
+ * The same question asked of the root's own name, or the nearest existing directory above it, when the root holds
408
+ * nothing to try: a root with no entries has none to alias yet, but two names created at once still would. Not
409
+ * remembered, since it is the volume above the root that answers.
410
+ */
411
+ async foldsAtAncestor() {
412
+ let dir = resolve(this.root);
413
+ for (; ; ) {
414
+ const top = parse(dir).root;
415
+ const parts = components(relative(top, dir));
416
+ let at = parts.length - 1;
417
+ while (at >= 0 && swapCase(parts[at]) === parts[at]) at--;
418
+ if (at === -1) return false;
419
+ const swapped = [...parts];
420
+ swapped[at] = swapCase(parts[at]);
421
+ let own;
422
+ try {
423
+ own = await stat(dir, { bigint: true });
424
+ } catch (err) {
425
+ if (!isAbsent(err)) return false;
426
+ const parent = dirname2(dir);
427
+ if (parent === dir) return false;
428
+ dir = parent;
429
+ continue;
430
+ }
431
+ const other = await stat(join3(top, ...swapped), { bigint: true }).catch(() => void 0);
432
+ return other !== void 0 && other.ino === own.ino && other.dev === own.dev;
433
+ }
434
+ }
435
+ /** Whether the root's filesystem folds case: names that differ only by case are one file there. */
436
+ folds() {
437
+ return this.foldsCase();
438
+ }
439
+ /**
440
+ * `true` when `path` (inside the root) is, on disk, a path whose name differs only by case from the one given:
441
+ * the deepest part of it that exists is resolved to its real name and compared exactly. A path that does not exist
442
+ * yet, or that nothing differs from, is `false`.
443
+ */
444
+ async differs(path) {
445
+ if (!await this.foldsCase()) return false;
446
+ const rootAbs = resolve(this.root);
447
+ let probe = resolve(path);
448
+ let real;
449
+ for (; ; ) {
450
+ if (probe === rootAbs || !relative(rootAbs, probe) || relative(rootAbs, probe).startsWith("..")) {
451
+ return false;
452
+ }
453
+ try {
454
+ real = await realpath(probe);
455
+ break;
456
+ } catch (err) {
457
+ if (!isAbsent(err)) throw err;
458
+ probe = dirname2(probe);
459
+ }
460
+ }
461
+ const realRoot = await realpath(rootAbs).catch(() => void 0);
462
+ if (realRoot === void 0) return false;
463
+ const actual = relative(realRoot, real);
464
+ if (actual.startsWith("..")) return false;
465
+ return firstCaseDifference(components(relative(rootAbs, probe)), components(actual)) !== -1;
466
+ }
467
+ /** Refuse a write to `path` when its real name differs from it only by case. */
468
+ async refuseVariant(path) {
469
+ if (await this.differs(path)) throw caseVariantError();
470
+ }
471
+ };
472
+
473
+ // src/drivers/localfs/storage.ts
474
+ async function readWhole(handle, key, length, offset) {
475
+ const buf = new Uint8Array(length);
476
+ let filled = 0;
477
+ while (filled < length) {
478
+ const { bytesRead } = await handle.read(buf, filled, length - filled, offset + filled);
479
+ if (bytesRead === 0) {
480
+ throw new IntegrityError(
481
+ `short read of ${key.segment}.${key.generation}: wanted ${length}B at ${offset}, got ${filled}B`
482
+ );
483
+ }
484
+ filled += bytesRead;
485
+ }
486
+ return buf;
487
+ }
488
+ var LocalFsStorageDriver = class {
489
+ /**
490
+ * `options.privateRoot` is the directory this one lives under, from which down directories are created private; it
491
+ * defaults to `root`.
492
+ */
493
+ constructor(root, options = {}) {
494
+ this.root = root;
495
+ this.options = options;
496
+ this.exactCase = new ExactCase(root);
497
+ }
498
+ root;
499
+ options;
500
+ exactCase;
501
+ capabilities() {
502
+ return {
503
+ rangeRead: true,
504
+ maxObjectBytes: Number.MAX_SAFE_INTEGER,
505
+ conditionalPut: false,
506
+ conditionalDelete: false
507
+ };
508
+ }
509
+ async putImmutable(key, write) {
510
+ const finalPath = storageObjectPath(this.root, key);
511
+ assertStorageNamesFit(key);
512
+ await this.exactCase.refuseVariant(finalPath);
513
+ await makeDirs(dirname3(finalPath), this.options.privateRoot ?? this.root);
514
+ void sweepOrphanTemps(dirname3(finalPath));
515
+ const tmpPath = tempPathFor(finalPath);
516
+ const handle = await open2(
517
+ tmpPath,
518
+ FS2.O_CREAT | FS2.O_EXCL | FS2.O_WRONLY | O_NOFOLLOW,
519
+ FILE_MODE
520
+ );
521
+ const hash = createHash2("sha256");
522
+ let size = 0;
523
+ const sink = {
524
+ async write(bytes) {
525
+ await writeAll(handle, bytes);
526
+ hash.update(bytes);
527
+ size += bytes.length;
528
+ }
529
+ };
530
+ try {
531
+ await write(sink);
532
+ await handle.sync();
533
+ } catch (err) {
534
+ await handle.close().catch(() => {
535
+ });
536
+ await unlink2(tmpPath).catch(() => {
537
+ });
538
+ throw mapFsError(err);
539
+ }
540
+ await handle.close();
541
+ try {
542
+ await link(tmpPath, finalPath);
543
+ } catch (err) {
544
+ await unlink2(tmpPath).catch(() => {
545
+ });
546
+ if (isCode(err, "ENOENT")) {
547
+ throw new TransientError(
548
+ `the temporary file of ${key.segment}.${key.generation} was removed mid-write`,
549
+ {
550
+ cause: err
551
+ }
552
+ );
553
+ }
554
+ if (isCode(err, "EEXIST")) {
555
+ throw new WriteConflictError(
556
+ `generation already exists (write-once): ${key.segment}.${key.generation}`
557
+ );
558
+ }
559
+ throw mapFsError(err);
560
+ }
561
+ await unlink2(tmpPath).catch(() => {
562
+ });
563
+ await fsyncDir(dirname3(finalPath));
564
+ return { size, sha256: hash.digest("hex") };
565
+ }
566
+ async getRange(key, offset, length) {
567
+ requireInteger(offset, { min: 0, what: "range offset" });
568
+ requireInteger(length, { min: 0, what: "range length" });
569
+ const handle = await this.openRead(key);
570
+ try {
571
+ const { size } = await handle.stat();
572
+ if (offset + length > size) {
573
+ throw new ValidationError(
574
+ `range [${offset}, ${offset + length}) out of bounds for ${size}B`
575
+ );
576
+ }
577
+ return await readWhole(handle, key, length, offset);
578
+ } finally {
579
+ await handle.close();
580
+ }
581
+ }
582
+ async getTail(key, maxBytes) {
583
+ checkTailLength(maxBytes);
584
+ const handle = await this.openRead(key);
585
+ try {
586
+ const { size } = await handle.stat();
587
+ const take = Math.min(Math.max(maxBytes, 0), size);
588
+ return { bytes: await readWhole(handle, key, take, size - take), size };
589
+ } finally {
590
+ await handle.close();
591
+ }
592
+ }
593
+ /**
594
+ * Idempotent. Ignores `options.ifVersion`, as `conditionalDelete: false` says: see the note on the class for why it
595
+ * cannot apply one. The parameter is declared so that a wrapper of this driver can hand the port's options on.
596
+ */
597
+ async delete(key, options) {
598
+ checkDeleteOptions(options);
599
+ const path = storageObjectPath(this.root, key);
600
+ if (await this.exactCase.differs(path)) return;
601
+ await unlink2(path).catch((err) => {
602
+ if (!isAbsent(err)) throw mapFsError(err);
603
+ });
604
+ }
605
+ async *list(ref) {
606
+ const dir = segmentsDir(this.root, ref);
607
+ if (await this.exactCase.differs(dir)) return;
608
+ let names;
609
+ try {
610
+ names = await readdir2(dir);
611
+ } catch (err) {
612
+ if (isAbsent(err)) return;
613
+ throw mapFsError(err);
614
+ }
615
+ for (const name of names) {
616
+ const generation = parseGeneration(ref.segment, name);
617
+ if (generation !== null) {
618
+ yield { namespace: ref.namespace, segment: ref.segment, generation };
619
+ }
620
+ }
621
+ }
622
+ async openRead(key) {
623
+ const path = storageObjectPath(this.root, key);
624
+ if (await this.exactCase.differs(path)) {
625
+ throw new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
626
+ }
627
+ try {
628
+ return await open2(path, FS2.O_RDONLY | O_NOFOLLOW);
629
+ } catch (err) {
630
+ if (isAbsent(err)) {
631
+ throw new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
632
+ }
633
+ if (isCode(err, "ELOOP")) {
634
+ throw new NotFoundError(
635
+ `generation path is a symlink, refusing: ${key.segment}.${key.generation}`
636
+ );
637
+ }
638
+ throw mapFsError(err);
639
+ }
640
+ }
641
+ };
642
+
643
+ // src/drivers/localfs/registry.ts
644
+ import { constants as FS3 } from "node:fs";
645
+ import { open as open3, opendir as opendir2, readdir as readdir3, realpath as realpath2, rename, unlink as unlink3 } from "node:fs/promises";
646
+ import { basename, dirname as dirname4, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
647
+ var DEFAULT_MAX_ROW_BYTES = 1 * 1024 * 1024;
648
+ var rowChains = /* @__PURE__ */ new Map();
649
+ async function rowLockKey(path) {
650
+ const abs = resolve2(path);
651
+ const tail = [basename(abs)];
652
+ let dir = dirname4(abs);
653
+ for (; ; ) {
654
+ try {
655
+ return join4(await realpath2(dir), ...tail);
656
+ } catch (err) {
657
+ const parent = dirname4(dir);
658
+ if (!isAbsent(err) || parent === dir) throw mapFsError(err);
659
+ tail.unshift(basename(dir));
660
+ dir = parent;
661
+ }
662
+ }
663
+ }
664
+ var LocalFsRegistryDriver = class {
665
+ constructor(root, options = {}) {
666
+ this.root = root;
667
+ this.options = options;
668
+ this.exactCase = new ExactCase(root);
669
+ this.now = options.now ?? (() => Date.now());
670
+ this.entropy = options.entropy ?? webCryptoEntropy;
671
+ }
672
+ root;
673
+ options;
674
+ now;
675
+ entropy;
676
+ exactCase;
677
+ /**
678
+ * `conditionalDelete`: a delete reads the row, checks it, and unlinks it under the row's lock, which every writer on
679
+ * the root in this process takes, so no write can land between the check and the unlink.
680
+ */
681
+ capabilities() {
682
+ return entropyIsAvailable(this.entropy) ? { strongRead: true, conditionalDelete: true } : { strongRead: true, canWrite: false, conditionalDelete: true };
683
+ }
684
+ /** Every write reports its token to `onToken` before the row's file is written. */
685
+ get reportsWriteTokens() {
686
+ return true;
687
+ }
688
+ async get(ref) {
689
+ const env = await this.readRow(registryRowPath(this.root, ref), ref);
690
+ return env && !env.deleted ? env.record : null;
691
+ }
692
+ async create(ref, record, options) {
693
+ checkRegistryWriteOptions(options);
694
+ const checked = validateNewRegistryRecord(record);
695
+ const path = registryRowPath(this.root, ref);
696
+ assertRegistryNamesFit(ref);
697
+ return this.withRowLock(path, async () => {
698
+ const current = await this.readRow(path, ref);
699
+ if (current !== null && !current.deleted) {
700
+ throw new WriteConflictError(`registry row already exists for segment ${ref.segment}`);
701
+ }
702
+ const token = newIncarnationToken(this.entropy, current?.record);
703
+ options?.onToken?.(token);
704
+ await this.writeRow(path, recordFromNew(ref, checked, this.now(), token));
705
+ return { token };
706
+ });
707
+ }
708
+ /**
709
+ * A caller's `held` row (see `RegistryWriteOptions`) has no read to spare here: the row's file is read under its lock and its token compared
710
+ * there, a local read and no request, so a stale one fails exactly as a lost race does.
711
+ */
712
+ async compareAndSwap(ref, expected, patch, options) {
713
+ checkRegistryWriteOptions(options);
714
+ const checked = validateRegistryPatch(patch);
715
+ const path = registryRowPath(this.root, ref);
716
+ assertRegistryNamesFit(ref);
717
+ return this.withRowLock(path, async () => {
718
+ const current = await this.readRow(path, ref);
719
+ if (current === null || current.deleted || current.record.token !== expected) {
720
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
721
+ }
722
+ const token = nextRegistryToken(current.record, this.entropy);
723
+ options?.onToken?.(token);
724
+ await this.writeRow(path, applyRegistryPatch(current.record, checked, this.now(), token));
725
+ return { token };
726
+ });
727
+ }
728
+ async *list(namespace) {
729
+ for (const ns of await this.namespaceDirs(namespace)) {
730
+ const dir = registryDir(this.root, ns);
731
+ if (await this.exactCase.differs(dir)) continue;
732
+ let entries;
733
+ try {
734
+ entries = await opendir2(dir);
735
+ } catch (err) {
736
+ if (isAbsent(err)) continue;
737
+ throw mapFsError(err);
738
+ }
739
+ for await (const entry of entries) {
740
+ const segment = parseRegistryRow(entry.name);
741
+ if (segment === null) continue;
742
+ const ref = { namespace: ns, segment };
743
+ const env = await this.readRow(registryRowPath(this.root, ref), ref);
744
+ if (env && !env.deleted) yield env.record;
745
+ }
746
+ }
747
+ }
748
+ async delete(ref, expected) {
749
+ const path = registryRowPath(this.root, ref);
750
+ return this.withRowLock(path, async () => {
751
+ const current = await this.readRow(path, ref);
752
+ if (expected !== void 0) {
753
+ if (current === null || current.deleted || current.record.token !== expected) {
754
+ throw new WriteConflictError(`OCC token mismatch for registry row ${ref.segment}`);
755
+ }
756
+ } else if (current === null || current.deleted) {
757
+ return;
758
+ }
759
+ await this.unlinkRow(path);
760
+ });
761
+ }
762
+ /** Remove a row's file and make the removal durable (fsync the directory). */
763
+ async unlinkRow(path) {
764
+ try {
765
+ await unlink3(path);
766
+ } catch (err) {
767
+ if (!isCode(err, "ENOENT")) throw mapFsError(err);
768
+ }
769
+ await fsyncDir(dirname4(path));
770
+ }
771
+ /** Namespaces to scan: just the one requested, or every namespace dir under the root. */
772
+ async namespaceDirs(namespace) {
773
+ if (namespace !== void 0) return [namespace];
774
+ let entries;
775
+ try {
776
+ entries = await readdir3(this.root);
777
+ } catch (err) {
778
+ if (isCode(err, "ENOENT")) return [];
779
+ throw mapFsError(err);
780
+ }
781
+ const namespaces = [];
782
+ for (const entry of entries) {
783
+ const parsed = parseNamespaceDir(entry);
784
+ if (parsed !== null) namespaces.push(parsed.namespace);
785
+ }
786
+ return namespaces;
787
+ }
788
+ /** Read the row at `path`, which must be the row of `ref`: one that names another segment is refused. */
789
+ async readRow(path, ref) {
790
+ let handle;
791
+ try {
792
+ if (await this.exactCase.differs(path)) return null;
793
+ handle = await open3(path, FS3.O_RDONLY | O_NOFOLLOW);
794
+ } catch (err) {
795
+ if (isAbsent(err) || isCode(err, "ELOOP")) return null;
796
+ throw mapFsError(err);
797
+ }
798
+ try {
799
+ const { size } = await handle.stat();
800
+ if (size > DEFAULT_MAX_ROW_BYTES) {
801
+ throw new IntegrityError(`registry row ${size}B exceeds cap ${DEFAULT_MAX_ROW_BYTES}B`);
802
+ }
803
+ const text = (await handle.readFile()).toString("utf8");
804
+ return parseRegistryEnvelope(text, relative2(this.root, path), ref);
805
+ } finally {
806
+ await handle.close();
807
+ }
808
+ }
809
+ async writeRow(path, record) {
810
+ await this.exactCase.refuseVariant(path);
811
+ await makeDirs(dirname4(path), this.options.privateRoot ?? this.root);
812
+ void sweepOrphanTemps(dirname4(path));
813
+ const out = Buffer.from(serializeRegistryEnvelope({ deleted: false, record }), "utf8");
814
+ if (out.length > DEFAULT_MAX_ROW_BYTES) {
815
+ throw new ValidationError(
816
+ `registry row ${out.length}B exceeds cap ${DEFAULT_MAX_ROW_BYTES}B`
817
+ );
818
+ }
819
+ const tmp = tempPathFor(path);
820
+ let handle;
821
+ try {
822
+ handle = await open3(tmp, FS3.O_CREAT | FS3.O_EXCL | FS3.O_WRONLY | O_NOFOLLOW, FILE_MODE);
823
+ } catch (err) {
824
+ throw mapFsError(err);
825
+ }
826
+ try {
827
+ await writeAll(handle, out);
828
+ await handle.sync();
829
+ } catch (err) {
830
+ await handle.close().catch(() => {
831
+ });
832
+ await unlink3(tmp).catch(() => {
833
+ });
834
+ throw mapFsError(err);
835
+ }
836
+ await handle.close();
837
+ await rename(tmp, path).catch(async (err) => {
838
+ await unlink3(tmp).catch(() => {
839
+ });
840
+ if (isCode(err, "ENOENT")) {
841
+ throw new TransientError("the temporary file of a registry row was removed mid-write", {
842
+ cause: err
843
+ });
844
+ }
845
+ throw mapFsError(err);
846
+ });
847
+ await fsyncDir(dirname4(path));
848
+ }
849
+ /** Serialize callbacks for a row so read-modify-write is atomic across every instance in the process. */
850
+ async withRowLock(path, fn) {
851
+ const resolved = await rowLockKey(path);
852
+ const key = await this.exactCase.folds() ? resolved.toLowerCase() : resolved;
853
+ const prev = rowChains.get(key) ?? Promise.resolve();
854
+ const result = prev.then(fn, fn);
855
+ const tail = result.then(
856
+ () => void 0,
857
+ () => void 0
858
+ );
859
+ rowChains.set(key, tail);
860
+ void tail.then(() => {
861
+ if (rowChains.get(key) === tail) rowChains.delete(key);
862
+ });
863
+ return result;
864
+ }
865
+ };
866
+
867
+ // src/drivers/backends.ts
868
+ var CLOCK_OPTION_KEYS = ["now"];
869
+ var MemoryStorage = class {
870
+ storage;
871
+ registry;
872
+ /**
873
+ * Builds an empty in-process backend. `options.now` is the clock for the registry's timestamps, `Date.now` by
874
+ * default; any other option throws `ValidationError`.
875
+ */
876
+ constructor(options = {}) {
877
+ refuseUnknownOptions("MemoryStorage", options, CLOCK_OPTION_KEYS);
878
+ checkNow("MemoryStorage", options.now);
879
+ this.storage = new MemoryStorageDriver();
880
+ this.registry = new MemoryRegistryDriver(options.now === void 0 ? {} : { now: options.now });
881
+ brandAsBackend(this);
882
+ }
883
+ /** Nothing to check: process memory has no lifecycle rules. */
884
+ checkLifecycle() {
885
+ return Promise.resolve({
886
+ status: "ok",
887
+ detail: "MemoryStorage keeps its objects in process memory, which has no lifecycle rules"
888
+ });
889
+ }
890
+ };
891
+ var LocalFsStorage = class {
892
+ /**
893
+ * Builds a backend rooted at `root`, a non-empty directory path. `options.now` is the clock for the registry's
894
+ * timestamps, `Date.now` by default; any other option throws `ValidationError`.
895
+ */
896
+ constructor(root, options = {}) {
897
+ this.root = root;
898
+ if (typeof root !== "string" || root.trim().length === 0) {
899
+ throw new ValidationError(
900
+ `LocalFsStorage needs a root directory: a non-empty string, got ${root === "" ? "an empty string" : typeof root}`
901
+ );
902
+ }
903
+ refuseUnknownOptions("LocalFsStorage", options, CLOCK_OPTION_KEYS);
904
+ checkNow("LocalFsStorage", options.now);
905
+ this.storage = new LocalFsStorageDriver(join5(root, "storage"), { privateRoot: root });
906
+ this.registry = new LocalFsRegistryDriver(
907
+ join5(root, "registry"),
908
+ options.now === void 0 ? { privateRoot: root } : { now: options.now, privateRoot: root }
909
+ );
910
+ brandAsBackend(this);
911
+ }
912
+ root;
913
+ storage;
914
+ registry;
915
+ /** Nothing to check: a local directory has no lifecycle rules. */
916
+ checkLifecycle() {
917
+ return Promise.resolve({
918
+ status: "ok",
919
+ detail: "LocalFsStorage keeps its objects in a local directory, which has no lifecycle rules"
920
+ });
921
+ }
922
+ };
923
+
924
+ // src/drivers/crypto.ts
925
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
926
+ var UTF8 = new TextEncoder();
927
+ var KEY_BYTES = 32;
928
+ var NONCE_BYTES = 12;
929
+ var TAG_BYTES = 16;
930
+ var isBytes = (v) => Object.prototype.toString.call(v) === "[object Uint8Array]";
931
+ function ownKey(key) {
932
+ const own = Buffer.alloc(key.length);
933
+ own.set(key);
934
+ return own;
935
+ }
936
+ var NodeAead = class {
937
+ key;
938
+ constructor(key) {
939
+ if (!isBytes(key))
940
+ throw new ValidationError("AEAD key must be a Uint8Array of key bytes, not text");
941
+ if (key.length !== KEY_BYTES)
942
+ throw new ValidationError(`AEAD key must be ${KEY_BYTES} bytes, got ${key.length}`);
943
+ this.key = ownKey(key);
944
+ }
945
+ seal(plaintext, aad) {
946
+ const nonce = randomBytes(NONCE_BYTES);
947
+ const cipher = createCipheriv("aes-256-gcm", this.key, nonce);
948
+ cipher.setAAD(aad);
949
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
950
+ return { nonce, ciphertext, tag: cipher.getAuthTag() };
951
+ }
952
+ open(sealed, aad) {
953
+ if (sealed.nonce.length !== NONCE_BYTES || sealed.tag.length !== TAG_BYTES)
954
+ throw new IntegrityError("AEAD nonce/tag have the wrong length");
955
+ const decipher = createDecipheriv("aes-256-gcm", this.key, sealed.nonce);
956
+ decipher.setAAD(aad);
957
+ decipher.setAuthTag(Buffer.from(sealed.tag));
958
+ try {
959
+ return Buffer.concat([decipher.update(sealed.ciphertext), decipher.final()]);
960
+ } catch {
961
+ throw new IntegrityError(
962
+ "AEAD authentication failed (wrong key, tampered data, or wrong context)"
963
+ );
964
+ }
965
+ }
966
+ };
967
+ function wrapKey(dek, kek, keyId) {
968
+ const sealed = new NodeAead(kek).seal(dek, UTF8.encode(keyId));
969
+ return Buffer.concat([sealed.nonce, sealed.ciphertext, sealed.tag]).toString("base64");
970
+ }
971
+ function unwrapKey(wrapped, kek, keyId) {
972
+ const blob = Buffer.from(wrapped, "base64");
973
+ if (blob.length < NONCE_BYTES + TAG_BYTES)
974
+ throw new IntegrityError("wrapped DEK is too short to be valid");
975
+ const nonce = blob.subarray(0, NONCE_BYTES);
976
+ const tag = blob.subarray(blob.length - TAG_BYTES);
977
+ const ciphertext = blob.subarray(NONCE_BYTES, blob.length - TAG_BYTES);
978
+ return new NodeAead(kek).open({ nonce, ciphertext, tag }, UTF8.encode(keyId));
979
+ }
980
+ var InProcessKeystore = class {
981
+ keys;
982
+ activeKeyId;
983
+ recoveryKeyId;
984
+ /**
985
+ * Builds a keystore from `options`; throws `ValidationError` for no options object, an option it does not take
986
+ * (a misspelt `recoveryKeyId` would otherwise leave no recovery wrapping), a `keys` that is not an object, an
987
+ * `activeKeyId` or `recoveryKeyId` that is not a string, no keys, a key given as text rather than a
988
+ * `Uint8Array`, a key that is not 32 bytes, or an `activeKeyId` or `recoveryKeyId` that is not in `keys`. A
989
+ * `recoveryKeyId` equal to `activeKeyId` means no recovery key.
990
+ */
991
+ constructor(options) {
992
+ refuseUnknownOptions("InProcessKeystore", options, ["keys", "activeKeyId", "recoveryKeyId"]);
993
+ const keys = options.keys;
994
+ const proto = keys === null || typeof keys !== "object" ? null : Object.getPrototypeOf(keys);
995
+ if (keys === null || typeof keys !== "object" || proto !== Object.prototype && proto !== null) {
996
+ throw new ValidationError(
997
+ "InProcessKeystore: `keys` must be a plain object of key id to 32-byte Uint8Array KEK, not a Map or an array"
998
+ );
999
+ }
1000
+ if (typeof options.activeKeyId !== "string") {
1001
+ throw new ValidationError(
1002
+ "InProcessKeystore: `activeKeyId` must be a string naming one of `keys`"
1003
+ );
1004
+ }
1005
+ if (options.recoveryKeyId !== void 0 && typeof options.recoveryKeyId !== "string") {
1006
+ throw new ValidationError(
1007
+ "InProcessKeystore: `recoveryKeyId` must be a string naming one of `keys`, or absent for none"
1008
+ );
1009
+ }
1010
+ const entries = Object.entries(keys);
1011
+ if (entries.length === 0) throw new ValidationError("keystore needs at least one KEK");
1012
+ for (const [id, k] of entries) {
1013
+ if (!isBytes(k))
1014
+ throw new ValidationError(`KEK "${id}" must be a Uint8Array of key bytes, not text`);
1015
+ if (k.length !== KEY_BYTES)
1016
+ throw new ValidationError(`KEK "${id}" must be ${KEY_BYTES} bytes, got ${k.length}`);
1017
+ }
1018
+ this.keys = new Map(entries.map(([id, k]) => [id, ownKey(k)]));
1019
+ if (!this.keys.has(options.activeKeyId))
1020
+ throw new ValidationError(`activeKeyId "${String(options.activeKeyId)}" is not in keys`);
1021
+ if (options.recoveryKeyId !== void 0 && !this.keys.has(options.recoveryKeyId))
1022
+ throw new ValidationError(`recoveryKeyId "${String(options.recoveryKeyId)}" is not in keys`);
1023
+ this.activeKeyId = options.activeKeyId;
1024
+ this.recoveryKeyId = options.recoveryKeyId === options.activeKeyId ? void 0 : options.recoveryKeyId;
1025
+ }
1026
+ // async to satisfy the IKeystore seam (a KMS adapter awaits a network call here); the in-process path is
1027
+ // synchronous, but staying async keeps unwrap failures as rejections, matching a remote keystore.
1028
+ async createDek() {
1029
+ const dek = randomBytes(KEY_BYTES);
1030
+ const ids = this.recoveryKeyId ? [this.activeKeyId, this.recoveryKeyId] : [this.activeKeyId];
1031
+ const wrapped = ids.map((keyId) => ({
1032
+ keyId,
1033
+ wrapped: wrapKey(dek, this.keys.get(keyId), keyId)
1034
+ }));
1035
+ return { wrapped, aead: new NodeAead(dek) };
1036
+ }
1037
+ // async for the same reason as createDek — a synchronous unwrap failure surfaces as a promise rejection.
1038
+ async openDek(wrapped) {
1039
+ let anyKekHeld = false;
1040
+ let lastUnwrapError;
1041
+ for (const w of wrapped) {
1042
+ const kek = this.keys.get(w.keyId);
1043
+ if (kek === void 0) continue;
1044
+ anyKekHeld = true;
1045
+ try {
1046
+ return new NodeAead(unwrapKey(w.wrapped, kek, w.keyId));
1047
+ } catch (err) {
1048
+ lastUnwrapError = err;
1049
+ }
1050
+ }
1051
+ if (anyKekHeld) throw lastUnwrapError;
1052
+ const need = wrapped.map((w) => w.keyId).join(", ") || "(none)";
1053
+ throw new KeyUnavailableError(
1054
+ `no held KEK can unwrap this DEK \u2014 the wrappings reference [${need}]. Restore the missing KEK (or its recovery KEK), or rebuild the segment from source.`
1055
+ );
1056
+ }
1057
+ };
1058
+ export {
1059
+ InProcessKeystore,
1060
+ LocalFsStorage,
1061
+ MemoryStorage,
1062
+ NodeAead
1063
+ };
1064
+ //# sourceMappingURL=node.js.map