@cloudbitmaps/core 0.8.2 → 0.10.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 (132) hide show
  1. package/NOTICE +4 -4
  2. package/README.md +28 -6
  3. package/dist/chunk-BUPDGZRR.js +461 -0
  4. package/dist/chunk-BUPDGZRR.js.map +7 -0
  5. package/dist/core/audit.d.ts +127 -0
  6. package/dist/core/bit-route.d.ts +21 -0
  7. package/dist/core/blob.d.ts +34 -0
  8. package/dist/core/budget.d.ts +66 -0
  9. package/dist/core/codec.d.ts +114 -0
  10. package/dist/core/concurrency.d.ts +15 -0
  11. package/dist/core/consistency.d.ts +73 -0
  12. package/dist/core/cooperative.d.ts +78 -0
  13. package/dist/core/cost.d.ts +102 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +116 -0
  16. package/dist/core/crbm/reader.d.ts +69 -0
  17. package/dist/core/crbm/varint.d.ts +13 -0
  18. package/dist/core/crbm/writer.d.ts +39 -0
  19. package/dist/core/crbm-storage-source.d.ts +432 -0
  20. package/dist/core/crypto.d.ts +80 -0
  21. package/dist/core/determinism.d.ts +42 -0
  22. package/dist/core/discover.d.ts +75 -0
  23. package/dist/core/due-index.d.ts +55 -0
  24. package/dist/core/engine.d.ts +204 -0
  25. package/dist/core/erase-id.d.ts +141 -0
  26. package/dist/core/erasure.d.ts +179 -0
  27. package/dist/core/errors.d.ts +120 -0
  28. package/dist/core/generation-gc.d.ts +52 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +102 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +132 -0
  33. package/dist/core/name-codec.d.ts +79 -0
  34. package/dist/core/pinned-storage-source.d.ts +69 -0
  35. package/dist/core/ports.d.ts +340 -0
  36. package/dist/core/registry-scan.d.ts +38 -0
  37. package/dist/core/retention-sweep.d.ts +190 -0
  38. package/dist/core/retention.d.ts +67 -0
  39. package/dist/core/retry.d.ts +65 -0
  40. package/dist/core/rollback.d.ts +71 -0
  41. package/dist/core/validate.d.ts +20 -0
  42. package/dist/driver-kit.d.ts +10 -0
  43. package/dist/driver-kit.js +314 -0
  44. package/dist/driver-kit.js.map +7 -0
  45. package/dist/drivers/_shared/aws-errors.d.ts +18 -0
  46. package/dist/drivers/_shared/keys.d.ts +27 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +84 -0
  49. package/dist/drivers/_shared/registry.d.ts +62 -0
  50. package/dist/drivers/backends.d.ts +49 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +20 -0
  53. package/dist/drivers/localfs/paths.d.ts +33 -0
  54. package/dist/drivers/localfs/registry.d.ts +28 -0
  55. package/dist/drivers/localfs/storage.d.ts +19 -0
  56. package/dist/drivers/memory.d.ts +57 -0
  57. package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
  58. package/dist/export/index.d.ts +92 -0
  59. package/dist/index.d.ts +85 -2534
  60. package/dist/index.js +3076 -2978
  61. package/dist/index.js.map +7 -1
  62. package/dist/testing/fuzz-core.d.ts +14 -0
  63. package/package.json +19 -115
  64. package/dist/azure/index.cjs +0 -368
  65. package/dist/azure/index.cjs.map +0 -1
  66. package/dist/azure/index.d.cts +0 -48
  67. package/dist/azure/index.d.ts +0 -48
  68. package/dist/azure/index.js +0 -304
  69. package/dist/azure/index.js.map +0 -1
  70. package/dist/cassandra/index.cjs +0 -265
  71. package/dist/cassandra/index.cjs.map +0 -1
  72. package/dist/cassandra/index.d.cts +0 -52
  73. package/dist/cassandra/index.d.ts +0 -52
  74. package/dist/cassandra/index.js +0 -204
  75. package/dist/cassandra/index.js.map +0 -1
  76. package/dist/chunk-7LMLYSVJ.js +0 -43
  77. package/dist/chunk-7LMLYSVJ.js.map +0 -1
  78. package/dist/chunk-L54FL5DW.js +0 -203
  79. package/dist/chunk-L54FL5DW.js.map +0 -1
  80. package/dist/chunk-NUIDEEFZ.js +0 -91
  81. package/dist/chunk-NUIDEEFZ.js.map +0 -1
  82. package/dist/chunk-SNJVZ227.js +0 -35
  83. package/dist/chunk-SNJVZ227.js.map +0 -1
  84. package/dist/chunk-YEHMG6W2.js +0 -6
  85. package/dist/chunk-YEHMG6W2.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -740
  87. package/dist/dynamodb/index.cjs.map +0 -1
  88. package/dist/dynamodb/index.d.cts +0 -106
  89. package/dist/dynamodb/index.d.ts +0 -106
  90. package/dist/dynamodb/index.js +0 -469
  91. package/dist/dynamodb/index.js.map +0 -1
  92. package/dist/gcs/index.cjs +0 -343
  93. package/dist/gcs/index.cjs.map +0 -1
  94. package/dist/gcs/index.d.cts +0 -46
  95. package/dist/gcs/index.d.ts +0 -46
  96. package/dist/gcs/index.js +0 -279
  97. package/dist/gcs/index.js.map +0 -1
  98. package/dist/index.cjs +0 -4608
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2535
  101. package/dist/mongodb/index.cjs +0 -260
  102. package/dist/mongodb/index.cjs.map +0 -1
  103. package/dist/mongodb/index.d.cts +0 -45
  104. package/dist/mongodb/index.d.ts +0 -45
  105. package/dist/mongodb/index.js +0 -199
  106. package/dist/mongodb/index.js.map +0 -1
  107. package/dist/mysql/index.cjs +0 -281
  108. package/dist/mysql/index.cjs.map +0 -1
  109. package/dist/mysql/index.d.cts +0 -56
  110. package/dist/mysql/index.d.ts +0 -56
  111. package/dist/mysql/index.js +0 -211
  112. package/dist/mysql/index.js.map +0 -1
  113. package/dist/ports-5BQTGKze.d.cts +0 -356
  114. package/dist/ports-5BQTGKze.d.ts +0 -356
  115. package/dist/postgres/index.cjs +0 -273
  116. package/dist/postgres/index.cjs.map +0 -1
  117. package/dist/postgres/index.d.cts +0 -54
  118. package/dist/postgres/index.d.ts +0 -54
  119. package/dist/postgres/index.js +0 -203
  120. package/dist/postgres/index.js.map +0 -1
  121. package/dist/redis/index.cjs +0 -257
  122. package/dist/redis/index.cjs.map +0 -1
  123. package/dist/redis/index.d.cts +0 -42
  124. package/dist/redis/index.d.ts +0 -42
  125. package/dist/redis/index.js +0 -197
  126. package/dist/redis/index.js.map +0 -1
  127. package/dist/s3/index.cjs +0 -891
  128. package/dist/s3/index.cjs.map +0 -1
  129. package/dist/s3/index.d.cts +0 -112
  130. package/dist/s3/index.d.ts +0 -112
  131. package/dist/s3/index.js +0 -555
  132. package/dist/s3/index.js.map +0 -1
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- CloudRoaring
1
+ @cloudbitmaps/core
2
2
  Copyright 2026 Sharvil Kadam
3
3
 
4
4
  This product includes software developed as an open-source project.
@@ -7,6 +7,6 @@ file. You may obtain a copy of the License at:
7
7
 
8
8
  http://www.apache.org/licenses/LICENSE-2.0
9
9
 
10
- CloudRoaring builds on the Roaring Bitmap data structure and is designed to use
11
- the CRoaring engine (via the `roaring` Node.js binding) for in-memory bit math.
12
- Those projects are independent works under their own licenses.
10
+ @cloudbitmaps/core is the codec-agnostic engine of CloudBitmaps. It contains no bitmap
11
+ codec and no cloud SDK, and has zero runtime dependencies; a flavor package supplies the
12
+ codec through the CodecInterface seam.
package/README.md CHANGED
@@ -1,20 +1,42 @@
1
1
  # @cloudbitmaps/core
2
2
 
3
+ > **ESM-only, Node ≥ 22.12.** This package ships as ES modules; there is no CommonJS bundle.
4
+ > `require()` works on Node 22.12+ through `require(esm)`, but a runner with its own CommonJS loader
5
+ > (notably Jest in its default configuration) does not get that and needs `import` instead. On TypeScript,
6
+ > a CommonJS project needs `"module": "nodenext"` or `"node20"`. See the
7
+ > [repository README](https://github.com/cloudbitmaps/cloudbitmaps#install--entry-points) for the details.
8
+
9
+
3
10
  The **codec-agnostic cloud engine** behind the [CloudBitmaps](https://github.com/cloudbitmaps/cloudbitmaps) family:
4
- tiered storage (HOT RAM → WARM key/value deltas → COLD immutable `.crbm` objects), serverless chunk-skipping
5
- intersection, the segment registry, crash-safe 2-phase-commit compaction, encryption-at-rest + crypto-shred, and
6
- **every storage driver** (S3 · GCS · Azure Blob · DynamoDB · PostgreSQL · Redis · MongoDB · Cassandra/ScyllaDB ·
7
- MySQL/MariaDB, each on its own subpath with the backend SDK as an optional peer dependency).
11
+ a bounded RAM cache over immutable `.crbm` objects in STORAGE, serverless chunk-skipping intersection, the segment
12
+ registry, the write-once load-and-publish write path, generation GC, encryption-at-rest + crypto-shred, subject
13
+ erasure by generation rewrite, segment lifecycle (disposal, and a per-segment retention policy with the sweep
14
+ that enforces it), and the **in-memory and local-filesystem drivers**. It contains **no cloud SDK at all**: the
15
+ cloud drivers are their own packages (`@cloudbitmaps/s3` · `/gcs` · `/azure-blob`), each hosting both the
16
+ generations and the registry, and each built against `@cloudbitmaps/core/driver-kit` — the declared contract a
17
+ driver package depends on, which a third-party driver can use too.
8
18
 
9
19
  ## You probably want a flavor, not this package
10
20
 
11
21
  This package holds no bitmap codec — that lives in a *flavor* package which depends on this one and supplies it
12
- through the `CodecInterface` seam. Install the flavor; `@cloudbitmaps/core` arrives **transitively**:
22
+ through the `CodecInterface` seam, and it holds no cloud driver either — those are packages of their own.
23
+ Install a codec and a storage; `@cloudbitmaps/core` is a dependency of both and never named by you:
13
24
 
14
25
  ```bash
15
- npm i @cloudbitmaps/roaring # the roaring flavor (flagship)
26
+ pnpm add @cloudbitmaps/roaring @cloudbitmaps/s3 # the roaring flavor (flagship), and the storage you have
27
+ # npm i @cloudbitmaps/roaring @cloudbitmaps/s3 # the same, with npm
16
28
  ```
17
29
 
30
+ > **On pnpm 10+, allow the one build script.** pnpm 10 skips dependency build scripts by default, so
31
+ > the `roaring` native addon never downloads and the package throws at `import` — while the install
32
+ > itself prints a warning and **exits 0**. Add this to your `package.json`, then install:
33
+ >
34
+ > ```json
35
+ > { "pnpm": { "onlyBuiltDependencies": ["roaring"] } }
36
+ > ```
37
+ >
38
+ > pnpm 9 and npm run it already. [Full symptoms and fixes](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/getting-started.md#cannot-find-module-buildreleaseroaringnode-after-a-successful-install).
39
+
18
40
  Depend on `@cloudbitmaps/core` directly only to **author a flavor or a driver**. It has **zero runtime
19
41
  dependencies** of its own.
20
42
 
@@ -0,0 +1,461 @@
1
+ // src/core/errors.ts
2
+ var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error");
3
+ var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error.transient");
4
+ var CloudRoaringError = class extends Error {
5
+ /** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
6
+ [ERROR_BRAND] = true;
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = new.target.name;
10
+ }
11
+ };
12
+ var ValidationError = class extends CloudRoaringError {
13
+ };
14
+ var WriteConflictError = class extends CloudRoaringError {
15
+ };
16
+ var IntegrityError = class extends CloudRoaringError {
17
+ };
18
+ var NotFoundError = class extends CloudRoaringError {
19
+ };
20
+ var UnsupportedError = class extends CloudRoaringError {
21
+ };
22
+ var CapabilityError = class extends CloudRoaringError {
23
+ };
24
+ var BudgetExceededError = class extends CloudRoaringError {
25
+ };
26
+ var KeyUnavailableError = class extends CloudRoaringError {
27
+ };
28
+ var TransientError = class extends CloudRoaringError {
29
+ /** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */
30
+ [TRANSIENT_BRAND] = true;
31
+ constructor(message, options) {
32
+ super(message);
33
+ if (options && "cause" in options) this.cause = options.cause;
34
+ }
35
+ };
36
+ var TimeoutError = class extends TransientError {
37
+ };
38
+ function hasBrand(err, brand) {
39
+ return typeof err === "object" && err !== null && err[brand] === true;
40
+ }
41
+ function isCloudRoaringError(err) {
42
+ return hasBrand(err, ERROR_BRAND);
43
+ }
44
+ function isWriteConflictError(err) {
45
+ return isCloudRoaringError(err) && err.name === "WriteConflictError";
46
+ }
47
+ function isTransientError(err) {
48
+ return hasBrand(err, TRANSIENT_BRAND);
49
+ }
50
+ function isNotFoundError(err) {
51
+ return isCloudRoaringError(err) && err.name === "NotFoundError";
52
+ }
53
+ function isIntegrityError(err) {
54
+ return isCloudRoaringError(err) && err.name === "IntegrityError";
55
+ }
56
+ function isValidationError(err) {
57
+ return isCloudRoaringError(err) && err.name === "ValidationError";
58
+ }
59
+
60
+ // src/core/name-codec.ts
61
+ var KEY_SAFE = /[A-Za-z0-9._:-]/;
62
+ var PATH_SAFE = /[A-Za-z0-9._-]/;
63
+ var UTF8 = new TextEncoder();
64
+ function escapeChar(ch) {
65
+ let out = "";
66
+ for (const byte of UTF8.encode(ch)) {
67
+ out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
68
+ }
69
+ return out;
70
+ }
71
+ function encodeWith(name, safe) {
72
+ let out = "";
73
+ for (const ch of name) {
74
+ out += ch !== "%" && safe.test(ch) ? ch : escapeChar(ch);
75
+ }
76
+ return out.startsWith("_") ? `%5F${out.slice(1)}` : out;
77
+ }
78
+ function decodePercent(encoded) {
79
+ const bytes = [];
80
+ for (let i = 0; i < encoded.length; ) {
81
+ if (encoded[i] === "%" && i + 2 < encoded.length + 1) {
82
+ const hex = encoded.slice(i + 1, i + 3);
83
+ if (!/^[0-9A-Fa-f]{2}$/.test(hex)) return encoded;
84
+ bytes.push(parseInt(hex, 16));
85
+ i += 3;
86
+ } else {
87
+ for (const b of new TextEncoder().encode(encoded[i])) bytes.push(b);
88
+ i += 1;
89
+ }
90
+ }
91
+ return new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(new Uint8Array(bytes));
92
+ }
93
+ function encodeNameForKey(name) {
94
+ return defuseDotComponent(encodeWith(name, KEY_SAFE));
95
+ }
96
+ function defuseDotComponent(encoded) {
97
+ return encoded === "." || encoded === ".." ? encoded.replaceAll(".", "%2E") : encoded;
98
+ }
99
+ function decodeNameFromKey(encoded) {
100
+ return decodePercent(encoded);
101
+ }
102
+ function encodeNameForPath(name) {
103
+ let encoded = encodeWith(name, PATH_SAFE);
104
+ const dotted = defuseDotComponent(encoded);
105
+ if (dotted !== encoded) return dotted;
106
+ const stem = encoded.split(".")[0] ?? "";
107
+ if (/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(stem)) {
108
+ encoded = escapeChar(encoded[0]) + encoded.slice(1);
109
+ }
110
+ if (encoded.endsWith(".")) encoded = `${encoded.slice(0, -1)}%2E`;
111
+ return encoded;
112
+ }
113
+ function decodeNameFromPath(encoded) {
114
+ return decodePercent(encoded);
115
+ }
116
+
117
+ // src/core/validate.ts
118
+ var CHUNK_KEY_MAX = 65535;
119
+ var MAX_NAME_LENGTH = 256;
120
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
121
+ function encodedNameLength(value) {
122
+ return Math.max(encodeNameForKey(value).length, encodeNameForPath(value).length);
123
+ }
124
+ function validatePart(value, field) {
125
+ if (typeof value !== "string") {
126
+ throw new ValidationError(`${field} must be a string; got ${typeof value}`);
127
+ }
128
+ if (value.length === 0) {
129
+ throw new ValidationError(`${field} must not be empty`);
130
+ }
131
+ if (value.length > MAX_NAME_LENGTH) {
132
+ throw new ValidationError(
133
+ `${field} is too long: ${value.length} characters (limit ${MAX_NAME_LENGTH} once encoded for a storage key)`
134
+ );
135
+ }
136
+ if (LONE_SURROGATE.test(value)) {
137
+ throw new ValidationError(
138
+ `${field} contains an unpaired surrogate, so it has no UTF-8 encoding and cannot be stored. This is usually a string sliced through an astral character (an emoji, say) \u2014 slice by code point instead.`
139
+ );
140
+ }
141
+ const encoded = encodedNameLength(value);
142
+ if (encoded > MAX_NAME_LENGTH) {
143
+ throw new ValidationError(
144
+ `${field} is too long: ${encoded} characters once encoded for storage (limit ${MAX_NAME_LENGTH}). The name itself is ${value.length} characters \u2014 encoding expands anything outside [A-Za-z0-9._-], so a name of mostly non-ASCII text, or one full of colons, reaches the limit sooner than its length suggests.`
145
+ );
146
+ }
147
+ }
148
+ function validateSegmentRef(ref) {
149
+ validatePart(ref.segment, "segment");
150
+ if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
151
+ }
152
+ function validateChunkRef(ref) {
153
+ validateSegmentRef(ref);
154
+ if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
155
+ throw new ValidationError(
156
+ `chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]; got ${ref.chunkKey}`
157
+ );
158
+ }
159
+ }
160
+
161
+ // src/core/concurrency.ts
162
+ async function mapWithConcurrency(items, limit, fn) {
163
+ if (!Number.isInteger(limit) || limit < 1) {
164
+ throw new ValidationError(`concurrency limit must be a positive integer; got ${limit}`);
165
+ }
166
+ const results = new Array(items.length);
167
+ let next = 0;
168
+ let failed = false;
169
+ let firstError;
170
+ const worker = async () => {
171
+ while (!failed) {
172
+ const i = next++;
173
+ if (i >= items.length) return;
174
+ try {
175
+ results[i] = await fn(items[i], i);
176
+ } catch (err) {
177
+ if (!failed) {
178
+ failed = true;
179
+ firstError = err;
180
+ }
181
+ return;
182
+ }
183
+ }
184
+ };
185
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
186
+ await Promise.all(workers);
187
+ if (failed) throw firstError;
188
+ return results;
189
+ }
190
+
191
+ // src/drivers/_shared/keys.ts
192
+ var DEFAULT_NAMESPACE = "_default";
193
+ function namespaceKeyPart(namespace) {
194
+ return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForKey(namespace);
195
+ }
196
+ function namespacePathPart(namespace) {
197
+ return namespace === void 0 ? DEFAULT_NAMESPACE : encodeNameForPath(namespace);
198
+ }
199
+
200
+ // src/core/ports.ts
201
+ var STORAGE_BACKEND_BRAND = /* @__PURE__ */ Symbol.for("cloudbitmaps.storage-backend");
202
+ function createBackend(halves) {
203
+ if (halves === null || typeof halves !== "object") {
204
+ throw new ValidationError("createBackend needs `{ storage, registry }`");
205
+ }
206
+ const { storage, registry } = halves;
207
+ if (storage === null || typeof storage !== "object" || typeof storage.putImmutable !== "function") {
208
+ throw new ValidationError(
209
+ "createBackend: `storage` must be an IStorageDriver (it has no `putImmutable`)"
210
+ );
211
+ }
212
+ if (registry === null || typeof registry !== "object" || typeof registry.compareAndSwap !== "function") {
213
+ throw new ValidationError(
214
+ "createBackend: `registry` must be an IRegistryDriver (it has no `compareAndSwap`)"
215
+ );
216
+ }
217
+ return brandAsBackend({ storage, registry });
218
+ }
219
+ var STORAGE_BACKEND = STORAGE_BACKEND_BRAND;
220
+ function brandAsBackend(target) {
221
+ Object.defineProperty(target, STORAGE_BACKEND_BRAND, {
222
+ value: true,
223
+ enumerable: false,
224
+ writable: false,
225
+ configurable: false
226
+ });
227
+ return target;
228
+ }
229
+ function isStorageBackend(value) {
230
+ return typeof value === "object" && value !== null && value[STORAGE_BACKEND_BRAND] === true;
231
+ }
232
+
233
+ // src/drivers/_shared/registry.ts
234
+ var STATUSES = ["active", "compacting", "erasing", "destroyed"];
235
+ var MAX_GOVERNANCE_BYTES = 64 * 1024;
236
+ var MAX_WRAPPED_DEKS = 8;
237
+ var MAX_WRAPPED_DEK_BYTES = 4 * 1024;
238
+ function validateGeneration(gen) {
239
+ if (gen === null) return;
240
+ if (!Number.isInteger(gen) || gen < 0) {
241
+ throw new ValidationError(
242
+ `currentGen must be a non-negative integer or null (no Storage generation yet); got ${gen}`
243
+ );
244
+ }
245
+ }
246
+ function validatePatchGeneration(patch) {
247
+ if (!("currentGen" in patch)) return;
248
+ if (patch.currentGen === void 0) {
249
+ throw new ValidationError(
250
+ `currentGen was present in the patch but undefined. Pass \`null\` to clear the pointer (the segment has no Storage generation), or omit the key to leave it unchanged \u2014 an undefined value is refused because it used to be a no-op and would now un-publish the segment's Storage data.`
251
+ );
252
+ }
253
+ validateGeneration(patch.currentGen);
254
+ }
255
+ function validateStatus(status) {
256
+ if (!STATUSES.includes(status)) {
257
+ throw new ValidationError(`status must be one of ${STATUSES.join("/")}; got ${status}`);
258
+ }
259
+ }
260
+ function validateGovernance(meta, field) {
261
+ if (meta === void 0) return;
262
+ if (meta === null || typeof meta !== "object" || Array.isArray(meta)) {
263
+ throw new ValidationError(
264
+ `${field} must be a plain object (got ${meta === null ? "null" : typeof meta})`
265
+ );
266
+ }
267
+ let json;
268
+ try {
269
+ json = JSON.stringify(meta);
270
+ } catch {
271
+ throw new ValidationError(`${field} must be JSON-serializable`);
272
+ }
273
+ if (json.length > MAX_GOVERNANCE_BYTES) {
274
+ throw new ValidationError(`${field} is ${json.length}B, exceeds cap ${MAX_GOVERNANCE_BYTES}B`);
275
+ }
276
+ }
277
+ function validateWrappedDeks(value, isStored) {
278
+ if (value === void 0) return;
279
+ const fail = (msg) => {
280
+ throw isStored ? new IntegrityError(msg) : new ValidationError(msg);
281
+ };
282
+ if (!Array.isArray(value)) fail("wrappedDeks must be an array");
283
+ const list = value;
284
+ if (list.length === 0) fail("wrappedDeks must be non-empty when present");
285
+ if (list.length > MAX_WRAPPED_DEKS)
286
+ fail(`wrappedDeks has ${list.length} entries, cap ${MAX_WRAPPED_DEKS}`);
287
+ for (const w of list) {
288
+ if (w === null || typeof w !== "object") fail("wrappedDeks entry must be an object");
289
+ const { keyId, wrapped } = w;
290
+ if (typeof keyId !== "string" || keyId.length === 0)
291
+ fail("wrappedDeks entry needs a non-empty keyId");
292
+ if (typeof wrapped !== "string" || wrapped.length === 0)
293
+ fail("wrappedDeks entry needs a non-empty wrapped blob");
294
+ if (wrapped.length > MAX_WRAPPED_DEK_BYTES)
295
+ fail(`wrappedDeks entry is ${wrapped.length}B, cap ${MAX_WRAPPED_DEK_BYTES}B`);
296
+ }
297
+ }
298
+ function validateNewRegistryRecord(rec) {
299
+ validateGeneration(rec.currentGen);
300
+ if (rec.status !== void 0) validateStatus(rec.status);
301
+ validateWrappedDeks(rec.wrappedDeks, false);
302
+ validateGovernance(rec.retention, "retention");
303
+ validateGovernance(rec.residency, "residency");
304
+ }
305
+ function validateRegistryPatch(patch) {
306
+ validatePatchGeneration(patch);
307
+ if (patch.status !== void 0) validateStatus(patch.status);
308
+ if ("wrappedDeks" in patch) validateWrappedDeks(patch.wrappedDeks, false);
309
+ if ("retention" in patch) validateGovernance(patch.retention, "retention");
310
+ if ("residency" in patch) validateGovernance(patch.residency, "residency");
311
+ }
312
+ var REGISTRY_SCHEMA_VERSION = 1;
313
+ function assertRegistrySchemaVersion(raw, ctx) {
314
+ if (raw === void 0) return;
315
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1) {
316
+ throw new IntegrityError(`registry row has a malformed schemaVersion (${String(raw)}): ${ctx}`);
317
+ }
318
+ if (raw > REGISTRY_SCHEMA_VERSION) {
319
+ throw new UnsupportedError(
320
+ `registry row schemaVersion ${raw} is newer than this build reads (v${REGISTRY_SCHEMA_VERSION}): ${ctx}`
321
+ );
322
+ }
323
+ }
324
+ function serializeRegistryEnvelope(env) {
325
+ return JSON.stringify({
326
+ schemaVersion: REGISTRY_SCHEMA_VERSION,
327
+ deleted: env.deleted,
328
+ record: env.record
329
+ });
330
+ }
331
+ function registryCounterOf(record) {
332
+ if (!/^(0|[1-9]\d*)$/.test(record.token)) {
333
+ throw new IntegrityError(`registry row token is not a canonical counter: ${record.token}`);
334
+ }
335
+ const n = Number(record.token);
336
+ if (!Number.isSafeInteger(n)) {
337
+ throw new IntegrityError(`registry row token is out of safe-integer range: ${record.token}`);
338
+ }
339
+ return n;
340
+ }
341
+ function parseRegistryEnvelope(text, ctx) {
342
+ let parsed;
343
+ try {
344
+ parsed = JSON.parse(text);
345
+ } catch {
346
+ throw new IntegrityError(`registry row is not valid JSON: ${ctx}`);
347
+ }
348
+ if (parsed === null || typeof parsed !== "object") {
349
+ throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
350
+ }
351
+ const env = parsed;
352
+ assertRegistrySchemaVersion(env.schemaVersion, ctx);
353
+ if (typeof env.deleted !== "boolean" || env.record === null || typeof env.record !== "object") {
354
+ throw new IntegrityError(`registry row has a malformed envelope: ${ctx}`);
355
+ }
356
+ const r = env.record;
357
+ assertStoredRecordShape(r, ctx);
358
+ if (typeof r.token !== "string") {
359
+ throw new IntegrityError(`registry row is missing its token: ${ctx}`);
360
+ }
361
+ return { deleted: env.deleted, record: env.record };
362
+ }
363
+ function recordFromNew(ref, rec, now, token) {
364
+ return {
365
+ namespace: ref.namespace,
366
+ segment: ref.segment,
367
+ currentGen: rec.currentGen,
368
+ wrappedDeks: rec.wrappedDeks,
369
+ keyId: rec.keyId,
370
+ status: rec.status ?? "active",
371
+ retention: rec.retention,
372
+ residency: rec.residency,
373
+ createdAt: now,
374
+ updatedAt: now,
375
+ token
376
+ };
377
+ }
378
+ function assertStoredRecordShape(r, ctx) {
379
+ 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") {
380
+ throw new IntegrityError(`registry record is missing required fields: ${ctx}`);
381
+ }
382
+ if (r.currentGen !== null && (!Number.isInteger(r.currentGen) || r.currentGen < 0)) {
383
+ throw new IntegrityError(`registry record has an invalid currentGen (${r.currentGen}): ${ctx}`);
384
+ }
385
+ if (!STATUSES.includes(r.status)) {
386
+ throw new IntegrityError(`registry record has an unknown status (${r.status}): ${ctx}`);
387
+ }
388
+ if (r.keyId !== void 0 && typeof r.keyId !== "string") {
389
+ throw new IntegrityError(`registry record has an invalid keyId: ${ctx}`);
390
+ }
391
+ validateWrappedDeks(r.wrappedDeks, true);
392
+ for (const field of ["retention", "residency"]) {
393
+ const meta = r[field];
394
+ if (meta !== void 0 && (meta === null || typeof meta !== "object" || Array.isArray(meta))) {
395
+ throw new IntegrityError(`registry record has a non-object ${field}: ${ctx}`);
396
+ }
397
+ }
398
+ }
399
+ function applyRegistryPatch(prev, patch, now, token) {
400
+ return {
401
+ namespace: prev.namespace,
402
+ segment: prev.segment,
403
+ // `'currentGen' in patch`, NOT `patch.currentGen ?? prev.currentGen`. `null` is a legal, meaningful value
404
+ // here (no Storage generation yet), and `??` treats it as absent — so the nullish form would silently ignore a
405
+ // patch that clears the pointer and leave the old generation in place. The same reason `wrappedDeks` and
406
+ // `keyId` below use presence: any field whose null is a *value* cannot be merged with `??`.
407
+ currentGen: "currentGen" in patch ? patch.currentGen ?? null : prev.currentGen,
408
+ wrappedDeks: "wrappedDeks" in patch ? patch.wrappedDeks : prev.wrappedDeks,
409
+ keyId: "keyId" in patch ? patch.keyId : prev.keyId,
410
+ status: patch.status ?? prev.status,
411
+ retention: "retention" in patch ? patch.retention : prev.retention,
412
+ residency: "residency" in patch ? patch.residency : prev.residency,
413
+ createdAt: prev.createdAt,
414
+ updatedAt: now,
415
+ token
416
+ };
417
+ }
418
+
419
+ export {
420
+ CloudRoaringError,
421
+ ValidationError,
422
+ WriteConflictError,
423
+ IntegrityError,
424
+ NotFoundError,
425
+ UnsupportedError,
426
+ CapabilityError,
427
+ BudgetExceededError,
428
+ KeyUnavailableError,
429
+ TransientError,
430
+ TimeoutError,
431
+ isCloudRoaringError,
432
+ isWriteConflictError,
433
+ isTransientError,
434
+ isNotFoundError,
435
+ isIntegrityError,
436
+ isValidationError,
437
+ encodeNameForKey,
438
+ decodeNameFromKey,
439
+ encodeNameForPath,
440
+ decodeNameFromPath,
441
+ MAX_NAME_LENGTH,
442
+ encodedNameLength,
443
+ validateSegmentRef,
444
+ validateChunkRef,
445
+ mapWithConcurrency,
446
+ DEFAULT_NAMESPACE,
447
+ namespaceKeyPart,
448
+ namespacePathPart,
449
+ createBackend,
450
+ STORAGE_BACKEND,
451
+ brandAsBackend,
452
+ isStorageBackend,
453
+ validateNewRegistryRecord,
454
+ validateRegistryPatch,
455
+ serializeRegistryEnvelope,
456
+ registryCounterOf,
457
+ parseRegistryEnvelope,
458
+ recordFromNew,
459
+ applyRegistryPatch
460
+ };
461
+ //# sourceMappingURL=chunk-BUPDGZRR.js.map