@cloudbitmaps/core 0.11.2 → 0.13.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/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
- package/dist/chunk-5W3XS2IU.js.map +7 -0
- package/dist/core/audit.d.ts +6 -0
- package/dist/core/chunk-window.d.ts +24 -0
- package/dist/core/codec.d.ts +33 -1
- package/dist/core/consistency.d.ts +21 -2
- package/dist/core/cost.d.ts +91 -10
- package/dist/core/crbm/format.d.ts +42 -3
- package/dist/core/crbm/reader.d.ts +45 -3
- package/dist/core/crbm/writer.d.ts +32 -3
- package/dist/core/crbm-storage-source.d.ts +204 -23
- package/dist/core/crypto.d.ts +11 -7
- package/dist/core/determinism.d.ts +7 -0
- package/dist/core/engine.d.ts +41 -8
- package/dist/core/erase-id.d.ts +23 -3
- package/dist/core/erasure.d.ts +8 -4
- package/dist/core/generation-gc.d.ts +102 -4
- package/dist/core/load-input.d.ts +55 -0
- package/dist/core/load.d.ts +65 -10
- package/dist/core/metadata.d.ts +42 -0
- package/dist/core/metrics.d.ts +7 -1
- package/dist/core/pinned-storage-source.d.ts +2 -1
- package/dist/core/ports.d.ts +119 -13
- package/dist/core/registry-scan.d.ts +6 -0
- package/dist/core/retention-sweep.d.ts +47 -2
- package/dist/core/retry.d.ts +14 -0
- package/dist/core/rollback.d.ts +34 -4
- package/dist/core/summary.d.ts +61 -0
- package/dist/core/token.d.ts +36 -0
- package/dist/core/validate.d.ts +2 -0
- package/dist/driver-kit.js +39 -20
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/entropy.d.ts +5 -0
- package/dist/drivers/_shared/object-registry.d.ts +36 -6
- package/dist/drivers/_shared/registry.d.ts +47 -19
- package/dist/drivers/localfs/registry.d.ts +10 -0
- package/dist/drivers/memory.d.ts +9 -3
- package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1878 -501
- package/dist/index.js.map +4 -4
- package/dist/testing/fuzz-core.d.ts +7 -6
- package/package.json +1 -1
- package/dist/chunk-5OMC5U7U.js.map +0 -7
|
@@ -116,6 +116,9 @@ function decodeNameFromPath(encoded) {
|
|
|
116
116
|
var CHUNK_KEY_MAX = 65535;
|
|
117
117
|
var MAX_NAME_LENGTH = 256;
|
|
118
118
|
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
119
|
+
function isWellFormedString(value) {
|
|
120
|
+
return !LONE_SURROGATE.test(value);
|
|
121
|
+
}
|
|
119
122
|
function encodedNameLength(value) {
|
|
120
123
|
return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
|
|
121
124
|
}
|
|
@@ -203,6 +206,13 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
// src/core/ports.ts
|
|
209
|
+
function assertRegistryCanWrite(registry, what) {
|
|
210
|
+
if (registry.capabilities().canWrite === false) {
|
|
211
|
+
throw new UnsupportedError(
|
|
212
|
+
`${what}: the registry reports it cannot write a row in this runtime (capabilities().canWrite is false), so nothing is written`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
206
216
|
var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
|
|
207
217
|
var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
|
|
208
218
|
function brandAsBackend(target) {
|
|
@@ -247,6 +257,96 @@ function namespacePathPart(namespace) {
|
|
|
247
257
|
return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
// src/core/metadata.ts
|
|
261
|
+
var MAX_METADATA_BYTES = 1024;
|
|
262
|
+
var MAX_METADATA_KEY_BYTES = 128;
|
|
263
|
+
var utf8Length = (s) => new TextEncoder().encode(s).length;
|
|
264
|
+
function canonicalMetadataJson(value, fail) {
|
|
265
|
+
if (value === null || typeof value !== "object") {
|
|
266
|
+
fail(`metadata must be a plain object (got ${value === null ? "null" : typeof value})`);
|
|
267
|
+
}
|
|
268
|
+
const proto = Object.getPrototypeOf(value);
|
|
269
|
+
if (Object.prototype.toString.call(value) !== "[object Object]" || proto !== Object.prototype && proto !== null) {
|
|
270
|
+
fail("metadata must be a plain object, not an array, a Map, a boxed value or a class instance");
|
|
271
|
+
}
|
|
272
|
+
const entries = [];
|
|
273
|
+
const keys = Reflect.ownKeys(value);
|
|
274
|
+
for (const key of keys) {
|
|
275
|
+
if (typeof key !== "string") fail("metadata keys must be strings, not symbols");
|
|
276
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
277
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.enumerable !== true) {
|
|
278
|
+
fail(`metadata key ${JSON.stringify(key)} must be an enumerable data property`);
|
|
279
|
+
}
|
|
280
|
+
if (key.length === 0) fail("metadata keys must be non-empty");
|
|
281
|
+
if (key === "__proto__") fail('metadata may not use the key "__proto__"');
|
|
282
|
+
if (!isWellFormedString(key)) fail("metadata keys must be well-formed UTF-16");
|
|
283
|
+
if (key.length * 3 > MAX_METADATA_KEY_BYTES) {
|
|
284
|
+
const keyBytes = utf8Length(key);
|
|
285
|
+
if (keyBytes > MAX_METADATA_KEY_BYTES) {
|
|
286
|
+
fail(`metadata key is ${keyBytes}B, over the ${MAX_METADATA_KEY_BYTES}B cap`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const v = descriptor.value;
|
|
290
|
+
if (typeof v === "string") {
|
|
291
|
+
if (!isWellFormedString(v)) {
|
|
292
|
+
fail(`metadata value of ${JSON.stringify(key)} must be well-formed UTF-16`);
|
|
293
|
+
}
|
|
294
|
+
} else if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
295
|
+
fail(`metadata value of ${JSON.stringify(key)} must be a string or a finite number`);
|
|
296
|
+
}
|
|
297
|
+
entries.push([key, v]);
|
|
298
|
+
}
|
|
299
|
+
entries.sort(([a], [b]) => a < b ? -1 : 1);
|
|
300
|
+
const json = `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`).join(",")}}`;
|
|
301
|
+
const bytes = utf8Length(json);
|
|
302
|
+
if (bytes > MAX_METADATA_BYTES) {
|
|
303
|
+
fail(`metadata is ${bytes}B as canonical JSON, over the ${MAX_METADATA_BYTES}B cap`);
|
|
304
|
+
}
|
|
305
|
+
return json;
|
|
306
|
+
}
|
|
307
|
+
function metadataBytes(value, fail) {
|
|
308
|
+
if (value === void 0) return void 0;
|
|
309
|
+
const json = canonicalMetadataJson(value, fail);
|
|
310
|
+
return json === "{}" ? void 0 : new TextEncoder().encode(json);
|
|
311
|
+
}
|
|
312
|
+
function copiedMetadata(value, fail) {
|
|
313
|
+
const bytes = metadataBytes(value, fail);
|
|
314
|
+
return bytes === void 0 ? void 0 : metadataFromBytes(bytes, fail);
|
|
315
|
+
}
|
|
316
|
+
function metadataFromBytes(bytes, fail) {
|
|
317
|
+
if (bytes.length > MAX_METADATA_BYTES) {
|
|
318
|
+
fail(`metadata is ${bytes.length}B, over the ${MAX_METADATA_BYTES}B cap`);
|
|
319
|
+
}
|
|
320
|
+
let text;
|
|
321
|
+
try {
|
|
322
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
323
|
+
} catch {
|
|
324
|
+
fail("metadata is not valid UTF-8");
|
|
325
|
+
}
|
|
326
|
+
let parsed;
|
|
327
|
+
try {
|
|
328
|
+
parsed = JSON.parse(text);
|
|
329
|
+
} catch {
|
|
330
|
+
fail("metadata is not JSON");
|
|
331
|
+
}
|
|
332
|
+
const json = canonicalMetadataJson(parsed, fail);
|
|
333
|
+
if (json === "{}") fail("metadata is empty; a generation without metadata carries none");
|
|
334
|
+
if (json !== text) fail("metadata is not in canonical form");
|
|
335
|
+
return Object.freeze(parsed);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/core/token.ts
|
|
339
|
+
var INCARNATION_TOKEN = /^([0-9a-f]{32})\.(0|[1-9]\d*)\.([0-9a-f]{16})$/;
|
|
340
|
+
function incarnationOf(token) {
|
|
341
|
+
return INCARNATION_TOKEN.exec(token)?.[1];
|
|
342
|
+
}
|
|
343
|
+
function sameIncarnation(a, b) {
|
|
344
|
+
const ia = incarnationOf(a.token);
|
|
345
|
+
const ib = incarnationOf(b.token);
|
|
346
|
+
if (ia !== void 0 || ib !== void 0) return ia === ib;
|
|
347
|
+
return a.createdAt === b.createdAt;
|
|
348
|
+
}
|
|
349
|
+
|
|
250
350
|
// src/drivers/_shared/registry.ts
|
|
251
351
|
var STATUSES = ["active", "destroyed"];
|
|
252
352
|
var RECORD_FIELDS = [
|
|
@@ -258,14 +358,20 @@ var RECORD_FIELDS = [
|
|
|
258
358
|
"status",
|
|
259
359
|
"retention",
|
|
260
360
|
"residency",
|
|
361
|
+
"summary",
|
|
261
362
|
"createdAt",
|
|
262
363
|
"updatedAt",
|
|
263
364
|
"token"
|
|
264
365
|
];
|
|
366
|
+
var SCHEMA_1_RECORD_FIELDS = RECORD_FIELDS.filter((f) => f !== "summary");
|
|
265
367
|
var ENVELOPE_FIELDS = ["schemaVersion", "deleted", "record"];
|
|
266
368
|
var MAX_GOVERNANCE_BYTES = 64 * 1024;
|
|
267
369
|
var MAX_WRAPPED_DEKS = 8;
|
|
268
370
|
var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
|
|
371
|
+
var MAX_SUMMARY_CARDINALITY = 2 ** 32;
|
|
372
|
+
var SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 16;
|
|
373
|
+
var SEALED_SUMMARY_MAX_BYTES = SEALED_SUMMARY_MIN_BYTES + MAX_METADATA_BYTES;
|
|
374
|
+
var BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
269
375
|
function validateGeneration(gen) {
|
|
270
376
|
if (gen === null) return;
|
|
271
377
|
if (!Number.isInteger(gen) || gen < 0) {
|
|
@@ -326,12 +432,93 @@ function validateWrappedDeks(value, isStored) {
|
|
|
326
432
|
fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
|
|
327
433
|
}
|
|
328
434
|
}
|
|
435
|
+
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
436
|
+
function base64PadBitsAreZero(text) {
|
|
437
|
+
const pad = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
438
|
+
if (pad === 0) return true;
|
|
439
|
+
const last = BASE64_ALPHABET.indexOf(text[text.length - pad - 1]);
|
|
440
|
+
return pad === 2 ? (last & 15) === 0 : (last & 3) === 0;
|
|
441
|
+
}
|
|
442
|
+
function validateSummary(value, isStored, ctx) {
|
|
443
|
+
if (value === void 0) return void 0;
|
|
444
|
+
const fail = (msg) => {
|
|
445
|
+
const where = ctx === void 0 ? "" : `: ${ctx}`;
|
|
446
|
+
throw isStored ? new IntegrityError(`registry record summary: ${msg}${where}`) : new ValidationError(`summary: ${msg}${where}`);
|
|
447
|
+
};
|
|
448
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
449
|
+
fail(
|
|
450
|
+
`must be an object (got ${value === null ? "null" : Array.isArray(value) ? "an array" : typeof value})`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const s = value;
|
|
454
|
+
const keys = Object.keys(s);
|
|
455
|
+
const isSealed = keys.includes("sealed");
|
|
456
|
+
const allowed = isSealed ? ["generation", "sealed"] : ["generation", "cardinality", "metadata"];
|
|
457
|
+
const extra = keys.filter((k) => !allowed.includes(k));
|
|
458
|
+
if (extra.length > 0) {
|
|
459
|
+
fail(
|
|
460
|
+
`has fields its ${isSealed ? "sealed" : "clear"} shape does not declare (${extra.join(", ")})`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const generation = s.generation;
|
|
464
|
+
if (!Number.isSafeInteger(generation) || generation < 0) {
|
|
465
|
+
fail(`generation must be a non-negative safe integer (got ${String(generation)})`);
|
|
466
|
+
}
|
|
467
|
+
if (isSealed) {
|
|
468
|
+
const sealed = s.sealed;
|
|
469
|
+
if (typeof sealed !== "string" || !BASE64.test(sealed) || !base64PadBitsAreZero(sealed)) {
|
|
470
|
+
fail("sealed must be canonical, padded standard base64");
|
|
471
|
+
}
|
|
472
|
+
const text = sealed;
|
|
473
|
+
const bytes = text.length / 4 * 3 - (text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0);
|
|
474
|
+
if (bytes < SEALED_SUMMARY_MIN_BYTES || bytes > SEALED_SUMMARY_MAX_BYTES) {
|
|
475
|
+
fail(
|
|
476
|
+
`sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B (a nonce, a fixed-width count, a tag, and metadata up to its cap)`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return Object.freeze({ generation, sealed: text });
|
|
480
|
+
}
|
|
481
|
+
const n = s.cardinality;
|
|
482
|
+
if (!Number.isInteger(n) || n < 0 || n > MAX_SUMMARY_CARDINALITY) {
|
|
483
|
+
fail(`cardinality must be an integer from 0 to 2^32 (got ${String(n)})`);
|
|
484
|
+
}
|
|
485
|
+
if (s.metadata === void 0) {
|
|
486
|
+
return Object.freeze({ generation, cardinality: n });
|
|
487
|
+
}
|
|
488
|
+
const canonical = canonicalMetadataJson(s.metadata, fail);
|
|
489
|
+
if (canonical === "{}") fail("metadata is empty; a generation without metadata carries none");
|
|
490
|
+
if (isStored) return s;
|
|
491
|
+
const metadata = Object.freeze(JSON.parse(canonical));
|
|
492
|
+
return Object.freeze({ generation, cardinality: n, metadata });
|
|
493
|
+
}
|
|
494
|
+
function validateSummaryNames(summary, currentGen) {
|
|
495
|
+
if (summary !== void 0 && summary.generation !== currentGen) {
|
|
496
|
+
throw new ValidationError(
|
|
497
|
+
`summary describes generation ${summary.generation}, but the row points at ${String(currentGen)}`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function summaryAgreesWithKeys(summary, wrappedDeks) {
|
|
502
|
+
return "sealed" in summary === (wrappedDeks !== void 0);
|
|
503
|
+
}
|
|
504
|
+
function validateSummaryKeys(summary, wrappedDeks) {
|
|
505
|
+
if (summary !== void 0 && !summaryAgreesWithKeys(summary, wrappedDeks)) {
|
|
506
|
+
throw new ValidationError(
|
|
507
|
+
wrappedDeks === void 0 ? "summary is sealed, but the row has no wrapped keys to open it with" : "summary is in the clear, but the row is encrypted: an encrypted segment's summary must be sealed"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
329
511
|
function validateNewRegistryRecord(rec) {
|
|
330
512
|
validateGeneration(rec.currentGen);
|
|
331
513
|
if (rec.status !== void 0) validateStatus(rec.status);
|
|
332
514
|
validateWrappedDeks(rec.wrappedDeks, false);
|
|
333
515
|
validateGovernance(rec.retention, "retention");
|
|
334
516
|
validateGovernance(rec.residency, "residency");
|
|
517
|
+
if (!("summary" in rec)) return rec;
|
|
518
|
+
const summary = validateSummary(rec.summary, false);
|
|
519
|
+
validateSummaryNames(summary, rec.currentGen);
|
|
520
|
+
validateSummaryKeys(summary, rec.wrappedDeks);
|
|
521
|
+
return { ...rec, summary };
|
|
335
522
|
}
|
|
336
523
|
function validateRegistryPatch(patch) {
|
|
337
524
|
validatePatchGeneration(patch);
|
|
@@ -339,13 +526,16 @@ function validateRegistryPatch(patch) {
|
|
|
339
526
|
if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
|
|
340
527
|
if ("retention" in patch) validateGovernance(patch.retention, "retention");
|
|
341
528
|
if ("residency" in patch) validateGovernance(patch.residency, "residency");
|
|
529
|
+
if (!("summary" in patch)) return patch;
|
|
530
|
+
return { ...patch, summary: validateSummary(patch.summary, false) };
|
|
342
531
|
}
|
|
343
|
-
var REGISTRY_SCHEMA_VERSION =
|
|
532
|
+
var REGISTRY_SCHEMA_VERSION = 2;
|
|
533
|
+
var OLDEST_REGISTRY_SCHEMA_VERSION = 1;
|
|
344
534
|
function assertRegistrySchemaVersion(raw, ctx) {
|
|
345
535
|
if (raw === void 0) {
|
|
346
536
|
throw new IntegrityError(`registry row has no schemaVersion: ${ctx}`);
|
|
347
537
|
}
|
|
348
|
-
if (typeof raw !== "number" || !Number.isInteger(raw) || raw <
|
|
538
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < OLDEST_REGISTRY_SCHEMA_VERSION) {
|
|
349
539
|
throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
|
|
350
540
|
}
|
|
351
541
|
if (raw > REGISTRY_SCHEMA_VERSION) {
|
|
@@ -353,6 +543,7 @@ function assertRegistrySchemaVersion(raw, ctx) {
|
|
|
353
543
|
`registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
|
|
354
544
|
);
|
|
355
545
|
}
|
|
546
|
+
return raw;
|
|
356
547
|
}
|
|
357
548
|
function serializeRegistryEnvelope(env) {
|
|
358
549
|
return JSON.stringify({
|
|
@@ -361,15 +552,51 @@ function serializeRegistryEnvelope(env) {
|
|
|
361
552
|
record: env.record
|
|
362
553
|
});
|
|
363
554
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
555
|
+
var COUNTER_TOKEN = /^(0|[1-9]\d*)$/;
|
|
556
|
+
var WRITTEN_COUNTER_TOKEN = /^(0|[1-9]\d*)\.([0-9a-f]{16})$/;
|
|
557
|
+
var INCARNATION_BYTES = 16;
|
|
558
|
+
var WRITE_BYTES = 8;
|
|
559
|
+
function tokenParts(token, schemaVersion, ctx) {
|
|
560
|
+
const bare = schemaVersion === void 0 || schemaVersion === 1 ? COUNTER_TOKEN.exec(token) : null;
|
|
561
|
+
const written = schemaVersion === void 0 || schemaVersion >= 2 ? WRITTEN_COUNTER_TOKEN.exec(token) ?? INCARNATION_TOKEN.exec(token) : null;
|
|
562
|
+
const match = bare ?? written;
|
|
563
|
+
if (match === null) {
|
|
564
|
+
throw new IntegrityError(
|
|
565
|
+
`registry row token is not one a schema-${schemaVersion ?? REGISTRY_SCHEMA_VERSION} row holds (${JSON.stringify(token)}): ${ctx}`
|
|
566
|
+
);
|
|
367
567
|
}
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
568
|
+
const born = match.length === 4;
|
|
569
|
+
const counter = Number(born ? match[2] : match[1]);
|
|
570
|
+
if (!Number.isSafeInteger(counter)) {
|
|
571
|
+
throw new IntegrityError(`registry row token's counter is out of safe-integer range: ${ctx}`);
|
|
371
572
|
}
|
|
372
|
-
return
|
|
573
|
+
return { incarnation: born ? match[1] : void 0, counter };
|
|
574
|
+
}
|
|
575
|
+
function drawHex(entropy, bytes) {
|
|
576
|
+
const drawn = entropy(bytes);
|
|
577
|
+
if (!(drawn instanceof Uint8Array) || drawn.length !== bytes) {
|
|
578
|
+
throw new ValidationError(
|
|
579
|
+
`the registry's entropy source must return ${bytes} bytes as a Uint8Array`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
let hex = "";
|
|
583
|
+
for (const b of drawn) hex += b.toString(16).padStart(2, "0");
|
|
584
|
+
return hex;
|
|
585
|
+
}
|
|
586
|
+
function drawIncarnation(entropy) {
|
|
587
|
+
return drawHex(entropy, INCARNATION_BYTES);
|
|
588
|
+
}
|
|
589
|
+
function drawWrite(entropy) {
|
|
590
|
+
return drawHex(entropy, WRITE_BYTES);
|
|
591
|
+
}
|
|
592
|
+
function nextRegistryToken(record, entropy) {
|
|
593
|
+
const { incarnation, counter } = tokenParts(record.token, void 0, record.segment);
|
|
594
|
+
const tail = `${counter + 1}.${drawWrite(entropy)}`;
|
|
595
|
+
return incarnation === void 0 ? tail : `${incarnation}.${tail}`;
|
|
596
|
+
}
|
|
597
|
+
function newIncarnationToken(entropy, tombstone) {
|
|
598
|
+
const counter = tombstone === void 0 ? 0 : tokenParts(tombstone.token, void 0, tombstone.segment).counter + 1;
|
|
599
|
+
return `${drawIncarnation(entropy)}.${counter}.${drawWrite(entropy)}`;
|
|
373
600
|
}
|
|
374
601
|
function parseRegistryEnvelope(text, ctx) {
|
|
375
602
|
let parsed;
|
|
@@ -382,7 +609,7 @@ function parseRegistryEnvelope(text, ctx) {
|
|
|
382
609
|
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
383
610
|
}
|
|
384
611
|
const env = parsed;
|
|
385
|
-
assertRegistrySchemaVersion(env.schemaVersion, ctx);
|
|
612
|
+
const schemaVersion = assertRegistrySchemaVersion(env.schemaVersion, ctx);
|
|
386
613
|
const extra = Object.keys(env).filter((k) => !ENVELOPE_FIELDS.includes(k));
|
|
387
614
|
if (extra.length > 0) {
|
|
388
615
|
throw new IntegrityError(
|
|
@@ -393,10 +620,11 @@ function parseRegistryEnvelope(text, ctx) {
|
|
|
393
620
|
throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
|
|
394
621
|
}
|
|
395
622
|
const r = env.record;
|
|
396
|
-
assertStoredRecordShape(r, ctx);
|
|
623
|
+
assertStoredRecordShape(r, ctx, schemaVersion);
|
|
397
624
|
if (typeof r.token !== "string") {
|
|
398
625
|
throw new IntegrityError(`registry row is missing its token: ${ctx}`);
|
|
399
626
|
}
|
|
627
|
+
tokenParts(r.token, schemaVersion, ctx);
|
|
400
628
|
return { deleted: env.deleted, record: env.record };
|
|
401
629
|
}
|
|
402
630
|
function recordFromNew(ref, rec, now, token) {
|
|
@@ -409,12 +637,13 @@ function recordFromNew(ref, rec, now, token) {
|
|
|
409
637
|
status: rec.status ?? "active",
|
|
410
638
|
retention: rec.retention,
|
|
411
639
|
residency: rec.residency,
|
|
640
|
+
summary: rec.summary,
|
|
412
641
|
createdAt: now,
|
|
413
642
|
updatedAt: now,
|
|
414
643
|
token
|
|
415
644
|
};
|
|
416
645
|
}
|
|
417
|
-
function assertStoredRecordShape(r, ctx) {
|
|
646
|
+
function assertStoredRecordShape(r, ctx, schemaVersion = REGISTRY_SCHEMA_VERSION) {
|
|
418
647
|
if (typeof r.segment !== "string" || r.namespace !== void 0 && typeof r.namespace !== "string" || r.currentGen !== null && typeof r.currentGen !== "number" || typeof r.status !== "string" || typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
|
|
419
648
|
throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
|
|
420
649
|
}
|
|
@@ -424,10 +653,11 @@ function assertStoredRecordShape(r, ctx) {
|
|
|
424
653
|
if (!STATUSES.includes(r.status)) {
|
|
425
654
|
throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
|
|
426
655
|
}
|
|
427
|
-
const
|
|
656
|
+
const declared = schemaVersion >= 2 ? RECORD_FIELDS : SCHEMA_1_RECORD_FIELDS;
|
|
657
|
+
const extra = Object.keys(r).filter((k) => !declared.includes(k));
|
|
428
658
|
if (extra.length > 0) {
|
|
429
659
|
throw new IntegrityError(
|
|
430
|
-
`registry record has fields
|
|
660
|
+
`registry record has fields a schema-${schemaVersion} row does not declare (${extra.join(", ")}): ${ctx}`
|
|
431
661
|
);
|
|
432
662
|
}
|
|
433
663
|
if (r.keyId !== void 0 && typeof r.keyId !== "string") {
|
|
@@ -440,27 +670,54 @@ function assertStoredRecordShape(r, ctx) {
|
|
|
440
670
|
throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
|
|
441
671
|
}
|
|
442
672
|
}
|
|
673
|
+
validateSummary(r.summary, true, ctx);
|
|
443
674
|
}
|
|
444
675
|
function applyRegistryPatch(prev, patch, now, token) {
|
|
676
|
+
const currentGen = "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen;
|
|
677
|
+
const wrappedDeks = "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks;
|
|
678
|
+
let summary;
|
|
679
|
+
if ("summary" in patch) {
|
|
680
|
+
validateSummaryNames(patch.summary, currentGen);
|
|
681
|
+
validateSummaryKeys(patch.summary, wrappedDeks);
|
|
682
|
+
summary = patch.summary;
|
|
683
|
+
} else {
|
|
684
|
+
const kept = currentGen === prev.currentGen ? prev.summary : void 0;
|
|
685
|
+
summary = kept !== void 0 && summaryAgreesWithKeys(kept, wrappedDeks) ? kept : void 0;
|
|
686
|
+
}
|
|
445
687
|
return {
|
|
446
688
|
namespace: prev.namespace,
|
|
447
689
|
segment: prev.segment,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and
|
|
451
|
-
// `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.
|
|
452
|
-
currentGen: "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen,
|
|
453
|
-
wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
|
|
690
|
+
currentGen,
|
|
691
|
+
wrappedDeks,
|
|
454
692
|
keyId: "keyId" in patch ? patch.keyId : prev.keyId,
|
|
455
693
|
status: patch.status ?? prev.status,
|
|
456
694
|
retention: "retention" in patch ? patch.retention : prev.retention,
|
|
457
695
|
residency: "residency" in patch ? patch.residency : prev.residency,
|
|
696
|
+
summary,
|
|
458
697
|
createdAt: prev.createdAt,
|
|
459
698
|
updatedAt: now,
|
|
460
699
|
token
|
|
461
700
|
};
|
|
462
701
|
}
|
|
463
702
|
|
|
703
|
+
// src/drivers/_shared/entropy.ts
|
|
704
|
+
function webCrypto() {
|
|
705
|
+
const c = globalThis.crypto;
|
|
706
|
+
return c !== void 0 && typeof c.getRandomValues === "function" ? c : void 0;
|
|
707
|
+
}
|
|
708
|
+
var webCryptoEntropy = (length) => {
|
|
709
|
+
const c = webCrypto();
|
|
710
|
+
if (c === void 0) {
|
|
711
|
+
throw new UnsupportedError(
|
|
712
|
+
"this runtime has no Web Crypto (crypto.getRandomValues), which a registry needs for the token of every row it writes: run on a runtime that has it"
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return c.getRandomValues(new Uint8Array(length));
|
|
716
|
+
};
|
|
717
|
+
function entropyIsAvailable(entropy) {
|
|
718
|
+
return entropy !== webCryptoEntropy || webCrypto() !== void 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
464
721
|
export {
|
|
465
722
|
CloudRoaringError,
|
|
466
723
|
ValidationError,
|
|
@@ -491,18 +748,31 @@ export {
|
|
|
491
748
|
validateUserRef,
|
|
492
749
|
validateChunkRef,
|
|
493
750
|
mapWithConcurrency,
|
|
751
|
+
assertRegistryCanWrite,
|
|
494
752
|
STORAGE_BACKEND,
|
|
495
753
|
brandAsBackend,
|
|
496
754
|
isStorageBackend,
|
|
755
|
+
MAX_METADATA_BYTES,
|
|
756
|
+
canonicalMetadataJson,
|
|
757
|
+
metadataBytes,
|
|
758
|
+
copiedMetadata,
|
|
759
|
+
metadataFromBytes,
|
|
760
|
+
incarnationOf,
|
|
761
|
+
sameIncarnation,
|
|
497
762
|
validateNewRegistryRecord,
|
|
498
763
|
validateRegistryPatch,
|
|
499
764
|
serializeRegistryEnvelope,
|
|
500
|
-
|
|
765
|
+
drawIncarnation,
|
|
766
|
+
drawWrite,
|
|
767
|
+
nextRegistryToken,
|
|
768
|
+
newIncarnationToken,
|
|
501
769
|
parseRegistryEnvelope,
|
|
502
770
|
recordFromNew,
|
|
503
771
|
applyRegistryPatch,
|
|
772
|
+
webCryptoEntropy,
|
|
773
|
+
entropyIsAvailable,
|
|
504
774
|
DEFAULT_NAMESPACE,
|
|
505
775
|
namespaceKeyPart,
|
|
506
776
|
namespacePathPart
|
|
507
777
|
};
|
|
508
|
-
//# sourceMappingURL=chunk-
|
|
778
|
+
//# sourceMappingURL=chunk-5W3XS2IU.js.map
|