@cloudbitmaps/core 0.9.0 → 0.11.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 +33 -16
  3. package/dist/chunk-5OMC5U7U.js +508 -0
  4. package/dist/chunk-5OMC5U7U.js.map +7 -0
  5. package/dist/core/audit.d.ts +128 -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 +68 -0
  9. package/dist/core/codec.d.ts +122 -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 +254 -0
  14. package/dist/core/crbm/crc32c.d.ts +10 -0
  15. package/dist/core/crbm/format.d.ts +114 -0
  16. package/dist/core/crbm/reader.d.ts +86 -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 +522 -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 +77 -0
  23. package/dist/core/due-index.d.ts +53 -0
  24. package/dist/core/engine.d.ts +246 -0
  25. package/dist/core/erase-id.d.ts +176 -0
  26. package/dist/core/erasure.d.ts +185 -0
  27. package/dist/core/errors.d.ts +113 -0
  28. package/dist/core/generation-gc.d.ts +56 -0
  29. package/dist/core/keys.d.ts +23 -0
  30. package/dist/core/load.d.ts +121 -0
  31. package/dist/core/lru.d.ts +69 -0
  32. package/dist/core/metrics.d.ts +136 -0
  33. package/dist/core/name-codec.d.ts +69 -0
  34. package/dist/core/pinned-storage-source.d.ts +90 -0
  35. package/dist/core/ports.d.ts +423 -0
  36. package/dist/core/reader-defaults.d.ts +18 -0
  37. package/dist/core/registry-scan.d.ts +47 -0
  38. package/dist/core/retention-sweep.d.ts +198 -0
  39. package/dist/core/retention.d.ts +67 -0
  40. package/dist/core/retry.d.ts +69 -0
  41. package/dist/core/rollback.d.ts +75 -0
  42. package/dist/core/validate.d.ts +44 -0
  43. package/dist/driver-kit.d.ts +9 -0
  44. package/dist/driver-kit.js +275 -0
  45. package/dist/driver-kit.js.map +7 -0
  46. package/dist/drivers/_shared/keys.d.ts +26 -0
  47. package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
  48. package/dist/drivers/_shared/object-registry.d.ts +89 -0
  49. package/dist/drivers/_shared/registry.d.ts +66 -0
  50. package/dist/drivers/backends.d.ts +43 -0
  51. package/dist/drivers/crypto.d.ts +38 -0
  52. package/dist/drivers/localfs/fs-util.d.ts +21 -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 +49 -0
  57. package/dist/drivers/retry/retrying-chunk-source.d.ts +59 -0
  58. package/dist/export/index.d.ts +93 -0
  59. package/dist/index.d.ts +76 -2760
  60. package/dist/index.js +3527 -3133
  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-BD22WDAN.js +0 -230
  79. package/dist/chunk-BD22WDAN.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-OZ6LAFV4.js +0 -6
  83. package/dist/chunk-OZ6LAFV4.js.map +0 -1
  84. package/dist/chunk-SNJVZ227.js +0 -35
  85. package/dist/chunk-SNJVZ227.js.map +0 -1
  86. package/dist/dynamodb/index.cjs +0 -767
  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 -4990
  99. package/dist/index.cjs.map +0 -1
  100. package/dist/index.d.cts +0 -2761
  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-CvcvJWf6.d.cts +0 -395
  114. package/dist/ports-CvcvJWf6.d.ts +0 -395
  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 -918
  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
@@ -0,0 +1,39 @@
1
+ import type { CrbmCrypto } from '../crypto.js';
2
+ import type { BlobSink } from '../blob.js';
3
+ export interface CrbmWriterOptions {
4
+ /** Self-describing generation number (also encoded in the object key). */
5
+ readonly generation: number;
6
+ /**
7
+ * Which codec produced the chunk payloads. Defaults to {@link PAYLOAD_CODEC_ROARING_PORTABLE}.
8
+ *
9
+ * A codec id rather than a roaring-specific one, because `.crbm` is a shared container and a second codec is
10
+ * expected before the format freezes.
11
+ */
12
+ readonly payloadCodecId?: number;
13
+ readonly elementWidth?: number;
14
+ /** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
15
+ readonly crypto?: CrbmCrypto;
16
+ }
17
+ export declare class CrbmWriter {
18
+ private readonly sink;
19
+ private readonly options;
20
+ private readonly entries;
21
+ private offset;
22
+ private lastKey;
23
+ private totalCardinality;
24
+ private preambleWritten;
25
+ private finished;
26
+ constructor(sink: BlobSink, options: CrbmWriterOptions);
27
+ /**
28
+ * Append one chunk's payload (the roaring-serialized bytes) with its cardinality. `chunkKey` must be
29
+ * strictly greater than the previous one (ascending, no duplicates); `cardinality` is `[1, 65536]`.
30
+ */
31
+ addChunk(chunkKey: number, payload: Uint8Array, cardinality: number): Promise<void>;
32
+ /** Seal one chunk payload into the on-disk frame `nonce ‖ ciphertext ‖ tag`, bound to its chunkKey. */
33
+ private sealChunk;
34
+ /** Write the index region + the fixed footer. After this the object is complete and immutable. */
35
+ finish(): Promise<void>;
36
+ private ensurePreamble;
37
+ private encodeIndex;
38
+ private buildFooter;
39
+ }
@@ -0,0 +1,522 @@
1
+ import { type IAuditSink } from './audit.js';
2
+ import type { Yielder } from './cooperative.js';
3
+ import type { Clock } from './determinism.js';
4
+ import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
+ import type { ChunkRef, StorageChunkSource, GenKey, IStorageDriver, IRegistryDriver, SegmentRef, SegmentSize, Token } from './ports.js';
6
+ import { CrbmReader } from './crbm/reader.js';
7
+ import type { CrbmReaderOptions } from './crbm/reader.js';
8
+ import type { CodecBitmap, CodecInterface } from './codec.js';
9
+ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
10
+ /**
11
+ * Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
12
+ * authoritative `currentGen` (one cheap strong read) instead of a `list` scan of every generation — the
13
+ * retirement of that scan. When absent, the source falls back to the `list`-scan (so the
14
+ * in-memory / simple setups keep working with no registry). The resolved generation is cached and
15
+ * **re-resolved on a short TTL** ({@link currentGenTtlMs}, needs a {@link clock}) so a long-lived source
16
+ * observes another process's load within the TTL, not only when something else happens to re-resolve it.
17
+ */
18
+ readonly registry?: IRegistryDriver;
19
+ /**
20
+ * Optional {@link IKeystore} for reading **encrypted** segments. When a segment's registry record
21
+ * carries wrapped DEK(s), the source unwraps the DEK via this keystore and decrypts each chunk/index. Reading
22
+ * an encrypted segment without a keystore throws {@link KeyUnavailableError}; cleartext segments ignore it.
23
+ * Requires a `registry` (that's where the wrapped DEKs live).
24
+ */
25
+ readonly keystore?: IKeystore;
26
+ /**
27
+ * Enforce that every segment read is encrypted. When true, resolving a **cleartext** segment (no
28
+ * wrapped DEKs) throws {@link KeyUnavailableError} — a guard against silently reading a segment that should
29
+ * have been encrypted. Off by default (encryption is opt-in).
30
+ */
31
+ readonly requireEncryption?: boolean;
32
+ /**
33
+ * Time source for the current-generation TTL refresh — the determinism seam; `core/` never reads
34
+ * ambient time. Refresh needs **both** a clock and a `registry`. Without either there is no timed refresh, and
35
+ * that is the only difference: a segment is still re-resolved when its reader is evicted, when a fetch finds its
36
+ * generation swept, and on {@link CrbmStorageChunkSource.invalidate}. What goes is the bound on how long another
37
+ * process's publish takes to arrive. The `CloudRoaring` facade passes its clock automatically, so wiring a
38
+ * `registry` is enough to get the bounded refresh.
39
+ */
40
+ readonly clock?: Pick<Clock, 'now'>;
41
+ /**
42
+ * How long (ms) a resolved `currentGen` is trusted before the next read re-resolves it (default 2000) — the
43
+ * bound on read staleness after a load publishes: a reader may serve the prior generation for up to this long,
44
+ * then converges. Lazy (checked on read — no timer); ≤ one cheap registry read per segment per window, and a
45
+ * new {@link CrbmReader} is opened only when the generation actually changed. `0` turns the refresh off, as a
46
+ * missing clock or registry does: {@link clock} says what that changes, and what it does not.
47
+ */
48
+ readonly currentGenTtlMs?: number;
49
+ /**
50
+ * Hard ceiling on how many segments' readers (each holding a fully-parsed `.crbm` index) are cached at once
51
+ * (default 1024) — a memory bound for a long-running server. Past it, the least-recently-used
52
+ * segment's reader is evicted; the next read of an evicted segment re-opens it (one cheap tail GET, since
53
+ * generations are immutable). Raise it for a big cache working set of small segments.
54
+ */
55
+ readonly maxOpenSegments?: number;
56
+ /**
57
+ * Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
58
+ * **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
59
+ * parsed index can be several MB, so 1024 wide indices could pin ~GBs and blow a small heap (e.g. a 128 MB
60
+ * Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedIndexBytes}
61
+ * across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
62
+ * of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
63
+ * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
64
+ */
65
+ readonly maxOpenIndexBytes?: number;
66
+ }
67
+ /**
68
+ * What a pin holds of the object it pinned: the version its cache entries are keyed by, and the object's
69
+ * fingerprint, which a pinned read checks each time it opens the generation. `pinGeneration` always records one.
70
+ * A pin built by hand without one is not checked: it reads whatever object is under its generation's key.
71
+ */
72
+ export interface PinnedObject {
73
+ readonly version: string;
74
+ readonly fingerprint?: string;
75
+ }
76
+ export declare class CrbmStorageChunkSource implements StorageChunkSource {
77
+ private readonly driver;
78
+ /**
79
+ * One resolved reader per segment, re-resolved on a short TTL ({@link CrbmStorageChunkSourceOptions.currentGenTtlMs},
80
+ * needs a clock). Within the TTL a segment's Storage bytes are treated as an immutable snapshot; when the TTL
81
+ * elapses the next read cheaply re-resolves `currentGen` and, only if it advanced (a load published),
82
+ * opens the new generation — so a long-lived source observes new generations within the TTL. The engine pairs
83
+ * this with a **generation-keyed** cache so a bump never serves a stale decoded chunk. Without a clock or a
84
+ * registry there is no timed refresh, and a segment's reader is replaced only when it is evicted, when a read
85
+ * has to fetch from a generation a sweep deleted, or when {@link invalidate} drops it. A segment with no generation yet is not memoized, so
86
+ * it's re-checked until one exists. **Bounded** by a {@link BoundedLru} ({@link CrbmStorageChunkSourceOptions.maxOpenSegments}, default
87
+ * 1024): past the ceiling the least-recently-used segment's reader (and its parsed index) is evicted — the
88
+ * steady-state memory bound; re-opening an evicted segment is one cheap tail GET.
89
+ */
90
+ private readonly snapshots;
91
+ /**
92
+ * Pins ({@link heldKey}) whose object has been found replaced: another object is under its generation's key. Kept,
93
+ * and bounded as the readers are, so that a pin's later chunk reads and any reopen fail at once rather than pay for
94
+ * the check again. A replacement can be undone, by a restore that puts the pinned object back, so
95
+ * {@link invalidate} forgets the segment's, and a pin of the same version that opens the object forgets the one
96
+ * against it. An object found gone is not kept: that is the open's own `NotFoundError`, and a 404 can pass. A
97
+ * reader still memoised keeps answering from the index it holds, so the pin's `count()` does, until a later pin of
98
+ * that version opens the object now under the key, which takes its place.
99
+ */
100
+ private readonly replacedPins;
101
+ /**
102
+ * A check of a pinned object still under way, by {@link heldKey}, so the reads that ask at once share one footer
103
+ * read. One forgotten before it ends, by {@link invalidate} or a pin, keeps nothing it finds.
104
+ */
105
+ private readonly checking;
106
+ /**
107
+ * How many times {@link invalidate} has run, so a read that waited can tell whether the store was told something
108
+ * meanwhile. An eviction from the reader cache is not an invalidation, and what a read found stands through one. It
109
+ * counts every segment's invalidations: a read that waited through another segment's only costs a later read the
110
+ * check again.
111
+ */
112
+ private invalidations;
113
+ private readonly registry;
114
+ private readonly keystore;
115
+ private readonly requireEncryption;
116
+ private readonly readerOptions;
117
+ private readonly clock;
118
+ private readonly currentGenTtlMs;
119
+ constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
120
+ /** The current resolved reader for a segment, refreshing on the TTL. Cheap within the TTL window. */
121
+ private resolvedReader;
122
+ /**
123
+ * A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
124
+ * {@link REFRESH_RETRY_MS} (never longer than the TTL) rather than after a whole TTL, so the next read after
125
+ * that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
126
+ */
127
+ private retrySoon;
128
+ /** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
129
+ private computeRefreshedReader;
130
+ /**
131
+ * Memoize a reader-promise as the segment's snapshot at `now()`, forgetting it later if it resolves to
132
+ * null / throws (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the
133
+ * (pending) promise **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
134
+ */
135
+ private install;
136
+ private now;
137
+ /**
138
+ * How often this source re-reads a segment's pointer while the segment is being read, in ms: its TTL, or 0 when
139
+ * it has no timed refresh (no clock, no registry, or a TTL of 0). The grounded cost report prices the refresh at
140
+ * this.
141
+ */
142
+ get pointerRefreshMs(): number;
143
+ private expired;
144
+ /**
145
+ * The segment's current generation number — the engine keys its chunk cache by this so a generation bump
146
+ * is observed instead of serving a stale decoded chunk. Served from the (TTL-refreshed)
147
+ * snapshot, so no extra backend read within the TTL window. `null` if the segment has no committed generation.
148
+ *
149
+ * Heals a swept generation exactly like {@link withFreshSnapshot}: a caller that resolves the generation once
150
+ * per op, before any chunk fetch, would otherwise fail the whole operation rather than one chunk. The engine
151
+ * keys by {@link currentVersion} instead, which heals the same way, and falls back to this for a source that
152
+ * cannot report a version. It is spelled out rather than delegated because a lookup like this runs once per
153
+ * operand of every `has`/`count`/`iterate`/`intersect`, almost always served from the cached snapshot with
154
+ * no backend call at all. Routing it through the generic helper cost ~115 ns/op on that path (a second async
155
+ * frame, a per-call closure, and an `await` on a plain number) for a race that fires only during a
156
+ * concurrent sweep. One retry, then propagate — same contract, same eviction rule.
157
+ */
158
+ currentGeneration(ref: SegmentRef): Promise<number | null>;
159
+ /**
160
+ * Forget everything derived from `ref`: the resolved snapshot and the open reader behind it (and with the
161
+ * reader, the DEK it unwrapped at open time), and what its pins' objects were found to be. The next read
162
+ * resolves the segment again.
163
+ *
164
+ * Needed because the TTL refresh only ever answers "has the pointer moved?", and every *destructive* verb —
165
+ * an erasure that deletes the generation holding the bit, a `dropSegment`, a crypto-shred, a retirement —
166
+ * leaves the pointer's answer unchanged from this source's point of view while making the snapshot wrong.
167
+ * Until this is called the source keeps serving that snapshot with no backend read at all, so nothing on the
168
+ * storage side can close the window; with no registry, no clock or `cache.genTtlMs: 0`, nothing bounds it at all.
169
+ */
170
+ invalidate(ref: SegmentRef): void;
171
+ /**
172
+ * `<generation>` for a registry-less source, or `<generation>:<row token>` with one. The token moves on every row
173
+ * write, so an unrelated write (a `setRetention`) costs the segment's decoded chunks once — bounded, and on an
174
+ * admin path. Exactness in the direction that matters: with a registry, two incarnations can never share a version
175
+ * string. Without one they can: a name purged and loaded again out of band restarts at the same bare number, and
176
+ * a live read that reopens it after an eviction reads the new object's chunks beside the old one's cached ones. A
177
+ * store with no registry cannot write, so that takes a change made from outside it; a pin is kept apart from it by
178
+ * the fingerprint it records.
179
+ *
180
+ * This is the call the engine makes **once per operand of every read**, before any chunk fetch, to key its
181
+ * chunk cache, so it heals a swept generation exactly as {@link currentGeneration} does: unhealed, a cold read
182
+ * racing a publish and a `keep: 0` sweep failed the whole operation with `NotFoundError`, where the same race
183
+ * on a chunk fetch heals. Spelled out rather than delegated for the reason `currentGeneration` gives: it is
184
+ * almost always served from the cached snapshot with no backend call at all. One retry, then propagate.
185
+ */
186
+ currentVersion(ref: SegmentRef): Promise<string | null>;
187
+ /**
188
+ * Resolve the segment **once** and report what a pin should hold: the generation, and the version that
189
+ * identifies those exact bytes. `null` when the segment resolves to no generation — there is nothing to pin,
190
+ * and a caller must treat that as "this handle reads empty", not "pinning is unsupported here".
191
+ *
192
+ * Heals a generation swept between the registry read and the open, as {@link currentVersion} does: a publish
193
+ * and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
194
+ * once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
195
+ */
196
+ pinGeneration(ref: SegmentRef): Promise<({
197
+ generation: number;
198
+ } & Required<PinnedObject>) | null>;
199
+ private pinOnce;
200
+ /**
201
+ * Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
202
+ * a name purged and loaded again gets a new row, and with it a new token. Without one the version is the bare
203
+ * generation number, which two incarnations share, so the object's fingerprint is added to tell them apart.
204
+ */
205
+ private pinnedKey;
206
+ /**
207
+ * One pin's hold on one segment: its version, and the fingerprint of the object it pinned. A check of that object
208
+ * is shared under it, and what the check finds is kept under it, with a registry or without one: pins of one
209
+ * version can hold different objects, since a pin built by hand can name any, and an object replaced from outside
210
+ * the store leaves the row, and so the version, as it was.
211
+ */
212
+ private heldKey;
213
+ /**
214
+ * A reader for one **specific** generation, memoized in the same bounded LRU as the live snapshots under a
215
+ * generation-qualified key.
216
+ *
217
+ * Sharing the LRU is the point, not an implementation detail. A pinned reader held in a private field is
218
+ * outside the memory ceiling the library advertises — measured at 10.34 MiB per live pin, 32 pins holding
219
+ * 111.7 MiB against an 8 MiB configured bound. A pinned *generation number* has no such problem: an object is
220
+ * immutable, so eviction is harmless, and re-opening at the same number reopens the same bytes unless the name
221
+ * was purged and loaded again, which the pin's fingerprint then says. It also removes a memoized-rejection bug by
222
+ * construction — `install` forgets a promise that rejects,
223
+ * where a hand-rolled `this.reader ??= open()` cached the rejection for the life of the handle and made a pin
224
+ * the one read path with no resilience.
225
+ *
226
+ * **`status` is re-checked here**, on every open rather than once at pin time. A pin taken before a
227
+ * crypto-shred must not keep unwrapping a DEK the shred destroyed; a destroyed segment resolves no generation
228
+ * for anyone, and a pin's read of one fails. That holds from the pin's next open: a reader already open keeps
229
+ * the key it unwrapped until it is evicted or invalidated.
230
+ *
231
+ * **And the object is checked**, when the caller says which one it pinned. A generation number is not an
232
+ * identity (invariant 1): once a name is purged and loaded again, its new segment starts again at generation 0,
233
+ * so a pin of the old one would open the new one's object as its own, and read it beside the chunks it had
234
+ * already cached from the old one. The row's token cannot tell them apart, since it moves on every write, a
235
+ * publish included; the object's fingerprint can, and a pin whose object has been replaced fails, as one whose
236
+ * generation has been swept does. The memo is keyed so that two pins of one generation number in two
237
+ * incarnations never share a reader, with a registry or without one ({@link pinnedKey}).
238
+ */
239
+ private readerAt;
240
+ private openAt;
241
+ /**
242
+ * Read one chunk of a specific generation — the pinned read path. `held`, when given, is what a pin holds of the
243
+ * object, and the read fails if the generation is now another object.
244
+ */
245
+ getChunkAt(ref: ChunkRef, generation: number, held?: PinnedObject): Promise<Uint8Array | null>;
246
+ /**
247
+ * Throws the pin's NotFoundError if the object under `generation`'s key is no longer the one the pin holds. Its
248
+ * footer says, read with no key and no row ({@link CrbmReader.sameObject}), so an object written under a key this
249
+ * store lacks answers the same way, and so does one gone from its key. A replacement is kept
250
+ * ({@link replacedPins}), and a check under way is shared by the reads that ask at once; neither "gone" nor "the
251
+ * same" is kept, since either can change. A transient fault is rethrown, for the store's retries to see for what it
252
+ * is. A footer that cannot be read otherwise says nothing about which object is there, so this returns, and the
253
+ * caller's own error stands.
254
+ */
255
+ private throwIfReplaced;
256
+ /** Chunk keys of a specific generation — the pinned shape read. `held` as for {@link getChunkAt}. */
257
+ listChunkKeysAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<number[]>;
258
+ /**
259
+ * Per-chunk cardinalities of a specific generation — powers a pinned `count()` with no payload reads. `held` as
260
+ * for {@link getChunkAt}.
261
+ */
262
+ cardinalitiesAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<ReadonlyMap<number, number> | null>;
263
+ /** Grounded size of a specific generation. `held` as for {@link getChunkAt}. */
264
+ sizeOfAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<SegmentSize | null>;
265
+ /**
266
+ * Whether the segment is a registered name at all. With a registry that is exactly "a row exists" — including
267
+ * a row with no generation yet (minted by `setRetention` before the first load) and a `destroyed` tombstone,
268
+ * both of which are names somebody deliberately created. Without a registry there is no registration to
269
+ * consult, so the honest answer is whether the bucket holds any generation of it.
270
+ *
271
+ * Deliberately NOT served from the snapshot memo: the memo resolves to `null` for both states this exists to
272
+ * tell apart.
273
+ */
274
+ exists(ref: SegmentRef): Promise<boolean>;
275
+ /**
276
+ * Evict a snapshot we just failed to read from — matched by its reader *promise*, so a concurrent call's
277
+ * fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
278
+ * wholesale, so comparing anything else would drop a good snapshot on the floor.
279
+ */
280
+ private dropStale;
281
+ private resolveLatest;
282
+ /** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
283
+ private openForTarget;
284
+ /**
285
+ * The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
286
+ * `list` scan for the max generation when there's no registry (a registry-less source can only read cleartext
287
+ * — there's nowhere a wrapped DEK could live).
288
+ */
289
+ private resolveTarget;
290
+ /** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
291
+ private cryptoForRead;
292
+ getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
293
+ listChunkKeys(ref: SegmentRef): Promise<number[]>;
294
+ sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
295
+ cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
296
+ /**
297
+ * Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
298
+ * generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
299
+ * {@link NotFoundError}. Surfacing that as a query failure would turn a benign, recoverable race into a
300
+ * caller-visible error, so instead we drop the stale snapshot,
301
+ * re-resolve `currentGen`, and retry once — the read then serves the newer (committed, immutable) generation,
302
+ * a monotonic move forward — within one incarnation of the row. A name that was retired and re-created is a
303
+ * different segment and can resolve to a LOWER generation, which is why the snapshot carries the row's token
304
+ * as its lineage rather than trusting the generation number (invariant 1).
305
+ *
306
+ * **Resolution and open are inside the retry, not before it.** Resolving `currentGen` and opening that
307
+ * generation's object are two backend round trips with a gap between them, so the object can be swept after
308
+ * the registry named it and before its tail is read — and that miss surfaces from awaiting the snapshot, not
309
+ * from `read`. Awaiting outside the retry left the *first* attempt unhealed on the one path GC actually
310
+ * races: `keep: 0`, which every id erasure passes, sweeps microseconds after the publish.
311
+ *
312
+ * A *second* NotFound is pathological (GC outrunning resolution) and propagates rather than fabricating an
313
+ * absent answer — never return a wrong result. `ifGone` is returned when the segment has no committed
314
+ * generation to serve: storage is empty, or the row was dropped or crypto-shredded, in which case reading empty
315
+ * is the documented outcome rather than a failure.
316
+ *
317
+ * **Bounded to exactly two resolve-and-open round trips.** That is a cost contract, not a detail: the retry
318
+ * now re-reads the *registry*, so an unbounded one would hammer the shared, throttle-prone resource in a
319
+ * tight loop on a segment whose pointer names an object that is permanently absent — and an N-way
320
+ * `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
321
+ */
322
+ private withFreshSnapshot;
323
+ }
324
+ /**
325
+ * Write one immutable generation from in-memory bitmaps. Chunks are
326
+ * sorted ascending and empty bitmaps skipped (empty chunks are never stored). Returns the driver's
327
+ * `{ size, sha256 }` for the written object. Pass `options.crypto` to AES-256-GCM-encrypt the generation
328
+ * (built from the segment's DEK, with associated data bound to `(segment, generation)`).
329
+ */
330
+ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey, chunks: Iterable<{
331
+ chunkKey: number;
332
+ bitmap: CodecBitmap;
333
+ }>, options?: {
334
+ crypto?: CrbmCrypto;
335
+ clock?: Yielder;
336
+ }): Promise<{
337
+ size: number;
338
+ sha256: string;
339
+ }>;
340
+ /** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
341
+ export interface StreamWriteResult {
342
+ readonly size: number;
343
+ readonly sha256: string;
344
+ /** Non-empty chunk keys written, in ascending order (for the post-write verify, since the stream is consumed). */
345
+ readonly chunkKeys: number[];
346
+ /** Total ids written. */
347
+ readonly cardinality: number;
348
+ }
349
+ /**
350
+ * Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
351
+ * sorted load): consumes an **already-ascending** async stream of `{ chunkKey, bitmap }`, feeding each to the
352
+ * codec and freeing it, instead of materializing the whole generation. Paired with a streaming storage sink (S3
353
+ * multipart / LocalFs temp file), peak memory is ~one chunk + one part. Returns a tally so the caller can verify
354
+ * the re-opened object without re-iterating the (now-consumed) stream. Input **must** be ascending by `chunkKey`
355
+ * — the codec rejects an out-of-order chunk; empty bitmaps are skipped.
356
+ */
357
+ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: GenKey, chunks: AsyncIterable<{
358
+ chunkKey: number;
359
+ bitmap: CodecBitmap;
360
+ }>, options?: {
361
+ crypto?: CrbmCrypto;
362
+ clock?: Yielder;
363
+ }): Promise<StreamWriteResult>;
364
+ /**
365
+ * Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
366
+ * generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
367
+ * if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
368
+ * out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
369
+ * retrying a few times under contention. Separated from the Storage write so callers can publish atomically
370
+ * after the immutable object is durable (write-then-publish).
371
+ *
372
+ * **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
373
+ * recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
374
+ * under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
375
+ * the row advertise encryption over cleartext generations. Contention that never clears throws
376
+ * {@link WriteConflictError} after the retries.
377
+ *
378
+ * **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
379
+ * content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
380
+ * generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
381
+ * from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
382
+ * it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
383
+ * publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
384
+ * report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
385
+ * so the check and the write see the same row.
386
+ *
387
+ * **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
388
+ * generation only within one incarnation of a name: `nextGeneration` restarts at `0` once the row is purged and
389
+ * the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
390
+ * `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
391
+ * over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
392
+ * OCC token is the identity that survives this: {@link IRegistryDriver.delete} tombstones rather than unlinks,
393
+ * so "a later `create` still gets a fresh, greater token" and no token is ever reused across incarnations.
394
+ *
395
+ * Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
396
+ * The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
397
+ * all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
398
+ * the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
399
+ */
400
+ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
401
+ wrappedDeks?: readonly WrappedDek[];
402
+ expectFrom?: number;
403
+ expectToken?: Token;
404
+ /**
405
+ * Publish only while the segment still has **no registry row**.
406
+ *
407
+ * The fence for a caller whose decision rests on the row being ABSENT. `expectFrom` and `expectToken`
408
+ * cannot express it: both compare against a value read from a row, so when there was no row there is
409
+ * nothing to compare and both are simply omitted — leaving the publish a bare forward-only advance that
410
+ * happily lands over whatever appeared in the meantime.
411
+ *
412
+ * That gap was a silent wipe, not a theoretical one. A guarded load into a segment that did not exist yet
413
+ * read "no row", so its empty/`minRetained` bounds had nothing to judge and passed vacuously; a
414
+ * concurrent writer then created the row and published a thousand ids; and the guarded load published an
415
+ * EMPTY generation over them, reporting `published: true` with no reason. Reproduced through both
416
+ * `loadSegment` and the `*Into` verbs, which hit it far more often because materialising into a
417
+ * destination that does not exist yet is the ordinary first run of a pipeline.
418
+ */
419
+ expectAbsent?: boolean;
420
+ }): Promise<boolean>;
421
+ /** What a bulk-load wrote — the driver's `{ size, sha256 }` plus a summary of the built generation. */
422
+ export interface BulkLoadResult {
423
+ /** Bytes written to the Storage object. */
424
+ readonly size: number;
425
+ /** The driver's content hash of the object. */
426
+ readonly sha256: string;
427
+ /** Distinct non-empty chunks written (≤ 65536). */
428
+ readonly chunkCount: number;
429
+ /**
430
+ * The DEK freshly minted for this generation, wrapped — present only when the segment is encrypted AND this
431
+ * call minted one (an existing segment reuses its key, which is already on the row).
432
+ *
433
+ * Returned so a caller that deliberately does **not** pass `registry` here — writing now and publishing later,
434
+ * which is how a load applies a guard while the old generation is still authoritative — can carry the key onto
435
+ * its own `publishGeneration`. Without it that publish stores no DEK and the generation it makes current is
436
+ * unreadable: the bytes are encrypted with a key nothing recorded.
437
+ */
438
+ readonly wrappedDeks?: readonly WrappedDek[];
439
+ /** Total distinct ids in the generation (post-dedup). */
440
+ readonly cardinality: number;
441
+ /**
442
+ * Whether this generation is now the segment's **current** one — `undefined` when no `registry` was wired, so
443
+ * there is no pointer and no publish step to report on.
444
+ *
445
+ * `false` means the object is durable but a **concurrent writer published a higher generation first**, so this
446
+ * one is an orphan that no reader will ever resolve. The write succeeded and the *load* did not: whatever this
447
+ * call was asked to make the segment contain, the segment does not contain. That is not a detail a caller can
448
+ * be left to infer — a `*Into` materialisation that reported the generation it wrote as "the destination's new
449
+ * current generation" on this path would state a plain untruth — so the flag is on the result and the verbs that
450
+ * promise a published generation check it.
451
+ */
452
+ readonly becameCurrent?: boolean;
453
+ }
454
+ /**
455
+ * **Bulk-load** a whole immutable generation from a flat id source — the batch "seed/rebuild" entry point.
456
+ * Unlike {@link writeCrbmGeneration} (which takes pre-grouped bitmaps), this
457
+ * consumes an arbitrary, **unsorted** stream of u32 ids (sync or async) and routes each into its chunk's
458
+ * Roaring bitmap as it goes. The input is consumed lazily and ids dedup on insert, so memory is bounded by
459
+ * the **built generation** — the in-memory Roaring representation of the *distinct* set (≤ one bitmap per
460
+ * non-empty chunk) — not by the input length: you can stream a billion duplicate-heavy ids and hold only the
461
+ * distinct result. Note this is `O(distinct set)`, **not** window-bounded like {@link SegmentEngine.intersect}:
462
+ * bulk-load holds the whole generation in RAM, which suits batch seed/rebuild jobs but not an
463
+ * unbounded-cardinality stream (a segment larger than RAM needs external-merge / pre-sorted input — later phase).
464
+ *
465
+ * Each id must be an integer in `[0, 2³²)` ({@link splitId} throws {@link ValidationError} otherwise) — the
466
+ * source is consumed lazily, so a bad id aborts mid-stream without writing a partial object (the driver
467
+ * commits only after the callback resolves). An empty source writes a valid empty generation.
468
+ *
469
+ * Writes a fresh full snapshot of a segment. The caller picks the generation number in `key` — `nextGeneration`
470
+ * computes the right one from the registry and the bucket — and re-using an existing generation throws
471
+ * {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
472
+ * generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
473
+ */
474
+ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number>, options?: {
475
+ registry?: IRegistryDriver;
476
+ keystore?: IKeystore;
477
+ requireEncryption?: boolean;
478
+ audit?: IAuditSink;
479
+ /** Bitmap codec. Optional in the type; a **flavor** package binds it ({@link requireCodec}). */
480
+ codec?: CodecInterface;
481
+ /**
482
+ * Injected clock. Supplying one makes bulk-load **cooperative**: it yields the event loop periodically so a
483
+ * long load does not stall everything else on the process. Without it the load still completes, just
484
+ * without yielding — which is the pre-existing behaviour, kept so this is purely additive.
485
+ *
486
+ * `@cloudbitmaps/roaring` supplies a real clock by default, so flavor users get cooperative behaviour with
487
+ * no wiring. `core/` cannot default it: it is timer-free by lint, which is exactly why waiting goes through
488
+ * this seam rather than `setTimeout`.
489
+ */
490
+ clock?: Clock;
491
+ /**
492
+ * Write the object but do **not** advance the pointer (default: publish when a `registry` is wired).
493
+ *
494
+ * For a caller that has to inspect what it wrote before deciding whether it should become current — a
495
+ * guarded {@link loadSegment} is the one in-tree case — because the check is only meaningful while the old
496
+ * generation is still authoritative. The `registry` is still required for an encrypted segment and still
497
+ * consulted: it is where an existing segment's DEK lives, and reusing that key is not optional. The wrapped
498
+ * DEK comes back on the result so the deferred publish can store it.
499
+ *
500
+ * A caller that defers the publish owns what it wrote: an unpublished object sits ABOVE `currentGen`, where
501
+ * generation collection deliberately never looks, so nothing reclaims it until a later generation above it is
502
+ * current.
503
+ */
504
+ publish?: boolean;
505
+ }): Promise<BulkLoadResult>;
506
+ /**
507
+ * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
508
+ * The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
509
+ * generation; the engine's read path goes through {@link CrbmStorageChunkSource} instead, which caches these.
510
+ */
511
+ export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
512
+ /**
513
+ * Re-open a freshly written generation and assert it round-trips exactly what was streamed into it: the same
514
+ * per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks).
515
+ * `expected` is the streaming writer's tally (the stream is consumed, so it can't be re-iterated) — the key-set
516
+ * comparison catches a dropped/extra chunk that a cardinality-only check could miss when two errors cancel out.
517
+ * Throws {@link IntegrityError}: the object is on disk but must not be published.
518
+ */
519
+ export declare function verifyGeneration(storage: IStorageDriver, key: GenKey, expected: {
520
+ readonly chunkKeys: readonly number[];
521
+ readonly cardinality: number;
522
+ }, crypto: CrbmCrypto | undefined): Promise<void>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Encryption seams — the **pure** interfaces `core/` depends on so the codec can encrypt/decrypt without
3
+ * importing `node:crypto` (lint-banned here, like `Clock`/`Rng`/drivers). Concrete AES-256-GCM lives outside
4
+ * `core/` (`src/drivers/crypto.ts`) and is injected. The envelope scheme: a per-segment DEK wrapped under an
5
+ * operator-held KEK, so discarding the DEK crypto-shreds the segment. Per-chunk AEAD over the `.crbm` payloads + index; a per-segment **DEK** wrapped (envelope
6
+ * encryption) under one or more **KEK**s by an {@link IKeystore}; the wrapped DEK(s) live in the registry, so
7
+ * crypto-shred = delete them.
8
+ */
9
+ /** One AEAD result: a fresh per-message nonce, the ciphertext, and the authentication tag. */
10
+ export interface AeadSealed {
11
+ readonly nonce: Uint8Array;
12
+ readonly ciphertext: Uint8Array;
13
+ readonly tag: Uint8Array;
14
+ }
15
+ /**
16
+ * Authenticated encryption bound to a single key (a segment's DEK). `aad` (associated data) is authenticated
17
+ * but not encrypted — the codec passes each chunk's `(namespace, segment, generation, chunkKey)` identity
18
+ * (the library derives it; an `Aead` implementation receives it), so a chunk's ciphertext can't be silently
19
+ * relocated to another segment/generation/chunk or
20
+ * the index, even by someone who can rewrite Storage objects. The seam is symmetric and self-framing-agnostic:
21
+ * the caller decides where the nonce/tag go (inline with a chunk, or in the footer for the index).
22
+ */
23
+ export interface Aead {
24
+ /** Encrypt `plaintext`, authenticating `aad`. Returns a fresh nonce + ciphertext + tag. */
25
+ seal(plaintext: Uint8Array, aad: Uint8Array): AeadSealed;
26
+ /**
27
+ * Decrypt + verify. Throws {@link IntegrityError} on **any** mismatch (wrong key, tampered ciphertext/tag,
28
+ * or `aad` that doesn't match what was sealed) — never returns wrong-but-plausible plaintext.
29
+ */
30
+ open(sealed: AeadSealed, aad: Uint8Array): Uint8Array;
31
+ }
32
+ /**
33
+ * A DEK wrapped under one KEK. A segment stores a **list** of these (one per KEK it was wrapped under — e.g.
34
+ * an active KEK plus an offline recovery KEK), so losing one KEK isn't fatal and KEK rotation needs no
35
+ * data re-encryption. `keyId` names which KEK; `wrapped` is the opaque wrapped-key blob (base64). Carries no
36
+ * plaintext key material — safe to store in the registry next to the (encrypted) data.
37
+ */
38
+ export interface WrappedDek {
39
+ readonly keyId: string;
40
+ readonly wrapped: string;
41
+ }
42
+ /**
43
+ * Envelope key management — the injected seam between the codec and however the operator holds key material
44
+ * (in-process BYOK by default; KMS/Vault as optional adapters later). Async so a remote keystore (KMS) fits
45
+ * the same interface. Never exposes raw key bytes to `core/`: it returns an {@link Aead} bound to the DEK.
46
+ */
47
+ export interface IKeystore {
48
+ /**
49
+ * Mint a fresh per-segment DEK, wrapped under the active KEK (and any recovery KEK). Returns the wrapping(s)
50
+ * to persist in the registry plus an {@link Aead} to encrypt this segment's chunks with.
51
+ */
52
+ createDek(): Promise<{
53
+ wrapped: WrappedDek[];
54
+ aead: Aead;
55
+ }>;
56
+ /**
57
+ * Reconstruct the {@link Aead} from a segment's stored wrappings by unwrapping with any KEK currently held.
58
+ * Throws {@link KeyUnavailableError} if it holds none of the referenced KEKs (lost key, or a shredded
59
+ * segment whose wrappings were deleted).
60
+ */
61
+ openDek(wrapped: readonly WrappedDek[]): Promise<Aead>;
62
+ }
63
+ /**
64
+ * What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
65
+ * given scope (`'index'` or a chunkKey). Built per (segment, generation) by the storage-source bridge so the
66
+ * codec itself stays segment-agnostic.
67
+ */
68
+ export interface CrbmCrypto {
69
+ readonly aead: Aead;
70
+ aadFor(scope: number | 'index'): Uint8Array;
71
+ }
72
+ /**
73
+ * Build the AEAD associated-data binding a chunk (or the index) to its exact location:
74
+ * `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`. Length-prefixed
75
+ * so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the authenticated label.
76
+ */
77
+ export declare function aadFor(ref: {
78
+ readonly namespace?: string;
79
+ readonly segment: string;
80
+ }, generation: number, scope: number | 'index'): Uint8Array;