@cloudbitmaps/core 0.22.0 → 0.24.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 (55) hide show
  1. package/README.md +7 -12
  2. package/dist/{chunk-OSU7W2Y3.js → chunk-2B2QF4FU.js} +205 -756
  3. package/dist/chunk-2B2QF4FU.js.map +7 -0
  4. package/dist/chunk-45ML6YU4.js +641 -0
  5. package/dist/chunk-45ML6YU4.js.map +7 -0
  6. package/dist/chunk-BUDAVGBI.js +73 -0
  7. package/dist/chunk-BUDAVGBI.js.map +7 -0
  8. package/dist/chunk-WPER7R2R.js +121 -0
  9. package/dist/chunk-WPER7R2R.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 +50 -0
  15. package/dist/core/codec.d.ts +13 -3
  16. package/dist/core/combine-many.d.ts +11 -0
  17. package/dist/core/consistency.d.ts +11 -2
  18. package/dist/core/cooperative.d.ts +15 -5
  19. package/dist/core/crbm/reader.d.ts +1 -1
  20. package/dist/core/crbm-storage-source.d.ts +7 -4
  21. package/dist/core/crypto.d.ts +5 -0
  22. package/dist/core/discover.d.ts +5 -3
  23. package/dist/core/engine.d.ts +5 -5
  24. package/dist/core/erase-id.d.ts +36 -13
  25. package/dist/core/erasure.d.ts +61 -27
  26. package/dist/core/errors.d.ts +30 -0
  27. package/dist/core/leases.d.ts +2 -0
  28. package/dist/core/load-input.d.ts +49 -1
  29. package/dist/core/load.d.ts +23 -2
  30. package/dist/core/metrics.d.ts +21 -0
  31. package/dist/core/pinned-storage-source.d.ts +27 -8
  32. package/dist/core/pointer-id.d.ts +9 -0
  33. package/dist/core/ports.d.ts +100 -7
  34. package/dist/core/preflight.d.ts +80 -0
  35. package/dist/core/retention-sweep.d.ts +60 -15
  36. package/dist/core/retention.d.ts +17 -6
  37. package/dist/core/retry.d.ts +1 -2
  38. package/dist/core/rollback.d.ts +22 -12
  39. package/dist/core/validate.d.ts +33 -2
  40. package/dist/driver-kit.d.ts +3 -3
  41. package/dist/driver-kit.js +61 -20
  42. package/dist/driver-kit.js.map +2 -2
  43. package/dist/drivers/_shared/object-registry.d.ts +24 -1
  44. package/dist/drivers/_shared/options.d.ts +15 -7
  45. package/dist/drivers/backends.d.ts +17 -2
  46. package/dist/drivers/crypto.d.ts +17 -2
  47. package/dist/export/index.d.ts +16 -2
  48. package/dist/index.d.ts +10 -9
  49. package/dist/index.js +1799 -1700
  50. package/dist/index.js.map +4 -4
  51. package/dist/node.d.ts +14 -0
  52. package/dist/node.js +1064 -0
  53. package/dist/node.js.map +7 -0
  54. package/package.json +11 -3
  55. package/dist/chunk-OSU7W2Y3.js.map +0 -7
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/drivers/_shared/options.ts", "../src/drivers/_shared/keys.ts", "../src/drivers/_shared/registry.ts", "../src/drivers/_shared/entropy.ts"],
4
+ "sourcesContent": ["/**\n * Option-bag checks shared by the backends and the drivers, so each states \"refuse what can never be honoured\" once:\n * an options value that is not an object, a key the backend does not take (named, with the keys it does), a `now` that\n * is not a function, a size that is not a positive safe integer and a switch that is not a boolean. Each throws\n * `ValidationError`, so a wiring mistake fails where it is made rather than at the first read.\n */\nimport {\n refuseUnknownOptions,\n requireBool,\n requireNonEmptyString,\n requirePositiveInteger,\n shown,\n} from '@/core/check';\n\nexport { refuseUnknownOptions, requireBool, requireNonEmptyString };\nimport { ValidationError } from '@/core/errors';\n\n/** Refuse a `now` that is present and not a function. */\nexport function checkNow(name: string, now: unknown): void {\n if (now !== undefined && typeof now !== 'function') {\n throw new ValidationError(`${name}: \\`now\\` must be a function returning epoch milliseconds`);\n }\n}\n\n/**\n * The option `name`'s `value`, refused with `ValidationError` unless it is absent or a positive safe integer (a whole\n * number from 1 to 2^53 - 1): `0`, a negative, a fraction, `NaN`, an infinity and a string are refused by name. An\n * absent value comes back `undefined`, for the caller's default.\n */\nexport function requirePositiveInt(name: string, value: unknown): number | undefined {\n return value === undefined ? undefined : requirePositiveInteger(value, name);\n}\n\nconst DELETE_OPTION_KEYS: readonly string[] = ['ifVersion'];\nconst WRITE_OPTION_KEYS: readonly string[] = ['held', 'onToken'];\n\n/**\n * The per-call options of a storage port's `delete`, checked: absent, or an object holding only `ifVersion`, a string.\n * A misspelt key would otherwise read as no condition and delete whatever is under the key. Returns the `ifVersion`.\n */\nexport function checkDeleteOptions(options: unknown): string | undefined {\n if (options === undefined) return undefined;\n refuseUnknownOptions('delete options', options, DELETE_OPTION_KEYS);\n const { ifVersion } = options as { ifVersion?: unknown };\n if (ifVersion !== undefined && typeof ifVersion !== 'string') {\n throw new ValidationError(\n `delete options: \\`ifVersion\\` must be a string; got ${shown(ifVersion)}`,\n );\n }\n return ifVersion;\n}\n\n/**\n * The per-call options of a registry port's `create` and `compareAndSwap`, checked: absent, or an object holding only\n * `held` (a record or `null`) and `onToken` (a function). A misspelt key would otherwise drop the hint or the token report\n * a publish relies on.\n */\nexport function checkRegistryWriteOptions(options: unknown): void {\n if (options === undefined) return;\n refuseUnknownOptions('write options', options, WRITE_OPTION_KEYS);\n const { held, onToken } = options as { held?: unknown; onToken?: unknown };\n if (held !== undefined && held !== null && typeof held !== 'object') {\n throw new ValidationError(\n `write options: \\`held\\` must be a registry record or null; got ${shown(held)}`,\n );\n }\n if (onToken !== undefined && typeof onToken !== 'function') {\n throw new ValidationError(\n `write options: \\`onToken\\` must be a function; got ${shown(onToken)}`,\n );\n }\n}\n", "/**\n * Shared key-grammar fragments used by every driver's logical-ref \u2192 physical-key mapping.\n *\n * One copy of the `_default` namespace sentinel for every driver that maps a namespace to a key or a path. Pure\n * string logic, no SDK, no I/O \u2014 it lives in the SDK-free `_shared` bundle so any driver may import it.\n */\n\n/**\n * The physical stand-in for an **absent** namespace. A caller MAY name a namespace `_default`; it simply does not collide, because\n * `namespaceKeyPart`/`namespacePathPart` encode the caller's namespace (to `%5Fdefault`) and emit this\n * sentinel literally. The separation is a property of the encoding, not of a grammar; reading it as a grammar is\n * exactly what would make the collision easy to reintroduce.\n */\nexport const DEFAULT_NAMESPACE = '_default';\n\n/**\n * The physical namespace component of an **object key**: the caller's namespace encoded, or the sentinel.\n *\n * The sentinel is emitted **literally, never encoded**, and that asymmetry is the whole point. Encoding it too\n * would send an absent namespace to `%5Fdefault` \u2014 exactly where a caller who names their namespace `_default`\n * already goes, since the encoder escapes their leading underscore. Both would land in one place and read each\n * other's data. Encoding only the caller's side means `_default` (ours) and `%5Fdefault` (theirs) are two\n * different strings, and no caller can produce the first.\n */\nexport function namespaceKeyPart(namespace: string | undefined): string {\n return namespace === undefined ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);\n}\n\n/** As {@link namespaceKeyPart}, for a filesystem path component. */\nexport function namespacePathPart(namespace: string | undefined): string {\n return namespace === undefined ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);\n}\n\n// The name codec lives in `core/` (pure string logic, and `validate.ts` needs it); imported here so the\n// drivers keep one import for key shaping, and re-exported for the same reason.\nimport { encodeNameForKey, encodeNameForPath } from '@/core/name-codec';\n\nexport {\n encodeNameForKey,\n decodeNameFromKey,\n encodeNameForPath,\n decodeNameFromPath,\n} from '@/core/name-codec';\n", "/**\n * Shared, SDK-free helpers for assembling + validating {@link RegistryRecord}s.\n *\n * The registry's record-construction, patch-application, and field-validation are identical across every\n * backend (memory / LocalFs / object store) \u2014 only the *storage* + OCC mechanics differ. This is the one home\n * for that logic so the three drivers can't drift. Pure: no I/O, no SDK, no clock (the caller passes `now`), and no\n * ambient randomness (the caller passes its `Entropy`; the Web Crypto default is in `entropy.ts`).\n */\nimport { IntegrityError, UnsupportedError, ValidationError } from '@/core/errors';\nimport type { Entropy } from '@/core/determinism';\nimport { canonicalMetadataJson, MAX_METADATA_BYTES } from '@/core/metadata';\nimport { fingerprintParts } from '@/core/crbm/fingerprint';\nimport { INCARNATION_TOKEN, incarnationOf } from '@/core/token';\nimport { renewsPointer } from '@/core/pointer-id';\nimport { MAX_SUMMARY_CARDINALITY, tailBytesFit } from '@/core/summary';\nimport { MAX_LEASES_PER_SEGMENT, MAX_STORED_LEASES } from '@/core/leases';\nimport { knownGensShapeFault } from '@/core/known-generations-shape';\nimport {\n MAX_KNOWN_GENERATIONS,\n MAX_STORED_KNOWN_GENERATIONS,\n usableKnownGens,\n} from '@/core/known-generations';\nimport type {\n KnownGeneration,\n LeaseEntry,\n NewRegistryRecord,\n RegistryPatch,\n RegistryRecord,\n RegistrySummary,\n SegmentRef,\n Token,\n} from '@/core/ports';\n\n/** The valid {@link RegistryStatus} values \u2014 used to validate both caller input and stored bytes. */\nconst STATUSES: readonly string[] = ['active', 'destroyed'];\n/**\n * The fields a stored record may carry: {@link RegistryRecord}'s, and nothing else. A field no reader resolves\n * through is refused on read-back like any other corruption (invariant 5), rather than ignored. A field the record\n * gains is listed here with its validator; the registry test fails to compile when this list and the record disagree.\n */\nexport const RECORD_FIELDS = [\n 'namespace',\n 'segment',\n 'currentGen',\n 'wrappedDeks',\n 'keyId',\n 'status',\n 'retention',\n 'residency',\n 'summary',\n 'createdAt',\n 'updatedAt',\n 'token',\n 'knownGens',\n 'leases',\n 'pointerId',\n] as const;\n/** The fields the registry sets on a row and a caller's write never does. */\nconst DRIVER_MANAGED: readonly string[] = ['token', 'pointerId'];\n/** The fields of the persisted envelope around a record. */\nconst ENVELOPE_FIELDS: readonly string[] = ['schemaVersion', 'deleted', 'record'];\n/** Cap on a serialized governance blob (retention/residency) \u2014 bounds row size so a row can't be bricked. */\nconst MAX_GOVERNANCE_BYTES = 64 * 1024;\n/** Bounds on the wrapped-DEK list (one DEK wrapped under a few KEKs) \u2014 keeps the row small + rejects abuse. */\nconst MAX_WRAPPED_DEKS = 8;\nconst MAX_WRAPPED_DEK_BYTES = 4 * 1024;\n/**\n * A sealed summary's fixed part: a 12-byte nonce, the u64 count, the object's fingerprint (a u64 size and a u32 footer\n * checksum), the u32 `tailBytes`, and a 16-byte tag.\n */\nconst SEALED_SUMMARY_MIN_BYTES = 12 + 8 + 8 + 4 + 4 + 16;\n/** The fixed part plus metadata at its cap. */\nconst SEALED_SUMMARY_MAX_BYTES = SEALED_SUMMARY_MIN_BYTES + MAX_METADATA_BYTES;\n/** Padded standard base64; canonical once its pad bits are checked too (see {@link base64PadBitsAreZero}). */\nconst BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n\n/** `null` is legal and meaningful: the segment has no Storage generation yet (see `RegistryRecord.currentGen`). */\nfunction validateGeneration(gen: number | null): void {\n if (gen === null) return;\n if (!Number.isSafeInteger(gen) || gen < 0) {\n throw new ValidationError(\n `currentGen must be a non-negative safe integer or null (no Storage generation yet); got ${gen}`,\n );\n }\n}\n\n/**\n * A patch that *mentions* `currentGen` must give it a real value \u2014 `null` to clear the pointer, or a generation.\n * `{ currentGen: undefined }` is refused rather than coerced. `{ currentGen: maybeUndefined }` is what `strict`\n * alone permits, since `exactOptionalPropertyTypes` is off, and a caller writing it means to leave the pointer\n * alone. Under presence-based merging the same call would silently *un-publish* a live segment: every Storage\n * generation goes invisible, and the collection then refuses to collect the objects (no pointer \u21D2 nothing\n * to compare against), so they are stranded and billed forever. Fail fast at the boundary instead.\n */\nfunction validatePatchGeneration(patch: RegistryPatch): void {\n if (!('currentGen' in patch)) return;\n // `validateGeneration(undefined)` fails the integer test on its own, because this check reads the raw patch\n // before `applyRegistryPatch` turns an undefined `currentGen` into `null`; this branch exists to make the\n // *message* say which value to pass instead, because the generic \"must be a non-negative integer or null\"\n // reads like a type error rather than the trap it is.\n if (patch.currentGen === undefined) {\n throw new ValidationError(\n `currentGen was present in the patch but undefined. Pass \\`null\\` to clear the pointer (the segment has no ` +\n `Storage generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it ` +\n `would un-publish the segment's Storage data.`,\n );\n }\n validateGeneration(patch.currentGen);\n}\n\nfunction validateStatus(status: string): void {\n if (!STATUSES.includes(status)) {\n throw new ValidationError(`status must be one of ${STATUSES.join('/')}; got ${status}`);\n }\n}\n\n/**\n * Validate a governance blob (retention/residency) at the write boundary: it must be JSON-serializable\n * (a `BigInt`/function/circular value fails fast with a typed error instead of a cryptic one deep in a\n * driver) and within a size cap (so an oversized blob can't write a row that's later unreadable \u2014 a brick).\n */\nfunction validateGovernance(meta: unknown, field: string): void {\n if (meta === undefined) return;\n // A plain object, not `null`/an array/a primitive. `JSON.stringify(null)` is 4 valid bytes, so without this the\n // size + serializability checks below happily store `\"retention\": null` \u2014 and `retention.expiresAt` is read with\n // an `in` test, which throws an untyped `TypeError` on a non-object and would abort a whole fleet retention\n // sweep rather than becoming one ledger entry. Reject at the write boundary; the read boundary\n // ({@link assertStoredRecordShape}) rejects it too, for a row edited by hand or written by another writer.\n if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {\n throw new ValidationError(\n `${field} must be a plain object (got ${meta === null ? 'null' : typeof meta})`,\n );\n }\n let json: string;\n try {\n json = JSON.stringify(meta);\n } catch {\n throw new ValidationError(`${field} must be JSON-serializable`);\n }\n if (json.length > MAX_GOVERNANCE_BYTES) {\n throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);\n }\n}\n\n/**\n * Whose value a validator is looking at: one a `caller` is writing (a bad one is a ValidationError, a list or summary\n * it returns is a frozen copy, and `knownGens` is held to {@link MAX_KNOWN_GENERATIONS}), or a row read back from\n * `stored` bytes (a bad one is an IntegrityError, `knownGens` is held to {@link MAX_STORED_KNOWN_GENERATIONS}, and the\n * parsed value is kept).\n */\ntype RecordOrigin = 'caller' | 'stored';\n\n/**\n * Validate the wrapped-DEK list at the write/read boundary: a bounded array of `{ keyId, wrapped }` with\n * non-empty string fields and a per-entry size cap (so a row can't be bricked, and corrupt bytes are rejected\n * rather than reaching the keystore). `origin` toggles the error class (write = ValidationError; read =\n * IntegrityError, invariant 5).\n */\nfunction validateWrappedDeks(value: unknown, origin: RecordOrigin): void {\n if (value === undefined) return;\n const fail = (msg: string): never => {\n throw origin === 'stored' ? new IntegrityError(msg) : new ValidationError(msg);\n };\n if (!Array.isArray(value)) fail('wrappedDeks must be an array');\n const list = value as unknown[];\n if (list.length === 0) fail('wrappedDeks must be non-empty when present');\n if (list.length > MAX_WRAPPED_DEKS)\n fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);\n for (const w of list) {\n if (w === null || typeof w !== 'object') fail('wrappedDeks entry must be an object');\n const { keyId, wrapped } = w as { keyId?: unknown; wrapped?: unknown };\n if (typeof keyId !== 'string' || keyId.length === 0)\n fail('wrappedDeks entry needs a non-empty keyId');\n if (typeof wrapped !== 'string' || wrapped.length === 0)\n fail('wrappedDeks entry needs a non-empty wrapped blob');\n if ((wrapped as string).length > MAX_WRAPPED_DEK_BYTES)\n fail(`wrappedDeks entry is ${(wrapped as string).length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);\n }\n}\n\n/** The standard base64 alphabet, whose index of a character is the six bits it carries. */\nconst BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n/**\n * Whether a padded standard base64 string is canonical: the bits of its last character past the data are zero.\n * Without this two strings would decode to the same bytes.\n */\nfunction base64PadBitsAreZero(text: string): boolean {\n const pad = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0;\n if (pad === 0) return true;\n const last = BASE64_ALPHABET.indexOf(text[text.length - pad - 1]!);\n return pad === 2 ? (last & 0b1111) === 0 : (last & 0b11) === 0;\n}\n\n/**\n * Validate a {@link RegistrySummary}'s shape at the write or read boundary, and return a frozen copy built from the\n * checked fields only: exactly one of the two shapes, a non-negative safe-integer generation, a cardinality from 0 to\n * 2^32, the fingerprint of an object (`<size>:<footer checksum>`, a size no smaller than a `.crbm` preamble and footer\n * and a 32-bit checksum), `tailBytes` (an integer from a footer's size to the object's size less its preamble, and at\n * most the cap a reader fetches for one open), metadata by the metadata rules (never the empty object, which is never stored) with its keys\n * in canonical order, or a sealed blob in canonical base64 whose length fits a sealed count and fingerprint plus\n * metadata at its cap. A writer\n * stores the copy, so a caller that changes its object after the call, while the driver awaits the row, changes\n * nothing that is written. Whether a summary may be *used* is the reader's rule, not this one: this only bounds what a\n * row can hold. `origin` picks the error class (write = ValidationError; read = IntegrityError, invariant 5), and a\n * read names the row by `ctx`.\n */\nfunction validateSummary(\n value: unknown,\n origin: RecordOrigin,\n ctx?: string,\n): RegistrySummary | undefined {\n if (value === undefined) return undefined;\n const fail = (msg: string): never => {\n const where = ctx === undefined ? '' : `: ${ctx}`;\n throw origin === 'stored'\n ? new IntegrityError(`registry record summary: ${msg}${where}`)\n : new ValidationError(`summary: ${msg}${where}`);\n };\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n fail(\n `must be an object (got ${value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value})`,\n );\n }\n const s = value as Record<string, unknown>;\n const keys = Object.keys(s);\n const isSealed = keys.includes('sealed');\n const allowed = isSealed\n ? ['generation', 'sealed']\n : ['generation', 'cardinality', 'fingerprint', 'tailBytes', 'metadata'];\n const extra = keys.filter((k) => !allowed.includes(k));\n if (extra.length > 0) {\n fail(\n `has fields its ${isSealed ? 'sealed' : 'clear'} shape does not declare (${extra.join(', ')})`,\n );\n }\n const generation = s.generation;\n if (!Number.isSafeInteger(generation) || (generation as number) < 0) {\n fail(`generation must be a non-negative safe integer (got ${String(generation)})`);\n }\n if (isSealed) {\n const sealed = s.sealed;\n if (typeof sealed !== 'string' || !BASE64.test(sealed) || !base64PadBitsAreZero(sealed)) {\n fail('sealed must be canonical, padded standard base64');\n }\n const text = sealed as string;\n const bytes = (text.length / 4) * 3 - (text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0);\n if (bytes < SEALED_SUMMARY_MIN_BYTES || bytes > SEALED_SUMMARY_MAX_BYTES) {\n fail(\n `sealed is ${bytes}B, outside ${SEALED_SUMMARY_MIN_BYTES}..${SEALED_SUMMARY_MAX_BYTES}B ` +\n '(a nonce, a fixed-width count, fingerprint and tail size, a tag, and metadata up to its cap)',\n );\n }\n return Object.freeze({ generation: generation as number, sealed: text });\n }\n const n = s.cardinality;\n if (!Number.isInteger(n) || (n as number) < 0 || (n as number) > MAX_SUMMARY_CARDINALITY) {\n fail(`cardinality must be an integer from 0 to 2^32 (got ${String(n)})`);\n }\n const fingerprint = s.fingerprint;\n const parts = fingerprintParts(fingerprint);\n if (parts === undefined) {\n fail(\n `fingerprint must be an object's, <size>:<footer checksum> in decimal (got ${shown(JSON.stringify(fingerprint))})`,\n );\n }\n const tailBytes = s.tailBytes;\n if (!tailBytesFit(tailBytes, parts!.size)) {\n fail(\n `tailBytes must be an integer from the footer's size to the object's size less its preamble, and at most ` +\n `the cap of one open's tail read (got ${shown(JSON.stringify(tailBytes))} for an object of ${parts!.size} bytes)`,\n );\n }\n if (s.metadata === undefined) {\n return Object.freeze({\n generation: generation as number,\n cardinality: n as number,\n fingerprint: fingerprint as string,\n tailBytes: tailBytes as number,\n });\n }\n const canonical = canonicalMetadataJson(s.metadata, fail);\n if (canonical === '{}') fail('metadata is empty; a generation without metadata carries none');\n // A read only checks the stored shape and keeps the object it parsed, so it builds no copy.\n if (origin === 'stored') return s as unknown as RegistrySummary;\n // Rebuilt from the canonical JSON, so its keys are in canonical order (JavaScript still lists integer-like keys\n // first, in numeric order, as it does in every object) and nothing of the caller's object is kept.\n const metadata = Object.freeze(JSON.parse(canonical) as Record<string, string | number>);\n return Object.freeze({\n generation: generation as number,\n cardinality: n as number,\n fingerprint: fingerprint as string,\n tailBytes: tailBytes as number,\n metadata,\n });\n}\n\n/** A summary given at a write must describe the generation the row will point at. */\nfunction validateSummaryNames(\n summary: RegistrySummary | undefined,\n currentGen: number | null,\n): void {\n if (summary !== undefined && summary.generation !== currentGen) {\n throw new ValidationError(\n `summary describes generation ${summary.generation}, but the row points at ${String(currentGen)}`,\n );\n }\n}\n\n/**\n * Whether a summary's shape agrees with the row's encryption: sealed on a row with wrapped keys, clear on one without.\n * A clear count or metadata beside a key would leak what the segment's objects hide; a sealed one on a cleartext row\n * could never be opened.\n */\nfunction summaryAgreesWithKeys(\n summary: RegistrySummary,\n wrappedDeks: RegistryRecord['wrappedDeks'],\n): boolean {\n return 'sealed' in summary === (wrappedDeks !== undefined);\n}\n\n/** A summary given at a write must agree with the row's encryption (see {@link summaryAgreesWithKeys}). */\nfunction validateSummaryKeys(\n summary: RegistrySummary | undefined,\n wrappedDeks: RegistryRecord['wrappedDeks'],\n): void {\n if (summary !== undefined && !summaryAgreesWithKeys(summary, wrappedDeks)) {\n throw new ValidationError(\n wrappedDeks === undefined\n ? 'summary is sealed, but the row has no wrapped keys to open it with'\n : \"summary is in the clear, but the row is encrypted: an encrypted segment's summary must be sealed\",\n );\n }\n}\n\n/**\n * Validate a {@link RegistryRecord.knownGens} list's shape at the write or read boundary: an array of at most `max`\n * entries (192 written, 384 read), each exactly `{ generation, footerCrc }`, the generation a non-negative safe integer\n * and unique, the checksum an integer from 0 to 2^32 - 1. The order is the order the generations were made current and\n * is kept as given: it cannot be checked, since it is not derivable from the numbers. A write returns a frozen copy, so\n * a caller that changes its array afterwards changes nothing that is written. `origin` picks the error class (write =\n * ValidationError; read = IntegrityError, invariant 5). Whether the last entry is the pointer, and whether its checksum\n * is the summary's, is not checked here on a read: a ledger that disagrees with its row is one its reader does not use,\n * not a row to refuse.\n */\nfunction validateKnownGens(\n value: unknown,\n origin: RecordOrigin,\n ctx?: string,\n): readonly KnownGeneration[] | undefined {\n if (value === undefined) return undefined;\n const fail = (msg: string): never => {\n const where = ctx === undefined ? '' : `: ${ctx}`;\n throw origin === 'stored'\n ? new IntegrityError(`registry record knownGens: ${msg}${where}`)\n : new ValidationError(`knownGens: ${msg}`);\n };\n const max = origin === 'stored' ? MAX_STORED_KNOWN_GENERATIONS : MAX_KNOWN_GENERATIONS;\n const fault = knownGensShapeFault(value, max, shown);\n if (fault !== undefined) fail(fault);\n const out: KnownGeneration[] = [];\n for (const entry of value as readonly KnownGeneration[]) {\n out.push(\n origin === 'stored'\n ? entry\n : Object.freeze({ generation: entry.generation, footerCrc: entry.footerCrc }),\n );\n }\n return origin === 'stored' ? out : Object.freeze(out);\n}\n\nconst HOLDER = /^[0-9a-f]{16}$/;\nconst LEASE_ENTRY_KEYS: readonly string[] = ['holder', 'generation', 'until'];\n\n/**\n * Validate a {@link RegistryRecord.leases} list at the write or read boundary: an array of at most\n * {@link MAX_STORED_LEASES} entries whatever the origin (a write's own, smaller cap is applied against the row it\n * patches), each entry exactly `{ holder, generation, until }`, the holder 16 lowercase hex digits\n * and unique, the two numbers non-negative safe integers. A write returns a frozen copy, and an empty list as\n * `undefined`, so a row that never held a lease is the row it was before leases. `origin` picks the error class\n * (write = ValidationError; read = IntegrityError, invariant 5). Whether an entry is still live, or its generation\n * exists, is not checked here: a collector reads that, and a row is not refused for it.\n */\nfunction validateLeases(\n value: unknown,\n origin: RecordOrigin,\n ctx?: string,\n): readonly LeaseEntry[] | undefined {\n if (value === undefined) return undefined;\n const fail = (msg: string): never => {\n const where = ctx === undefined ? '' : `: ${ctx}`;\n throw origin === 'stored'\n ? new IntegrityError(`registry record leases: ${msg}${where}`)\n : new ValidationError(`leases: ${msg}`);\n };\n if (!Array.isArray(value)) fail('must be an array');\n const list = value as unknown[];\n const max = MAX_STORED_LEASES; // a write's own cap is applied against the row it patches\n if (list.length > max) fail(`has ${list.length} entries, cap ${max}`);\n const seen = new Set<string>();\n const out: LeaseEntry[] = [];\n for (const e of list) {\n if (typeof e !== 'object' || e === null || Array.isArray(e)) fail('an entry must be an object');\n const entry = e as Record<string, unknown>;\n const extra = Object.keys(entry).filter((k) => !LEASE_ENTRY_KEYS.includes(k));\n if (extra.length > 0) fail(`an entry carries an undeclared field (${extra.join(', ')})`);\n const { holder, generation, until } = entry;\n if (typeof holder !== 'string' || !HOLDER.test(holder)) {\n fail('a holder must be 16 lowercase hex digits');\n }\n if (seen.has(holder as string)) fail('holders must be unique');\n seen.add(holder as string);\n for (const [name, n] of [\n ['generation', generation],\n ['until', until],\n ] as const) {\n if (!Number.isSafeInteger(n) || (n as number) < 0) {\n fail(`${name} must be a non-negative safe integer (got ${String(n)})`);\n }\n }\n out.push(\n origin === 'stored'\n ? (entry as unknown as LeaseEntry)\n : Object.freeze({\n holder: holder as string,\n generation: generation as number,\n until: until as number,\n }),\n );\n }\n if (origin === 'stored') return out;\n return out.length === 0 ? undefined : Object.freeze(out);\n}\n\n/** A non-empty list given at a write needs a pointer to hold generations of. */\nfunction validateLeasesPointer(\n leases: readonly LeaseEntry[] | undefined,\n currentGen: number | null,\n): void {\n if (leases !== undefined && leases.length > 0 && currentGen === null) {\n throw new ValidationError('leases: a row with no pointer has no generation to lease');\n }\n}\n\n/**\n * A ledger given at a write must fit the row it makes: none on a row with no pointer, its last entry the row's resulting\n * `currentGen`, and, where the row's resulting summary is in the clear and describes that generation, its checksum the\n * one the summary's fingerprint carries. A registry cannot check a sealed summary's: the library's writers take both\n * values from one object.\n */\nfunction validateKnownGensFit(\n knownGens: readonly KnownGeneration[] | undefined,\n currentGen: number | null,\n summary: RegistrySummary | undefined,\n): void {\n if (knownGens === undefined) return;\n if (currentGen === null) {\n throw new ValidationError('knownGens: a row with no pointer has no generation to know');\n }\n const last = knownGens[knownGens.length - 1];\n if (last === undefined || last.generation !== currentGen) {\n throw new ValidationError(\n `knownGens must end with the row's pointer ${currentGen}, in the order the generations were made current ` +\n `(its last entry is ${last === undefined ? 'missing' : last.generation})`,\n );\n }\n if (\n summary !== undefined &&\n summary.generation === currentGen &&\n 'fingerprint' in summary &&\n fingerprintParts(summary.fingerprint)?.footerCrc !== last.footerCrc\n ) {\n throw new ValidationError(\n `knownGens records footerCrc ${last.footerCrc} for generation ${currentGen}, but the summary's ` +\n `fingerprint ${summary.fingerprint} carries another: both describe the one current object`,\n );\n }\n}\n\n/**\n * A write that names a field the registry sets (`token`, `pointerId`) is refused rather than stored or ignored: the\n * type does not allow either, so one arrives only from a caller in JavaScript, or a whole record passed as a patch, and\n * either would read as if it had set the row's identity.\n */\nfunction refuseDriverManaged(write: object, what: string): void {\n const named = DRIVER_MANAGED.filter((field) => field in write);\n if (named.length > 0) {\n throw new ValidationError(\n `${what} names ${named.join(' and ')}, which the registry sets on every write and a caller never does`,\n );\n }\n}\n\n/**\n * A `keyId` is absent or a non-empty string. A read accepts any string, so this is stricter: an empty id names no key,\n * and a value of another type would make the row unreadable.\n */\nfunction validateKeyId(keyId: unknown): void {\n if (keyId !== undefined && (typeof keyId !== 'string' || keyId.length === 0)) {\n throw new ValidationError('keyId must be a non-empty string, or absent');\n }\n}\n\n/**\n * Validate the caller-settable fields at `create`, and return the record to store: the caller's, with its summary\n * replaced by the checked, frozen copy {@link validateSummary} builds.\n */\nexport function validateNewRegistryRecord(rec: NewRegistryRecord): NewRegistryRecord {\n refuseDriverManaged(rec, 'a new record');\n validateKeyId(rec.keyId);\n validateGeneration(rec.currentGen);\n if (rec.status !== undefined) validateStatus(rec.status);\n validateWrappedDeks(rec.wrappedDeks, 'caller');\n validateGovernance(rec.retention, 'retention');\n validateGovernance(rec.residency, 'residency');\n const knownGens = validateKnownGens(rec.knownGens, 'caller');\n const withKnown = knownGens === undefined ? rec : { ...rec, knownGens };\n const summary = 'summary' in rec ? validateSummary(rec.summary, 'caller') : undefined;\n validateKnownGensFit(knownGens, rec.currentGen, summary);\n if (!('summary' in rec)) return withKnown;\n validateSummaryNames(summary, rec.currentGen);\n validateSummaryKeys(summary, rec.wrappedDeks);\n return { ...withKnown, summary };\n}\n\n/**\n * Validate the caller-settable fields in a `compareAndSwap` patch, and return the patch to apply: the caller's, with\n * its summary replaced by the checked, frozen copy. That a given `summary` names the row's resulting `currentGen`,\n * and agrees with its resulting keys, needs the stored row, so {@link applyRegistryPatch} checks both.\n */\nexport function validateRegistryPatch(patch: RegistryPatch): RegistryPatch {\n refuseDriverManaged(patch, 'a patch');\n if ('keyId' in patch) validateKeyId(patch.keyId);\n validatePatchGeneration(patch);\n if (patch.status !== undefined) validateStatus(patch.status);\n if ('wrappedDeks' in patch) validateWrappedDeks(patch.wrappedDeks, 'caller');\n if ('retention' in patch) validateGovernance(patch.retention, 'retention');\n if ('residency' in patch) validateGovernance(patch.residency, 'residency');\n const withKnown =\n patch.knownGens === undefined\n ? patch\n : { ...patch, knownGens: validateKnownGens(patch.knownGens, 'caller') };\n const withLeases =\n 'leases' in withKnown\n ? { ...withKnown, leases: validateLeases(withKnown.leases, 'caller') }\n : withKnown;\n if (!('summary' in patch)) return withLeases;\n return { ...withLeases, summary: validateSummary(patch.summary, 'caller') };\n}\n\n/**\n * The persisted envelope for a registry row: the record plus a tombstone flag. A **deleted** row keeps its\n * record (with an advanced counter) rather than being removed, so a re-create carries the counter on and the two\n * incarnations' tokens are apart by construction, as well as by their random parts \u2014 ABA-safety. A row removed by a\n * conditional delete leaves no counter to carry on, so a re-create starts at 0 and only the random parts keep its\n * tokens apart from the old ones. Shared by the\n * persistent drivers (LocalFs file, S3 object).\n *\n * `schemaVersion` is a wire-only concern \u2014 it stamps the persisted bytes, not the in-memory domain object \u2014\n * so it lives on the serialize/parse boundary ({@link serializeRegistryEnvelope} /\n * {@link parseRegistryEnvelope}), never on this interface.\n */\nexport interface RegistryEnvelope {\n readonly deleted: boolean;\n readonly record: RegistryRecord;\n}\n\n/**\n * Current registry-row schema version \u2014 a pre-1.0 format-freeze prerequisite. Every persistent\n * registry driver \u2014 every one of which persists the `{ deleted, record }` envelope \u2014 stamps its rows\n * with this so a reader can fail-closed on a layout it does not read instead of misparsing it. Bump only\n * on a backward-incompatible change. Policy: any stamp but this one \u2192 `UnsupportedError` (fail-closed); an\n * **absent** or malformed stamp \u2192 `IntegrityError`, since every row this build writes has one.\n *\n * Schema 5 is the record with `knownGens`, the ledger of the generations a row made current (each with its object's footer\n * checksum, in the order they were made current), and a summary that records `tailBytes`. This build reads schema 5 and\n * nothing else: a row stamped lower carries no ledger and, below 4, no `pointerId` either, so nothing could say which\n * generations the row published or which write set what it resolves to.\n */\nexport const REGISTRY_SCHEMA_VERSION = 5;\n\n/**\n * Validate a persisted registry row's `schemaVersion` (untrusted bytes, invariant 5) and return it: an absent or\n * malformed value \u2192 `IntegrityError`; any version but {@link REGISTRY_SCHEMA_VERSION} \u2192 `UnsupportedError`\n * (fail-closed rather than misread a format this build does not read), naming what to do for an earlier one. The steps\n * are in the changelog's entry for the release that introduced the schema; the message names that entry and no anchor,\n * which cannot go stale.\n */\nexport function assertRegistrySchemaVersion(raw: unknown, ctx: string): number {\n if (raw === undefined) {\n throw new IntegrityError(`registry row has no schemaVersion: ${ctx}`);\n }\n if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 1) {\n throw new IntegrityError(`registry row has a malformed schemaVersion (${shown(raw)}): ${ctx}`);\n }\n if (raw < REGISTRY_SCHEMA_VERSION) {\n throw new UnsupportedError(\n `registry row schemaVersion ${raw} is one this build does not read: it reads schema ` +\n `${REGISTRY_SCHEMA_VERSION} only. Export the segments with the release that wrote the row and load them ` +\n `with this one into a new prefix (steps: ` +\n `https://github.com/cloudbitmaps/cloudbitmaps/blob/main/CHANGELOG.md, the Breaking entry on registry schema 5): ${ctx}`,\n );\n }\n if (raw > REGISTRY_SCHEMA_VERSION) {\n throw new UnsupportedError(\n `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`,\n );\n }\n return raw;\n}\n\n/** Serialize a registry envelope for persistence, stamping the current {@link REGISTRY_SCHEMA_VERSION}. */\nexport function serializeRegistryEnvelope(env: RegistryEnvelope): string {\n return JSON.stringify({\n schemaVersion: REGISTRY_SCHEMA_VERSION,\n deleted: env.deleted,\n record: env.record,\n });\n}\n\n// \u2500\u2500 The OCC token \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// A token is `<incarnation>.<counter>.<write>`. The incarnation is 32 lowercase hex digits drawn from injected\n// entropy when the row is created; it keeps a re-created name from ever being taken for an earlier incarnation of\n// it, even once the earlier row's record is gone and nothing is left to continue a counter from. The counter is a\n// canonical decimal that every write of the row advances by one, and carries on across a tombstone, so while a row's\n// history is intact its tokens are apart by construction. The write part is 16 lowercase hex digits drawn afresh for\n// every write: a row restored from a backup is back at an older counter, and without it the writes after the restore\n// would be given the tokens the writes after the backup already had. Only a create starts a new incarnation. Tokens\n// are compared only by equality; nothing orders them. A row's `pointerId` is the token of one of its writes, so it has\n// the same form, the row's incarnation, and a counter no higher than the token's.\n\n/** The incarnation id's width, in bytes. */\nconst INCARNATION_BYTES = 16;\n/** The write part's width, in bytes. */\nconst WRITE_BYTES = 8;\n\n/** A token taken apart: its incarnation and its counter. */\ninterface TokenParts {\n readonly incarnation: string;\n readonly counter: number;\n}\n\n/**\n * Take a token apart, refusing anything that is not the form a shipped driver writes (invariant 5): `\"7\"`, `\"1e3\"`,\n * `\"0x10\"`, `\" 5 \"`, `\"\"`, a counter past 2^53, upper-case hex. `what` names the field in the message.\n */\nfunction tokenParts(token: unknown, ctx: string, what = 'token'): TokenParts {\n const match = typeof token === 'string' ? INCARNATION_TOKEN.exec(token) : null;\n if (match === null) {\n throw new IntegrityError(\n `registry row ${what} is not one a shipped registry writes (${shown(JSON.stringify(token))}): ${ctx}`,\n );\n }\n const counter = Number(match[2]);\n if (!Number.isSafeInteger(counter)) {\n throw new IntegrityError(`registry row ${what}'s counter is out of safe-integer range: ${ctx}`);\n }\n return { incarnation: match[1]!, counter };\n}\n\n/**\n * Refuse a stored `pointerId` a shipped registry cannot have written (invariant 5): one in another form, of another\n * incarnation than the row's token, or with a higher counter than the token's. One of another incarnation was spliced\n * from another row, and a reader trusting it would key a cache on another incarnation's identity.\n */\nfunction checkPointerId(pointerId: unknown, token: TokenParts, ctx: string): void {\n const parts = tokenParts(pointerId, ctx, 'pointerId');\n if (parts.incarnation !== token.incarnation) {\n throw new IntegrityError(\n `registry row pointerId names another incarnation than its token: ${ctx}`,\n );\n }\n if (parts.counter > token.counter) {\n throw new IntegrityError(`registry row pointerId is a write after its token: ${ctx}`);\n }\n}\n\nexport { usableKnownGens };\n\n// `incarnationOf` is defined in `core/token`, where the publish reads it too, and is the same function here.\nexport { incarnationOf };\n\n/** `bytes` fresh bytes from `entropy`, as lowercase hex, refusing a source that does not give them. */\nfunction drawHex(entropy: Entropy, bytes: number): string {\n const drawn = entropy(bytes);\n if (!(drawn instanceof Uint8Array) || drawn.length !== bytes) {\n throw new ValidationError(\n `the registry's entropy source must return ${bytes} bytes as a Uint8Array`,\n );\n }\n let hex = '';\n for (const b of drawn) hex += b.toString(16).padStart(2, '0');\n return hex;\n}\n\n/** A fresh incarnation id from `entropy`: 128 bits as 32 lowercase hex digits. */\nexport function drawIncarnation(entropy: Entropy): string {\n return drawHex(entropy, INCARNATION_BYTES);\n}\n\n/** A fresh write part from `entropy`: 64 bits as 16 lowercase hex digits. */\nexport function drawWrite(entropy: Entropy): string {\n return drawHex(entropy, WRITE_BYTES);\n}\n\n/**\n * The token for the write after `record`'s: the same incarnation, the counter one on, and a fresh write part. For a\n * compare-and-swap or a tombstone.\n */\nexport function nextRegistryToken(record: RegistryRecord, entropy: Entropy): Token {\n const { incarnation, counter } = tokenParts(record.token, record.segment);\n return `${incarnation}.${counter + 1}.${drawWrite(entropy)}`;\n}\n\n/**\n * The token of a new row: a fresh incarnation and write part from `entropy`, and a counter that continues\n * `tombstone`'s when the row is created over one, else starts at 0.\n */\nexport function newIncarnationToken(\n entropy: Entropy,\n tombstone: RegistryRecord | undefined,\n): Token {\n const counter =\n tombstone === undefined ? 0 : tokenParts(tombstone.token, tombstone.segment).counter + 1;\n return `${drawIncarnation(entropy)}.${counter}.${drawWrite(entropy)}`;\n}\n\n/** A stored value as a message shows it: at most 64 characters, so a hostile row cannot fill every log line it reaches. */\nfunction shown(v: unknown): string {\n const text = String(v);\n return text.length > 64 ? `${text.slice(0, 64)}\u2026` : text;\n}\n\n/**\n * Parse + structurally validate a persisted `{ deleted, record }` envelope from stored bytes. A published row\n * is always whole (atomic write), so a parse failure or a missing/mistyped field means corruption/tampering \u2014\n * fail fast (invariant 5), never silently report \"absent\". `ctx` names the source (path/key) for the message.\n * `expected` is the segment the row was read under; a row that names another is refused.\n */\nexport function parseRegistryEnvelope(\n text: string,\n ctx: string,\n expected?: SegmentRef,\n): RegistryEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);\n }\n // Guard before any property access: `JSON.parse` yields `null`/primitives for hostile bytes (e.g. `\"null\"`,\n // `\"5\"`), which would throw an *untyped* TypeError on the dereference below (invariant 5 wants a typed error).\n if (parsed === null || typeof parsed !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const env = parsed as { schemaVersion?: unknown; deleted?: unknown; record?: unknown };\n assertRegistrySchemaVersion(env.schemaVersion, ctx);\n const extra = Object.keys(env).filter((k) => !ENVELOPE_FIELDS.includes(k));\n if (extra.length > 0) {\n throw new IntegrityError(\n `registry row has fields its envelope does not declare (${shown(extra.join(', '))}): ${ctx}`,\n );\n }\n if (typeof env.deleted !== 'boolean' || env.record === null || typeof env.record !== 'object') {\n throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);\n }\n const r = env.record as Record<string, unknown>;\n assertStoredRecordShape(r, ctx);\n if (typeof r.token !== 'string') {\n throw new IntegrityError(`registry row is missing its token: ${ctx}`);\n }\n checkPointerId(r.pointerId, tokenParts(r.token, ctx), ctx);\n // A row is the row of the name it is stored under. One copied or restored under another name would read as that\n // segment while naming this one, and every sweep that acts on the name it carries (an erasure, a report, a retention\n // pass) would act on the wrong segment, or on this one twice.\n if (\n expected !== undefined &&\n (r.segment !== expected.segment ||\n (r.namespace ?? undefined) !== (expected.namespace ?? undefined))\n ) {\n throw new IntegrityError(\n `registry row names another segment than the one it is stored for: ${ctx}`,\n );\n }\n return { deleted: env.deleted, record: env.record as RegistryRecord };\n}\n\n/**\n * Build a full record from a {@link NewRegistryRecord} plus identity, audit timestamps, and an OCC token. The create\n * sets what the row resolves to, so its `pointerId` is its own token.\n */\nexport function recordFromNew(\n ref: SegmentRef,\n rec: NewRegistryRecord,\n now: number,\n token: Token,\n): RegistryRecord {\n return {\n namespace: ref.namespace,\n segment: ref.segment,\n currentGen: rec.currentGen,\n wrappedDeks: rec.wrappedDeks,\n keyId: rec.keyId,\n status: rec.status ?? 'active',\n retention: rec.retention,\n residency: rec.residency,\n summary: rec.summary,\n knownGens: rec.knownGens,\n // A new row has no holder: a lease is written only against a row that already exists.\n leases: undefined,\n createdAt: now,\n updatedAt: now,\n token,\n pointerId: token,\n };\n}\n\n/**\n * Fail-fast structural check on a record read back from a persistent tier (untrusted bytes, invariant 5):\n * a published row is always whole, so a missing/mistyped required field means corruption/tampering \u2014 reject\n * it rather than silently report \"absent\". The form of `token` and `pointerId`, and how the two relate, is checked by\n * the caller.\n */\nexport function assertStoredRecordShape(r: Record<string, unknown>, ctx: string): void {\n if (\n typeof r.segment !== 'string' ||\n typeof r.pointerId !== 'string' ||\n (r.namespace !== undefined && typeof r.namespace !== 'string') ||\n (r.currentGen !== null && typeof r.currentGen !== 'number') ||\n typeof r.status !== 'string' ||\n typeof r.createdAt !== 'number' ||\n typeof r.updatedAt !== 'number'\n ) {\n throw new IntegrityError(`registry record is missing required fields: ${ctx}`);\n }\n // Enforce the same value invariants the write path checks, so corrupt/tampered bytes are rejected at the\n // read boundary (invariant 5) rather than leaking a bad currentGen/status downstream.\n if (r.currentGen !== null && (!Number.isSafeInteger(r.currentGen) || r.currentGen < 0)) {\n throw new IntegrityError(\n `registry record has an invalid currentGen (${shown(r.currentGen)}): ${ctx}`,\n );\n }\n if (!STATUSES.includes(r.status)) {\n throw new IntegrityError(`registry record has an unknown status (${shown(r.status)}): ${ctx}`);\n }\n const declared: readonly string[] = RECORD_FIELDS;\n const extra = Object.keys(r).filter((k) => !declared.includes(k));\n if (extra.length > 0) {\n throw new IntegrityError(\n `registry record has fields a schema-${REGISTRY_SCHEMA_VERSION} row does not declare (${shown(extra.join(', '))}): ${ctx}`,\n );\n }\n if (r.keyId !== undefined && typeof r.keyId !== 'string') {\n throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);\n }\n validateWrappedDeks(r.wrappedDeks, 'stored'); // invariant 5: reject a corrupt wrapped-DEK list on read-back\n // The governance blobs' SHAPE is checked on read-back too, because one of them carries semantics:\n // `retention.expiresAt` is read with an `in` test, which throws an untyped `TypeError` on a stored\n // `null`/string/number. That is reachable \u2014 the write boundary only checks\n // JSON-serializability and size, so `\"retention\": null` round-trips through every driver from a hand-edit,\n // another writer, or a restore \u2014 and it would abort a whole fleet retention sweep rather than becoming one\n // ledger entry. Reject it here, typed, at the trust boundary (invariant 5).\n for (const field of ['retention', 'residency'] as const) {\n const meta = r[field];\n if (meta !== undefined && (meta === null || typeof meta !== 'object' || Array.isArray(meta))) {\n throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);\n }\n }\n // Shape only. Whether the summary agrees with the row's keys and names its `currentGen` is not checked here: a\n // stored row that disagrees is a summary its reader must not use, not a row to refuse, and refusing it would let\n // one row stop every listing that reaches it.\n validateSummary(r.summary, 'stored', ctx);\n validateKnownGens(r.knownGens, 'stored', ctx);\n validateLeases(r.leases, 'stored', ctx);\n}\n\n/**\n * Apply a patch to an existing record, returning a new one with a fresh `updatedAt` + `token` (identity and\n * `createdAt` are preserved). Optional fields use `'k' in patch` so a patch can *clear* them (set to\n * `undefined`, e.g. dropping `keyId` on crypto-shred); required fields use `??` (they always have a value). A patch\n * that names a resolved field ({@link renewsPointer}) sets `pointerId` to this write's token; any other keeps it.\n *\n * `summary` describes the current generation, so it follows the pointer and the keys: a patch that moves\n * `currentGen`, or changes `wrappedDeks` so the summary's shape no longer agrees, without mentioning `summary` drops\n * the old one; and a summary the patch gives must name the resulting `currentGen` and agree with the resulting keys\n * (`ValidationError`, before anything is written).\n */\nexport function applyRegistryPatch(\n prev: RegistryRecord,\n patch: RegistryPatch,\n now: number,\n token: Token,\n): RegistryRecord {\n // `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value\n // here (no Storage generation yet), and `??` treats it as absent \u2014 so the nullish form would silently ignore a\n // patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and\n // `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.\n const currentGen = 'currentGen' in patch ? (patch.currentGen ?? null) : prev.currentGen;\n const wrappedDeks = 'wrappedDeks' in patch ? patch.wrappedDeks : prev.wrappedDeks;\n let summary: RegistrySummary | undefined;\n if ('summary' in patch) {\n validateSummaryNames(patch.summary, currentGen);\n validateSummaryKeys(patch.summary, wrappedDeks);\n summary = patch.summary;\n } else {\n // Kept only while it still describes the row: the same generation, and the same encryption.\n const kept = currentGen === prev.currentGen ? prev.summary : undefined;\n summary = kept !== undefined && summaryAgreesWithKeys(kept, wrappedDeks) ? kept : undefined;\n }\n // The ledger ends with the pointer, so it follows the pointer as the summary does: kept while the pointer stays,\n // dropped when it moves without the patch naming the ledger, and checked against the pointer and summary it gets.\n let knownGens: readonly KnownGeneration[] | undefined;\n if ('knownGens' in patch) {\n validateKnownGensFit(patch.knownGens, currentGen, summary);\n knownGens = patch.knownGens;\n } else {\n knownGens = currentGen === prev.currentGen ? prev.knownGens : undefined;\n }\n // Leases name generations a holder pinned, and the pointer moving does not end them: kept unless the patch names the list.\n let leases: readonly LeaseEntry[] | undefined;\n if ('leases' in patch) {\n leases = patch.leases;\n validateLeasesPointer(leases, currentGen);\n // A writer writes at most the cap, except that a list may always shrink: a release over a row that holds more\n // (written by another build) writes that list less one, and so can never be stuck behind the cap.\n const most = Math.max(MAX_LEASES_PER_SEGMENT, prev.leases?.length ?? 0);\n if (leases !== undefined && leases.length > most) {\n throw new ValidationError(`leases: has ${leases.length} entries, cap ${most}`);\n }\n } else {\n leases = prev.leases;\n }\n return {\n namespace: prev.namespace,\n segment: prev.segment,\n currentGen,\n wrappedDeks,\n keyId: 'keyId' in patch ? patch.keyId : prev.keyId,\n status: patch.status ?? prev.status,\n retention: 'retention' in patch ? patch.retention : prev.retention,\n residency: 'residency' in patch ? patch.residency : prev.residency,\n summary,\n knownGens,\n leases,\n createdAt: prev.createdAt,\n updatedAt: now,\n token,\n pointerId: renewsPointer(patch) ? token : prev.pointerId,\n };\n}\n", "/**\n * The default entropy a shipped registry draws its tokens' random parts from: the platform's Web Crypto.\n *\n * Kept apart from the record helpers, which are pure, because this reads an ambient global. It is checked when a\n * row is written, not when a registry is built, so a process that only reads runs on a runtime without it.\n */\nimport { UnsupportedError } from '@/core/errors';\nimport type { Entropy } from '@/core/determinism';\n\ninterface WebCrypto {\n getRandomValues(array: Uint8Array): Uint8Array;\n}\n\n/** The platform's Web Crypto, or `undefined` when this runtime has none. */\nfunction webCrypto(): WebCrypto | undefined {\n const c = (globalThis as { crypto?: { getRandomValues?: unknown } }).crypto;\n return c !== undefined && typeof c.getRandomValues === 'function' ? (c as WebCrypto) : undefined;\n}\n\n/** Draws from the platform's Web Crypto; `UnsupportedError` on a runtime without it. */\nexport const webCryptoEntropy: Entropy = (length) => {\n const c = webCrypto();\n if (c === undefined) {\n throw new UnsupportedError(\n 'this runtime has no Web Crypto (crypto.getRandomValues), which a registry needs for the token of every ' +\n 'row it writes: run on a runtime that has it',\n );\n }\n return c.getRandomValues(new Uint8Array(length));\n};\n\n/** Whether `entropy` can draw here: an injected source always, the default only where Web Crypto exists. */\nexport function entropyIsAvailable(entropy: Entropy): boolean {\n return entropy !== webCryptoEntropy || webCrypto() !== undefined;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["shown", "shown"]
7
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ requirePositiveInteger
3
+ } from "./chunk-2B2QF4FU.js";
4
+
5
+ // src/core/concurrency.ts
6
+ async function mapWithConcurrency(items, limit, fn) {
7
+ requirePositiveInteger(limit, "concurrency limit");
8
+ const results = new Array(items.length);
9
+ let next = 0;
10
+ let failed = false;
11
+ let firstError;
12
+ const worker = async () => {
13
+ while (!failed) {
14
+ const i = next++;
15
+ if (i >= items.length) return;
16
+ try {
17
+ results[i] = await fn(items[i], i);
18
+ } catch (err) {
19
+ if (!failed) {
20
+ failed = true;
21
+ firstError = err;
22
+ }
23
+ return;
24
+ }
25
+ }
26
+ };
27
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
28
+ await Promise.all(workers);
29
+ if (failed) throw firstError;
30
+ return results;
31
+ }
32
+ function boundedAll(items, limit, fn) {
33
+ try {
34
+ requirePositiveInteger(limit, "concurrency limit");
35
+ } catch (err) {
36
+ return Promise.reject(err);
37
+ }
38
+ if (items.length > limit) return inWaves(items, limit, fn);
39
+ const calls = [];
40
+ for (const item of items) {
41
+ try {
42
+ calls.push(fn(item));
43
+ } catch (err) {
44
+ void Promise.allSettled(calls);
45
+ return Promise.reject(err);
46
+ }
47
+ }
48
+ return Promise.all(calls);
49
+ }
50
+ async function inWaves(items, limit, fn) {
51
+ const results = new Array(items.length);
52
+ let next = 0;
53
+ let failed = false;
54
+ const worker = async () => {
55
+ while (!failed && next < items.length) {
56
+ const i = next++;
57
+ try {
58
+ results[i] = await fn(items[i]);
59
+ } catch (err) {
60
+ failed = true;
61
+ throw err;
62
+ }
63
+ }
64
+ };
65
+ await Promise.all(Array.from({ length: limit }, () => worker()));
66
+ return results;
67
+ }
68
+
69
+ export {
70
+ mapWithConcurrency,
71
+ boundedAll
72
+ };
73
+ //# sourceMappingURL=chunk-BUDAVGBI.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/concurrency.ts"],
4
+ "sourcesContent": ["/**\n * Bounded-concurrency fan-out \u2014 the missing primitive for running async work over many items without either\n * a thundering herd (unbounded `Promise.all` \u2192 N simultaneous object-store calls) or full serial latency\n * (`for await`). Pure + I/O-free, so it lives in `core/`; callers inject the async work.\n */\n\nimport { requirePositiveInteger } from './check';\n\n/**\n * Run `fn` over `items` with at most `limit` promises in flight at once, returning results in **input order**\n * (regardless of completion order). If any `fn` rejects, the pool stops scheduling new work, lets the in-flight\n * tasks settle, and rejects with the **first** error \u2014 callers that must not abort on one failure (e.g. an\n * erasure ledger that records per-item faults) should catch inside `fn` so it never rejects.\n *\n * `items` is a materialized array on purpose: every caller here already enumerates its source (registry list,\n * S3 page, chunk map), so an array keeps the pool trivial and race-free (no shared-iterator hazard).\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n requirePositiveInteger(limit, 'concurrency limit');\n const results = new Array<R>(items.length);\n let next = 0;\n let failed = false;\n let firstError: unknown;\n const worker = async (): Promise<void> => {\n while (!failed) {\n const i = next++;\n if (i >= items.length) return;\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n if (!failed) {\n failed = true; // first failure wins; stop scheduling new items (in-flight ones still settle)\n firstError = err;\n }\n return;\n }\n }\n };\n const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());\n await Promise.all(workers);\n if (failed) throw firstError;\n return results;\n}\n\n/**\n * `Promise.all` over `items` with at most `limit` calls of `fn` in flight: the results in input order, or the first\n * failure, at once. Unlike {@link mapWithConcurrency} it does not wait for the calls still in flight before it rejects,\n * as `Promise.all` does not; it starts no call after a failure, and what the calls in flight answer is dropped. For reads\n * a caller abandons on the first fault, never for work that must finish or be accounted for once begun. When every item\n * fits under `limit` it is `Promise.all` itself, so a small fan-out costs and interleaves exactly as `Promise.all` does.\n */\nexport function boundedAll<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n try {\n requirePositiveInteger(limit, 'concurrency limit');\n } catch (err) {\n return Promise.reject(err);\n }\n if (items.length > limit) return inWaves(items, limit, fn);\n const calls: Promise<R>[] = [];\n for (const item of items) {\n try {\n calls.push(fn(item));\n } catch (err) {\n // The calls already made still run; settle them, so one that rejects later is not left unhandled.\n void Promise.allSettled(calls);\n return Promise.reject(err);\n }\n }\n return Promise.all(calls);\n}\n\n/** {@link boundedAll} over more items than `limit`: `limit` workers, each taking the next item as its last settles. */\nasync function inWaves<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n let failed = false;\n const worker = async (): Promise<void> => {\n while (!failed && next < items.length) {\n const i = next++;\n try {\n results[i] = await fn(items[i]!);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n };\n await Promise.all(Array.from({ length: limit }, () => worker()));\n return results;\n}\n"],
5
+ "mappings": ";;;;;AAiBA,eAAsB,mBACpB,OACA,OACA,IACc;AACd,yBAAuB,OAAO,mBAAmB;AACjD,QAAM,UAAU,IAAI,MAAS,MAAM,MAAM;AACzC,MAAI,OAAO;AACX,MAAI,SAAS;AACb,MAAI;AACJ,QAAM,SAAS,YAA2B;AACxC,WAAO,CAAC,QAAQ;AACd,YAAM,IAAI;AACV,UAAI,KAAK,MAAM,OAAQ;AACvB,UAAI;AACF,gBAAQ,CAAC,IAAI,MAAM,GAAG,MAAM,CAAC,GAAI,CAAC;AAAA,MACpC,SAAS,KAAK;AACZ,YAAI,CAAC,QAAQ;AACX,mBAAS;AACT,uBAAa;AAAA,QACf;AACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC;AACpF,QAAM,QAAQ,IAAI,OAAO;AACzB,MAAI,OAAQ,OAAM;AAClB,SAAO;AACT;AASO,SAAS,WACd,OACA,OACA,IACc;AACd,MAAI;AACF,2BAAuB,OAAO,mBAAmB;AAAA,EACnD,SAAS,KAAK;AACZ,WAAO,QAAQ,OAAO,GAAG;AAAA,EAC3B;AACA,MAAI,MAAM,SAAS,MAAO,QAAO,QAAQ,OAAO,OAAO,EAAE;AACzD,QAAM,QAAsB,CAAC;AAC7B,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,YAAM,KAAK,GAAG,IAAI,CAAC;AAAA,IACrB,SAAS,KAAK;AAEZ,WAAK,QAAQ,WAAW,KAAK;AAC7B,aAAO,QAAQ,OAAO,GAAG;AAAA,IAC3B;AAAA,EACF;AACA,SAAO,QAAQ,IAAI,KAAK;AAC1B;AAGA,eAAe,QACb,OACA,OACA,IACc;AACd,QAAM,UAAU,IAAI,MAAS,MAAM,MAAM;AACzC,MAAI,OAAO;AACX,MAAI,SAAS;AACb,QAAM,SAAS,YAA2B;AACxC,WAAO,CAAC,UAAU,OAAO,MAAM,QAAQ;AACrC,YAAM,IAAI;AACV,UAAI;AACF,gBAAQ,CAAC,IAAI,MAAM,GAAG,MAAM,CAAC,CAAE;AAAA,MACjC,SAAS,KAAK;AACZ,iBAAS;AACT,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,MAAM,OAAO,CAAC,CAAC;AAC/D,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ ValidationError,
3
+ encodeNameForKey
4
+ } from "./chunk-2B2QF4FU.js";
5
+
6
+ // src/core/keys.ts
7
+ var FIELD = " ";
8
+ var NO_NAMESPACE = "/";
9
+ function segmentKey(ref) {
10
+ const ns = ref.namespace === void 0 ? NO_NAMESPACE : encodeNameForKey(ref.namespace);
11
+ return `${ns}${FIELD}${encodeNameForKey(ref.segment)}`;
12
+ }
13
+ function segmentPrefix(ref) {
14
+ return `${segmentKey(ref)}${FIELD}`;
15
+ }
16
+ var KEPT_SEGMENT_KEYS = 1024;
17
+ var KeptSegmentKeys = class {
18
+ constructor(encode, max = KEPT_SEGMENT_KEYS) {
19
+ this.encode = encode;
20
+ this.max = max;
21
+ }
22
+ encode;
23
+ max;
24
+ byNamespace = /* @__PURE__ */ new Map();
25
+ kept = 0;
26
+ /** The segment's encoded key. */
27
+ of(ref) {
28
+ let names = this.byNamespace.get(ref.namespace);
29
+ const found = names?.get(ref.segment);
30
+ if (found !== void 0) return found;
31
+ const encoded = this.encode(ref);
32
+ if (this.kept >= this.max) {
33
+ this.byNamespace.clear();
34
+ this.kept = 0;
35
+ names = void 0;
36
+ }
37
+ if (names === void 0) {
38
+ names = /* @__PURE__ */ new Map();
39
+ this.byNamespace.set(ref.namespace, names);
40
+ }
41
+ names.set(ref.segment, encoded);
42
+ this.kept += 1;
43
+ return encoded;
44
+ }
45
+ /** How many encoded keys are kept. */
46
+ get size() {
47
+ return this.kept;
48
+ }
49
+ };
50
+ function chunkKeyUnder(prefix, chunkKey, version) {
51
+ return version === void 0 ? `${prefix}${chunkKey}` : `${prefix}${chunkKey}${FIELD}${version}`;
52
+ }
53
+ function shardOf(key, totalShards) {
54
+ let h = 2166136261;
55
+ for (let i = 0; i < key.length; i++) {
56
+ h ^= key.charCodeAt(i);
57
+ h = Math.imul(h, 16777619);
58
+ }
59
+ return (h >>> 0) % totalShards;
60
+ }
61
+
62
+ // src/core/blob.ts
63
+ function checkTailLength(maxBytes) {
64
+ if (!Number.isSafeInteger(maxBytes)) {
65
+ throw new ValidationError(`invalid tail length ${String(maxBytes)}`);
66
+ }
67
+ }
68
+ var BufferSink = class {
69
+ chunks = [];
70
+ length = 0;
71
+ write(bytes) {
72
+ this.chunks.push(new Uint8Array(bytes));
73
+ this.length += bytes.length;
74
+ return Promise.resolve();
75
+ }
76
+ bytes() {
77
+ const out = new Uint8Array(this.length);
78
+ let offset = 0;
79
+ for (const chunk of this.chunks) {
80
+ out.set(chunk, offset);
81
+ offset += chunk.length;
82
+ }
83
+ return out;
84
+ }
85
+ };
86
+ var BufferReader = class {
87
+ constructor(buffer) {
88
+ this.buffer = buffer;
89
+ }
90
+ buffer;
91
+ getRange(offset, length) {
92
+ if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0 || offset + length > this.buffer.length) {
93
+ throw new ValidationError(
94
+ `range [${offset}, ${offset + length}) out of bounds for ${this.buffer.length}-byte blob`
95
+ );
96
+ }
97
+ return Promise.resolve(this.buffer.subarray(offset, offset + length));
98
+ }
99
+ getTail(maxBytes) {
100
+ try {
101
+ checkTailLength(maxBytes);
102
+ } catch (err) {
103
+ return Promise.reject(err);
104
+ }
105
+ const size = this.buffer.length;
106
+ const take = Math.min(Math.max(maxBytes, 0), size);
107
+ return Promise.resolve({ bytes: this.buffer.subarray(size - take), size });
108
+ }
109
+ };
110
+
111
+ export {
112
+ segmentKey,
113
+ segmentPrefix,
114
+ KeptSegmentKeys,
115
+ chunkKeyUnder,
116
+ shardOf,
117
+ checkTailLength,
118
+ BufferSink,
119
+ BufferReader
120
+ };
121
+ //# sourceMappingURL=chunk-WPER7R2R.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/core/keys.ts", "../src/core/blob.ts"],
4
+ "sourcesContent": ["/**\n * Canonical key encoding for a segment / chunk \u2014 one source of truth, used by both the storage\n * drivers and the cache (DRY). The encoding is collision-proof and injection-proof.\n *\n * **The name parts are encoded, and that is what makes the delimiters safe.** A name is any non-empty string,\n * so it may contain a space or a `/`, and with the parts left literal two distinct segments could produce one\n * key:\n *\n * ```\n * segmentKey({ segment: 's' }) \u2192 \"/ s\"\n * segmentKey({ namespace: '/', segment: 's' }) \u2192 \"/ s\" \u2190 the same key\n * segmentKey({ namespace: 'a', segment: 'b c' }) \u2192 \"a b c\"\n * segmentKey({ namespace: 'a b', segment: 'c' }) \u2192 \"a b c\" \u2190 the same key\n * ```\n *\n * These keys reach the reader LRU and the decoded-chunk cache, so a collision is one segment serving\n * another's data on the read path, on every backend \u2014 invariant 3, and a tenant-isolation break.\n *\n * `encodeNameForKey` escapes both delimiters (`/` \u2192 `%2F`, space \u2192 `%20`) along with control characters, so\n * neither can be injected across a field \u2014 a property of the encoding, not of what a caller chooses to name\n * things. The absent-namespace sentinel keeps the same ours-vs-theirs asymmetry the\n * storage keys use: it is emitted literally while a caller's namespace is encoded, so a namespace actually\n * named `/` encodes to `%2F` and cannot impersonate it.\n *\n * These keys are in-memory and driver-internal; nothing on disk is named by them.\n */\nimport { encodeNameForKey } from './name-codec';\nimport type { ChunkRef, SegmentRef } from './ports';\n\nconst FIELD = ' ';\nconst NO_NAMESPACE = '/';\n\nexport function segmentKey(ref: SegmentRef): string {\n const ns = ref.namespace === undefined ? NO_NAMESPACE : encodeNameForKey(ref.namespace);\n return `${ns}${FIELD}${encodeNameForKey(ref.segment)}`;\n}\n\nexport function segmentPrefix(ref: SegmentRef): string {\n return `${segmentKey(ref)}${FIELD}`;\n}\n\n/**\n * How many segments' encoded keys a {@link KeptSegmentKeys} holds before it empties: the reader cache's default\n * segment count, a working set a long-lived reader keeps open. With 30-character names that is about 150 KB.\n */\nexport const KEPT_SEGMENT_KEYS = 1024;\n\n/**\n * Segments' encoded keys ({@link segmentKey}, or {@link segmentPrefix}: whichever `encode` gives), kept so a read that\n * looks up chunk after chunk compares names rather than encoding them again, which costs about a third of a warm\n * `has()`. Held by namespace and then by segment, so segments of one name in several namespaces sit side by side, and\n * no number of segments read in turn makes every lookup encode again. Bounded: at `max` entries it empties and fills\n * again. An encoding is the names' and nothing else, so a kept one is the one an encoding would give now.\n */\nexport class KeptSegmentKeys {\n private readonly byNamespace = new Map<string | undefined, Map<string, string>>();\n private kept = 0;\n\n constructor(\n private readonly encode: (ref: SegmentRef) => string,\n private readonly max: number = KEPT_SEGMENT_KEYS,\n ) {}\n\n /** The segment's encoded key. */\n of(ref: SegmentRef): string {\n let names = this.byNamespace.get(ref.namespace);\n const found = names?.get(ref.segment);\n if (found !== undefined) return found;\n const encoded = this.encode(ref);\n if (this.kept >= this.max) {\n this.byNamespace.clear();\n this.kept = 0;\n names = undefined;\n }\n if (names === undefined) {\n names = new Map();\n this.byNamespace.set(ref.namespace, names);\n }\n names.set(ref.segment, encoded);\n this.kept += 1;\n return encoded;\n }\n\n /** How many encoded keys are kept. */\n get size(): number {\n return this.kept;\n }\n}\n\nexport function chunkRefKey(ref: ChunkRef): string {\n return chunkKeyUnder(segmentPrefix(ref), ref.chunkKey);\n}\n\n/**\n * {@link chunkRefKey}, or {@link chunkGenKey} with a `version`, from the segment's {@link segmentPrefix} already\n * encoded: for a caller that looks up many chunks of a few segments and keeps their prefixes.\n */\nexport function chunkKeyUnder(prefix: string, chunkKey: number, version?: string | number): string {\n return version === undefined ? `${prefix}${chunkKey}` : `${prefix}${chunkKey}${FIELD}${version}`;\n}\n\n/**\n * Cache key for a chunk scoped to a specific **version** of the segment \u2014 {@link chunkRefKey} plus that\n * version (space-delimited, injection-proof exactly as above \u2014 the name parts are already encoded). A publish bumps the version, so it naturally\n * misses the cache instead of serving a stale superseded chunk, and the superseded entries age out under the\n * LRU ceiling (no active purge needed).\n *\n * `version` is whatever the Storage source reports as identifying the bytes a read will see \u2014 its\n * `currentVersion` where it has one, otherwise the bare generation number. **A generation number alone is not\n * an identity**: a load's generation number restarts at 0 once a registry row is purged and the bucket emptied, so a\n * retired-and-re-created name serves different data at the same `currentGen`, and a key built on the number\n * hands the new incarnation the old one's decoded chunks.\n */\nexport function chunkGenKey(ref: ChunkRef, version: string | number): string {\n return chunkKeyUnder(segmentPrefix(ref), ref.chunkKey, version);\n}\n\n/**\n * Deterministic FNV-1a shard assignment for a segment key \u2014 dependency-free, stable across workers and\n * restarts. One definition, so every sharded fleet operation (today the retention sweep) agrees on it exactly:\n * two definitions that disagreed would make the union across workers neither disjoint nor complete.\n */\nexport function shardOf(key: string, totalShards: number): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < key.length; i++) {\n h ^= key.charCodeAt(i);\n h = Math.imul(h, 0x0100_0193);\n }\n return (h >>> 0) % totalShards;\n}\n", "/**\n * The byte-IO seam the `.crbm` codec reads/writes through.\n *\n * Deliberately tiny and driver-independent: a `BlobSink` is a streaming append target (the writer needs\n * no random access), a `BlobReader` supports the speculative tail read + bounded range reads. An\n * `IStorageDriver` adapts a `GenKey` to these; the in-memory impls below let the format be\n * golden-tested and let the in-memory storage tier hold a real `.crbm` with no filesystem.\n */\nimport { ValidationError } from './errors';\n\n/**\n * Streaming append target for a single object being written. A caller may reuse the buffer it passed to `write` once\n * the write resolves, so a sink that holds bytes past that copies them.\n */\nexport interface BlobSink {\n /**\n * Appends `bytes` to the object. Once it resolves the caller may reuse the buffer, so a sink that keeps the bytes\n * copies them.\n */\n write(bytes: Uint8Array): Promise<void>;\n}\n\n/**\n * Random/tail read access to a single finished object. The total size comes back from `getTail` (the\n * codec's entry point), so there's no separate `size()` \u2014 keeping the seam minimal for driver authors.\n */\nexport interface BlobReader {\n /** Exactly `length` bytes starting at `offset`. Out-of-range is a `ValidationError`, never a short read. */\n getRange(offset: number, length: number): Promise<Uint8Array>;\n /**\n * The last `min(maxBytes, size)` bytes plus the total size \u2014 the one-GET footer+index path. `maxBytes` is a safe\n * integer, zero or less for an empty tail; any other length is refused with `ValidationError`.\n */\n getTail(maxBytes: number): Promise<{ bytes: Uint8Array; size: number }>;\n}\n\n/**\n * Check the length a tail read asks for, the one rule every tail read holds to (each storage driver's `getTail` and\n * {@link BufferReader.getTail}): a safe integer. Zero or less answers an empty tail with the size, and a length past\n * the object's size answers the whole object. Anything else is refused with `ValidationError` before anything is read:\n * `NaN`, a fraction (a negative one too), an infinity of either sign, and a whole number past the safe range (2^53 or\n * more, or -2^53 or less), so no reader takes one for some other length.\n */\nexport function checkTailLength(maxBytes: number): void {\n if (!Number.isSafeInteger(maxBytes)) {\n throw new ValidationError(`invalid tail length ${String(maxBytes)}`);\n }\n}\n\n/** In-memory `BlobSink` that accumulates written bytes; `bytes()` returns the concatenation. */\nexport class BufferSink implements BlobSink {\n private readonly chunks: Uint8Array[] = [];\n private length = 0;\n\n write(bytes: Uint8Array): Promise<void> {\n // Copy into a plain Uint8Array: callers may reuse their buffer after write() resolves, and on a Node Buffer\n // `slice()` is a view of it, not a copy.\n this.chunks.push(new Uint8Array(bytes));\n this.length += bytes.length;\n return Promise.resolve();\n }\n\n bytes(): Uint8Array {\n const out = new Uint8Array(this.length);\n let offset = 0;\n for (const chunk of this.chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n }\n}\n\n/** In-memory `BlobReader` over a fixed byte buffer. */\nexport class BufferReader implements BlobReader {\n constructor(private readonly buffer: Uint8Array) {}\n\n getRange(offset: number, length: number): Promise<Uint8Array> {\n if (\n !Number.isInteger(offset) ||\n !Number.isInteger(length) ||\n offset < 0 ||\n length < 0 ||\n offset + length > this.buffer.length\n ) {\n throw new ValidationError(\n `range [${offset}, ${offset + length}) out of bounds for ${this.buffer.length}-byte blob`,\n );\n }\n return Promise.resolve(this.buffer.subarray(offset, offset + length));\n }\n\n getTail(maxBytes: number): Promise<{ bytes: Uint8Array; size: number }> {\n try {\n checkTailLength(maxBytes);\n } catch (err) {\n return Promise.reject(err);\n }\n const size = this.buffer.length;\n const take = Math.min(Math.max(maxBytes, 0), size);\n return Promise.resolve({ bytes: this.buffer.subarray(size - take), size });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -73,10 +73,13 @@ export type AuditEvent = {
73
73
  * segment's registry row changed while the load was writing). `cardinality` is what the refused generation
74
74
  * would have contained, and `0` for a load that lost its generation number and wrote nothing.
75
75
  *
76
- * `unanswered` is present, and `true`, on a `'superseded'` refusal whose registry write ended without an answer
77
- * before the row was found to have moved on: that write may have landed, so the generation may have been the
78
- * segment's current one for a while before another writer replaced it. The refusal says only that it is not
79
- * current now. Absent when every write the load made was answered.
76
+ * `unanswered` is present, and `true`, on a `'superseded'` refusal after a registry write whose answer did not
77
+ * say whether it landed: one that ended without an answer, or a conflict its registry marked as one that may
78
+ * follow the write's own landing (Azure Blob marks every conflict of a row write, since its client may have sent
79
+ * the write twice). That write may have landed, so the generation may have been the segment's current one for a
80
+ * while before another write replaced it, or, where a write renewed the row's `pointerId` over it, may be current
81
+ * still. Absent when every write was answered and every conflict was one its registry vouches the write did not
82
+ * land.
80
83
  */
81
84
  readonly kind: 'segment.load-refused';
82
85
  readonly namespace?: string;
@@ -115,11 +118,12 @@ export type AuditEvent = {
115
118
  * not hold, or an encrypted one under a cleartext row (a first load's that lost the race, or crashed). The one
116
119
  * emitter is `eraseIdFromSegment`, once a listing of the bucket shows nothing left holding the id, so the record
117
120
  * exists only for a finished erasure. A call that ends otherwise throws or reports `erased: false` with another
118
- * reason and emits nothing, even when it had deleted a holder; its re-run emits when it finishes, and reports
119
- * `'not-member'` with no event when another collection deleted the last holder meanwhile, as its ledger entry
120
- * does. One finished erasure emits it and then throws `NotFoundError`: one that found, after its deletes, the
121
- * segment's pointer on a generation it had deleted (a rollback moved it there meanwhile), so the deletion is on
122
- * the record although the call cannot answer.
121
+ * reason and emits nothing, even when it had deleted a holder; its re-run emits when it finishes, and, when
122
+ * another collection deleted the last holder meanwhile, emits nothing and answers `'not-member'`, which
123
+ * `eraseSubject`'s ledger shows by listing nothing for the segment. One finished erasure emits it and then
124
+ * throws `NotFoundError`: one that found, after its deletes, the segment's pointer on a generation it had
125
+ * deleted (a rollback moved it there meanwhile), so the deletion is on the record although the call cannot
126
+ * answer.
123
127
  *
124
128
  * `fromGeneration` is the newest generation the call found holding the id, and is absent when it found the id in
125
129
  * none and deleted only objects it could not search: then the ledger lists no entry for the segment, and this
@@ -137,24 +141,52 @@ export type AuditEvent = {
137
141
  readonly collected: readonly number[];
138
142
  } | {
139
143
  /**
140
- * A segment was **crypto-shredded** — its wrapped DEK(s) are gone, so its at-rest Storage bytes are now
141
- * permanently unreadable. Emitted only for a genuine key shred, never for a cleartext tombstone (which
142
- * leaves the Storage bytes readable) or an idempotent re-run.
144
+ * A segment was **crypto-shredded**: its wrapped DEK(s) are gone from the segment's **current** registry row,
145
+ * so nothing that opens the segment through that row can decrypt its Storage bytes, in the bucket or in any
146
+ * backup, replica or WORM copy of its objects. It destroys no KEK: a noncurrent version, a backup or a
147
+ * point-in-time copy of the row still holds the wrapped DEK(s), and a registry restore to a point before the
148
+ * shred makes the segment readable again wherever its objects survive. The destruction is complete once no
149
+ * retained copy of the row holds them, or once every KEK that wrapped them is destroyed. A process that had
150
+ * already opened the segment keeps its unwrapped key in memory until told (`store.invalidate(ref)`). Emitted
151
+ * only for a genuine key shred, never for a cleartext tombstone (which leaves the Storage bytes readable) or
152
+ * an idempotent re-run.
143
153
  */
144
154
  readonly kind: 'segment.erase';
145
155
  readonly namespace?: string;
146
156
  readonly segment: string;
147
157
  /** The incarnation of the row the shred turned into a tombstone. */
148
158
  readonly incarnation?: string;
159
+ } | {
160
+ /**
161
+ * A segment was tombstoned **without a key shred**: `destroySegment` or `eraseNamespace` with `allowCleartext`
162
+ * turned the row of a segment that holds no wrapped key into a `destroyed` tombstone. No read resolves the
163
+ * segment any more, but nothing was deleted and no key was destroyed, so its objects stay readable in the
164
+ * bucket, in any backup and through any copy of the row from before the tombstone.
165
+ *
166
+ * Deliberately not {@link AuditEvent} `segment.erase`, which attests a key shred, nor `segment.dispose`, which
167
+ * attests that a drop swept the storage: this one is the record that the row was tombstoned, which is what a
168
+ * registry restore undoes and what the restore procedure looks for. `objectsReadable` is always `true`; it
169
+ * is a field so a consumer that forwards events cannot read this one as an erasure receipt. Emitted once per
170
+ * tombstone this call wrote, not for the idempotent re-run, and not for a segment that was encrypted
171
+ * (`segment.erase`), or dropped (`segment.dispose`).
172
+ */
173
+ readonly kind: 'segment.tombstone';
174
+ readonly namespace?: string;
175
+ readonly segment: string;
176
+ /** The incarnation of the row the call turned into a tombstone. */
177
+ readonly incarnation?: string;
178
+ /** Always `true`: the segment's objects were not deleted and no key was destroyed. */
179
+ readonly objectsReadable: true;
149
180
  } | {
150
181
  /**
151
182
  * A segment was **disposed of** — tombstoned and its storage reclaimed (its Storage generations deleted) by
152
183
  * `dropSegment`, *without* a key shred.
153
184
  *
154
185
  * Deliberately a separate kind from {@link AuditEvent} `segment.erase`, and the distinction is the point.
155
- * `segment.erase` attests that bytes are unreadable **everywhere, backups included** — the only claim that
156
- * survives WORM. Deleting an object is strictly weaker: a noncurrent version, a cross-region replica or a
157
- * PITR snapshot can still hold the cleartext. Emitting one kind for both would make a compliance dashboard
186
+ * `segment.erase` attests that the current row no longer holds the key, which is the one erasure claim that
187
+ * reaches WORM copies of the objects, once no retained copy of the row holds it. Deleting an object is
188
+ * strictly weaker: a noncurrent version, a cross-region replica or a PITR snapshot can still hold the
189
+ * cleartext. Emitting one kind for both would make a compliance dashboard
158
190
  * over-attest, so a cleartext drop gets this instead.
159
191
  *
160
192
  * An **encrypted** segment dropped via `dropSegment` emits **both** — `segment.erase` for the key shred and
@@ -178,6 +210,10 @@ export type AuditEvent = {
178
210
  };
179
211
  /** Sink for {@link AuditEvent}s. Injected via the lifecycle options; omit it and nothing is recorded. */
180
212
  export interface IAuditSink {
213
+ /**
214
+ * Receives one audit event, called synchronously by the operation that caused it. What it throws, or rejects with, is
215
+ * swallowed and never breaks that operation.
216
+ */
181
217
  onEvent(event: AuditEvent): void;
182
218
  }
183
219
  /** The default sink: records nothing. */
@@ -200,8 +236,10 @@ export declare function ignoreRejection(returned: unknown): void;
200
236
  /** A ready-made sink that records events into an in-memory list — handy for tests + simple audit trails. */
201
237
  export declare class RecordingAuditSink implements IAuditSink {
202
238
  private readonly recorded;
239
+ /** Records the event, in emission order. */
203
240
  onEvent(event: AuditEvent): void;
204
241
  /** An independent copy of the recorded events, in emission order. */
205
242
  snapshot(): AuditEvent[];
243
+ /** Forgets every recorded event. */
206
244
  reset(): void;
207
245
  }
@@ -13,6 +13,8 @@ interface BitRoute {
13
13
  }
14
14
  /** Was `err` thrown by {@link splitId} for an id outside the u32 range? */
15
15
  export declare function isBadIdError(err: unknown): err is ValidationError;
16
+ /** The refusal {@link splitId} throws for a value that is not a u32 id, for a caller that checks the range itself. */
17
+ export declare function refusalOfId(id: unknown): ValidationError;
16
18
  /** Split a u32 id into `{ chunkKey, remainder }`. Throws `ValidationError` for non-u32 input. */
17
19
  export declare function splitId(id: number): BitRoute;
18
20
  /**
@@ -3,6 +3,10 @@
3
3
  * the write resolves, so a sink that holds bytes past that copies them.
4
4
  */
5
5
  export interface BlobSink {
6
+ /**
7
+ * Appends `bytes` to the object. Once it resolves the caller may reuse the buffer, so a sink that keeps the bytes
8
+ * copies them.
9
+ */
6
10
  write(bytes: Uint8Array): Promise<void>;
7
11
  }
8
12
  /**
@@ -15,6 +15,11 @@
15
15
  * search generation by generation is also charged one unit for each generation it opens beyond the current one,
16
16
  * since that is O(generations) work the segment count alone does not show.
17
17
  */
18
+ /**
19
+ * The cap on one operation's fan-out: its `maxRequests` units, counted in storage chunk fetches for a read and in
20
+ * segments scanned for an admin scan. `eraseSubject` also charges one unit for each generation beyond the current one
21
+ * that it opens, in a segment it has to search generation by generation.
22
+ */
18
23
  export interface Budget {
19
24
  /** Max units of fan-out (Storage chunk fetches, or segments scanned) one op may issue before it is refused. */
20
25
  readonly maxRequests: number;
@@ -30,13 +35,13 @@ export type BudgetOption = Partial<Budget> | false;
30
35
  /**
31
36
  * Resolve a budget option against a fallback to a concrete `Budget`, or `null` when disabled. Validates: anything but
32
37
  * `false`, an absent value or an object of `{ maxRequests }` is refused, since a cap of the wrong shape (`5`,
33
- * `{ maxRequest: 5 }`) would otherwise read as no override and leave the fallback in force. `null` and a key whose value
34
- * is `undefined` read as absent.
38
+ * `{ maxRequest: 5 }`) would otherwise read as no override and leave the fallback in force. A key whose value is `undefined` reads
39
+ * as absent; a `null` budget and a `maxRequests` of `null` are refused.
35
40
  */
36
41
  export declare function resolveBudget(opt: BudgetOption | undefined, fallback: Budget): Budget | null;
37
42
  /**
38
43
  * Resolve a **per-op** budget override against the store's own resolved budget (never the raw global default):
39
- * `undefined` or `null` inherits the store budget as-is (including a disabled `null`); a partial `{}` / omitted
44
+ * `undefined` inherits the store budget as-is (including a disabled `null`); a partial `{}` / omitted
40
45
  * `maxRequests` inherits the store's *tightening* (so a per-op passthrough of an absent config value can't
41
46
  * silently lift a tight tenant ceiling back to the generous default); `{ maxRequests }` replaces it; `false`
42
47
  * disables. Falls back to {@link DEFAULT_BUDGET} only when the store itself has no budget (disabled).