@cloudbitmaps/core 0.11.2 → 0.12.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 (42) hide show
  1. package/dist/{chunk-5OMC5U7U.js → chunk-5W3XS2IU.js} +292 -22
  2. package/dist/chunk-5W3XS2IU.js.map +7 -0
  3. package/dist/core/audit.d.ts +6 -0
  4. package/dist/core/codec.d.ts +33 -1
  5. package/dist/core/consistency.d.ts +21 -2
  6. package/dist/core/cost.d.ts +91 -10
  7. package/dist/core/crbm/format.d.ts +31 -1
  8. package/dist/core/crbm/reader.d.ts +44 -2
  9. package/dist/core/crbm/writer.d.ts +32 -3
  10. package/dist/core/crbm-storage-source.d.ts +203 -22
  11. package/dist/core/crypto.d.ts +11 -7
  12. package/dist/core/determinism.d.ts +7 -0
  13. package/dist/core/engine.d.ts +17 -5
  14. package/dist/core/erase-id.d.ts +20 -0
  15. package/dist/core/erasure.d.ts +8 -4
  16. package/dist/core/generation-gc.d.ts +102 -4
  17. package/dist/core/load-input.d.ts +55 -0
  18. package/dist/core/load.d.ts +65 -10
  19. package/dist/core/metadata.d.ts +42 -0
  20. package/dist/core/pinned-storage-source.d.ts +2 -1
  21. package/dist/core/ports.d.ts +119 -13
  22. package/dist/core/registry-scan.d.ts +6 -0
  23. package/dist/core/retention-sweep.d.ts +47 -2
  24. package/dist/core/retry.d.ts +14 -0
  25. package/dist/core/rollback.d.ts +34 -4
  26. package/dist/core/summary.d.ts +61 -0
  27. package/dist/core/token.d.ts +36 -0
  28. package/dist/core/validate.d.ts +2 -0
  29. package/dist/driver-kit.js +39 -20
  30. package/dist/driver-kit.js.map +2 -2
  31. package/dist/drivers/_shared/entropy.d.ts +5 -0
  32. package/dist/drivers/_shared/object-registry.d.ts +36 -6
  33. package/dist/drivers/_shared/registry.d.ts +47 -19
  34. package/dist/drivers/localfs/registry.d.ts +10 -0
  35. package/dist/drivers/memory.d.ts +9 -3
  36. package/dist/drivers/retry/retrying-chunk-source.d.ts +2 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +1794 -461
  39. package/dist/index.js.map +4 -4
  40. package/dist/testing/fuzz-core.d.ts +6 -5
  41. package/package.json +1 -1
  42. package/dist/chunk-5OMC5U7U.js.map +0 -7
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Frozen `.crbm` v1.0 layout constants.
2
+ * Frozen `.crbm` v1 layout constants (format 1.0, with its optional extension block).
3
3
  *
4
4
  * These byte widths/offsets are pinned by the golden corpus and must never change for v1 —
5
5
  * a new layout is a new format version. All multi-byte integers are little-endian (v1 fixes LE).
@@ -41,6 +41,12 @@ export declare const FOOTER_CRC_COVERAGE: 96;
41
41
  export declare const FLAG_ENCRYPTED: number;
42
42
  export declare const FLAG_INDEX_COMPRESSED: number;
43
43
  export declare const FLAG_LITTLE_ENDIAN: number;
44
+ /**
45
+ * The object carries an extension block just before its index (a generation's metadata). Set only when there is
46
+ * something to put in it, so an object without metadata is the same bytes as before the block existed; a reader that
47
+ * does not know the bit refuses the object rather than reading past what it cannot see.
48
+ */
49
+ export declare const FLAG_EXTENSION: number;
44
50
  /** Flag bits a v1 reader understands; any bit outside this mask is an unsupported feature. */
45
51
  export declare const KNOWN_FLAGS: number;
46
52
  /**
@@ -102,6 +108,30 @@ export declare const DEFAULT_MAX_INDEX_BYTES: number;
102
108
  export declare const DEFAULT_MAX_PAYLOAD_BYTES: number;
103
109
  /** A chunk's cardinality is in `[1, 65536]` (empty chunks are never written). */
104
110
  export declare const MAX_CHUNK_CARDINALITY = 65536;
111
+ /**
112
+ * The extension block, present when the footer sets {@link FLAG_EXTENSION}: `section* ‖ u32 sectionsLength ‖ u32
113
+ * crc32c ‖ "CRBX"`, between the last payload and the index, so the index starts right after it. A reader finds it from
114
+ * `indexOffset`: the trailer is the {@link EXT_TRAILER_BYTES} bytes before the index, and the CRC32C covers the sections
115
+ * and the length field. Each section is `u8 type ‖ u32 length ‖ bytes`, in strictly ascending type order; a reader
116
+ * skips a type it does not know, so a section must be safe to ignore.
117
+ */
118
+ export declare const EXT_MAGIC: Uint8Array<ArrayBuffer>;
119
+ /** The trailer: the sections' length (u32), their CRC32C (u32) and {@link EXT_MAGIC}. */
120
+ export declare const EXT_TRAILER_BYTES = 12;
121
+ /** A section's header: its type (u8) and its length (u32). */
122
+ export declare const EXT_SECTION_HEADER_BYTES = 5;
123
+ /**
124
+ * Cap on the sections of one block, in bytes. A reader refuses a larger block from its trailer alone, before it reads
125
+ * any sections beyond what it already holds (the read that brings the index may already hold up to this much before
126
+ * it), and a writer of any 1.x minor keeps within it, so a later section type stays readable by this reader.
127
+ */
128
+ export declare const MAX_EXT_BYTES: number;
129
+ /**
130
+ * Section type 1: the generation's metadata, its canonical JSON in UTF-8. On an encrypted object the section is
131
+ * `nonce ‖ ciphertext ‖ tag`, sealed under the segment's key with the metadata scope of the associated data.
132
+ * Type 0 is not a section type: a reader refuses it.
133
+ */
134
+ export declare const EXT_SECTION_METADATA = 1;
105
135
  /** Fixed width of a per-chunk CRC32C field in the index (high-entropy → not varint). */
106
136
  export declare const CRC32C_BYTES = 4;
107
137
  /**
@@ -1,5 +1,6 @@
1
1
  import type { BlobReader } from '../blob.js';
2
2
  import type { CrbmCrypto } from '../crypto.js';
3
+ import type { GenerationMetadata } from '../ports.js';
3
4
  /**
4
5
  * A parsed index as parallel typed arrays, one slot per entry, ascending by key. Typed arrays hold each field at
5
6
  * its own width with no per-entry object, so the retained heap is the arrays' byte length and nothing else.
@@ -34,7 +35,11 @@ export interface CrbmReaderOptions {
34
35
  readonly maxPayloadBytes?: number;
35
36
  /** Hard cap on the whole index region fetched/parsed from one object (default 8 MB). */
36
37
  readonly maxIndexBytes?: number;
37
- /** Decryption context for an encrypted object (its DEK's AEAD + AAD builder). Required iff `FLAG_ENCRYPTED`. */
38
+ /**
39
+ * Decryption context for an encrypted object (its DEK's AEAD + AAD builder). Required iff the object's footer sets
40
+ * `FLAG_ENCRYPTED`: an encrypted object opened without one throws {@link ValidationError}, and an object that is
41
+ * not encrypted, opened with one, is refused with {@link IntegrityError}.
42
+ */
38
43
  readonly crypto?: CrbmCrypto;
39
44
  /**
40
45
  * Opaque marker for the *incarnation of the name* this object belongs to — the caller's own identity for the
@@ -44,6 +49,15 @@ export interface CrbmReaderOptions {
44
49
  */
45
50
  readonly lineage?: unknown;
46
51
  }
52
+ /** An object's fingerprint, from its size and its footer's CRC (see {@link CrbmReader.fingerprint}). */
53
+ export declare const fingerprintFor: (size: number, footerCrc: number) => string;
54
+ /**
55
+ * Whether the object behind `blob` says it is encrypted, from one tail read of a footer's worth, with no key and no
56
+ * index; a footer that fails its own checks throws, as an open would. For a caller that must not open an object with
57
+ * the key it holds: an erasure looking for an id in a cleartext object under an encrypted segment, and a rollback
58
+ * checking that its target is what the row says.
59
+ */
60
+ export declare function footerSaysEncrypted(blob: BlobReader): Promise<boolean>;
47
61
  export declare class CrbmReader {
48
62
  private readonly blob;
49
63
  private readonly objectSize;
@@ -52,12 +66,22 @@ export declare class CrbmReader {
52
66
  readonly lineage: unknown;
53
67
  readonly totalCardinality: number;
54
68
  private readonly index;
55
- /** True when `open()` satisfied the index from the tail GET alone (no second range read). */
69
+ /**
70
+ * True when `open()` satisfied the index, and any extension block, from the tail GET alone (no second range
71
+ * read).
72
+ */
56
73
  readonly servedFromTail: boolean;
57
74
  /** Set iff the object is encrypted — used to decrypt each chunk payload in {@link getChunk}. */
58
75
  private readonly crypto;
59
76
  /** The footer's own CRC, as verified at open: it covers the index's CRC, the chunk count and the generation. */
60
77
  private readonly footerCrc;
78
+ /**
79
+ * The metadata the generation was written with, checked and decrypted at open; `undefined` when it has none
80
+ * (every format 1.0 object). Frozen: every caller shares one copy.
81
+ */
82
+ readonly metadata: GenerationMetadata | undefined;
83
+ /** What that metadata adds to {@link retainedBytes}; 0 when there is none. */
84
+ private readonly metadataWeight;
61
85
  private constructor();
62
86
  /**
63
87
  * Which object this reader opened, as its size and its footer's CRC. The footer covers the index's CRC, so two
@@ -73,6 +97,13 @@ export declare class CrbmReader {
73
97
  * arrays that hold it, {@link RETAINED_BYTES_PER_INDEX_ENTRY} per entry.
74
98
  */
75
99
  get retainedIndexBytes(): number;
100
+ /**
101
+ * What this reader holds, the weight the storage reader cache bounds on: its parsed index
102
+ * ({@link retainedIndexBytes}), plus a weight for the metadata it decoded, from its canonical length and its number
103
+ * of keys. Equal to {@link retainedIndexBytes} for a generation with no metadata. A cache heuristic, not a contract:
104
+ * how the metadata is weighed may change in any release.
105
+ */
106
+ get retainedBytes(): number;
76
107
  /** Per-chunk cardinality (`chunkKey → count`) from the parsed index — no payload reads. */
77
108
  cardinalities(): Map<number, number>;
78
109
  /**
@@ -100,4 +131,15 @@ export declare class CrbmReader {
100
131
  */
101
132
  getChunk(chunkKey: number): Promise<Uint8Array | null>;
102
133
  }
134
+ /**
135
+ * Read the sections of an extension block, its bytes before the trailer, once their CRC has passed. Each section is
136
+ * `u8 type ‖ u32 length ‖ bytes`, in strictly ascending type order, and together they fill the region exactly. Type 1
137
+ * is the generation's metadata, held to the metadata rules and its canonical form (and opened first with `crypto`,
138
+ * under the metadata scope, when the object is encrypted); a type this build does not know is a later build's, and
139
+ * is skipped. Returns the metadata, or `undefined` when the block has none. Every other shape is an
140
+ * {@link IntegrityError}.
141
+ *
142
+ * Exported for the coverage-guided fuzz harness, which drives it directly, past the block's CRC; not public API.
143
+ */
144
+ export declare function parseExtension(sections: Uint8Array, crypto: CrbmCrypto | undefined): GenerationMetadata | undefined;
103
145
  export declare function parseIndex(indexBytes: Uint8Array, payloadEnd: number, maxPayloadBytes: number, minPayloadBytes?: number): ParsedIndex;
@@ -1,4 +1,5 @@
1
1
  import type { CrbmCrypto } from '../crypto.js';
2
+ import type { GenerationMetadata } from '../ports.js';
2
3
  import type { BlobSink } from '../blob.js';
3
4
  export interface CrbmWriterOptions {
4
5
  /** Self-describing generation number (also encoded in the object key). */
@@ -13,6 +14,13 @@ export interface CrbmWriterOptions {
13
14
  readonly elementWidth?: number;
14
15
  /** When set, payloads + index are AES-256-GCM-encrypted, bound to `(segment, generation, scope)` via AAD. */
15
16
  readonly crypto?: CrbmCrypto;
17
+ /**
18
+ * The generation's metadata, checked against the metadata rules and copied as its canonical JSON when the writer
19
+ * is constructed, so a later change to the caller's object never reaches the object. A value that breaks a rule
20
+ * throws {@link ValidationError} from the constructor, before a byte is written. None, or the empty object, writes
21
+ * format 1.0.
22
+ */
23
+ readonly metadata?: GenerationMetadata;
16
24
  }
17
25
  export declare class CrbmWriter {
18
26
  private readonly sink;
@@ -23,17 +31,38 @@ export declare class CrbmWriter {
23
31
  private totalCardinality;
24
32
  private preambleWritten;
25
33
  private finished;
34
+ /** The metadata's canonical JSON, copied at construction; `undefined` leaves the footer's extension flag clear. */
35
+ private readonly metadata;
36
+ /** Bytes written so far, the footer included once {@link finish} has run. */
37
+ private written;
38
+ private footerCrc32c;
26
39
  constructor(sink: BlobSink, options: CrbmWriterOptions);
27
40
  /**
28
41
  * Append one chunk's payload (the roaring-serialized bytes) with its cardinality. `chunkKey` must be
29
42
  * strictly greater than the previous one (ascending, no duplicates); `cardinality` is `[1, 65536]`.
30
43
  */
31
44
  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. */
45
+ /**
46
+ * Write the extension block (when there is metadata), the index region and the fixed footer. After this the object
47
+ * is complete and immutable.
48
+ */
35
49
  finish(): Promise<void>;
50
+ /**
51
+ * The finished object's size and its footer's CRC: what names it, as a reader's fingerprint does. A writer that
52
+ * has to tell its own object from another stored later under the same key compares these with the object's
53
+ * footer. Throws before {@link finish}.
54
+ */
55
+ get identity(): {
56
+ readonly size: number;
57
+ readonly footerCrc: number;
58
+ };
36
59
  private ensurePreamble;
60
+ /**
61
+ * The extension block: one metadata section (`u8 type ‖ u32 length ‖ bytes`), then the trailer
62
+ * (`u32 sectionsLength ‖ u32 crc32c(sections ‖ sectionsLength) ‖ "CRBX"`). Encrypted, the section holds
63
+ * `nonce ‖ ciphertext ‖ tag`, sealed with the metadata scope of the associated data.
64
+ */
65
+ private extensionBlock;
37
66
  private encodeIndex;
38
67
  private buildFooter;
39
68
  }
@@ -1,11 +1,12 @@
1
1
  import { type IAuditSink } from './audit.js';
2
2
  import type { Yielder } from './cooperative.js';
3
- import type { Clock } from './determinism.js';
3
+ import type { Clock, Rng } from './determinism.js';
4
4
  import type { CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
5
- import type { ChunkRef, StorageChunkSource, GenKey, IStorageDriver, IRegistryDriver, SegmentRef, SegmentSize, Token } from './ports.js';
5
+ import type { ChunkRef, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
6
6
  import { CrbmReader } from './crbm/reader.js';
7
7
  import type { CrbmReaderOptions } from './crbm/reader.js';
8
8
  import type { CodecBitmap, CodecInterface } from './codec.js';
9
+ import { DecodedLoadInput } from './load-input.js';
9
10
  export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
10
11
  /**
11
12
  * Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
@@ -57,8 +58,8 @@ export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
57
58
  * Aggregate byte ceiling on the parsed `.crbm` indices resident in the reader cache (default 64 MiB) — the
58
59
  * **second half of that memory bound**. `maxOpenSegments` alone bounds by *count*, but a wide/dense segment's
59
60
  * parsed index can reach about 1.3 MB, so 1024 wide indices could pin over a GB 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
61
+ * Lambda) while the count is nominally "in bounds". This caps the summed {@link CrbmReader.retainedBytes} (the
62
+ * parsed index, and any metadata a generation carries) across cached readers; the least-recently-used reader is evicted once the total would exceed it — whichever
62
63
  * of the count/byte bounds binds first. Lower it for memory-tight deployments with wide segments; a single
63
64
  * segment whose index alone exceeds the budget is still cached (it can't be shrunk) but nothing else alongside.
64
65
  */
@@ -110,6 +111,13 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
110
111
  * check again.
111
112
  */
112
113
  private invalidations;
114
+ /**
115
+ * Generations whose row summary this process found to disagree with their object, by segment and generation. A
116
+ * summary here is not used again: a count of that generation reads the object. Bounded as the readers are, so a
117
+ * long-lived process cannot grow it without limit; one that falls out is trusted again until its next open finds
118
+ * the disagreement again.
119
+ */
120
+ private readonly distrusted;
113
121
  private readonly registry;
114
122
  private readonly keystore;
115
123
  private readonly requireEncryption;
@@ -117,7 +125,12 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
117
125
  private readonly clock;
118
126
  private readonly currentGenTtlMs;
119
127
  constructor(driver: IStorageDriver, options?: CrbmStorageChunkSourceOptions);
120
- /** The current resolved reader for a segment, refreshing on the TTL. Cheap within the TTL window. */
128
+ /**
129
+ * The segment's live snapshot, refreshing on the TTL. Cheap within the TTL window. The snapshot holds the resolved
130
+ * target, and its reader once a read has asked for one.
131
+ */
132
+ private liveSnapshot;
133
+ /** The current resolved reader for a segment, refreshing on the TTL. */
121
134
  private resolvedReader;
122
135
  /**
123
136
  * A refresh failed transiently and the prior reader keeps serving: make this snapshot lapse after
@@ -125,12 +138,16 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
125
138
  * that asks the registry again. It costs no read of its own; it only changes when the next refresh happens.
126
139
  */
127
140
  private retrySoon;
128
- /** TTL elapsed: cheaply re-resolve `currentGen`; reopen only if it actually changed, else reuse the prior reader. */
129
- private computeRefreshedReader;
130
141
  /**
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.
142
+ * TTL elapsed: cheaply re-resolve the pointer. The row read is always fresh, so what the new snapshot says of the
143
+ * generation (its summary included) is what the row says now, never the prior snapshot's. The reader is reopened
144
+ * only if the generation or the row changed, and only when a read asks for it.
145
+ */
146
+ private refreshedTarget;
147
+ /**
148
+ * Memoize a snapshot as the segment's at `now()`, forgetting it later if it resolves to null / throws
149
+ * (identity-guarded, so a stale cleanup never clobbers a fresher snapshot). Installing the (pending) snapshot
150
+ * **synchronously** is what lets concurrent callers coalesce onto one in-flight resolve.
134
151
  */
135
152
  private install;
136
153
  private now;
@@ -273,26 +290,51 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
273
290
  */
274
291
  exists(ref: SegmentRef): Promise<boolean>;
275
292
  /**
276
- * Evict a snapshot we just failed to read from — matched by its reader *promise*, so a concurrent call's
293
+ * Evict a snapshot we just failed to read from — matched by the snapshot itself, so a concurrent call's
277
294
  * fresher snapshot is never clobbered. The identity guard is the whole point: `install` replaces the entry
278
295
  * wholesale, so comparing anything else would drop a good snapshot on the floor.
279
296
  */
280
297
  private dropStale;
281
- private resolveLatest;
282
298
  /** Open a {@link CrbmReader} for an already-resolved generation target (decrypting if the segment is encrypted). */
283
299
  private openForTarget;
300
+ private openGeneration;
301
+ /**
302
+ * Open the generation a resolution found, and hold the row's summary against the object while it is open anyway:
303
+ * the same count and metadata, or the summary is not believed again in this process ({@link distrusted}). This
304
+ * costs no request; a mismatch is not an error, since the object is the truth and this read has it.
305
+ */
306
+ private openLive;
307
+ private distrustKey;
308
+ /** What the row says of the generation, unless it is a summary this process has found to disagree with its object. */
309
+ private trustedSummary;
284
310
  /**
285
311
  * The current generation + its DEK wrappings: the registry's authoritative record (one strong read), or a
286
312
  * `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).
313
+ * — there's nowhere a wrapped DEK could live). The row's summary rides along, to be used or not by whoever
314
+ * asks.
288
315
  */
289
- private resolveTarget;
316
+ private resolveLive;
290
317
  /** Build the per-generation decryption context for an encrypted segment (undefined for cleartext). */
291
318
  private cryptoForRead;
292
319
  getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
293
320
  listChunkKeys(ref: SegmentRef): Promise<number[]>;
294
321
  sizeOf(ref: SegmentRef): Promise<SegmentSize | null>;
295
322
  cardinalities(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
323
+ /**
324
+ * The current generation's number, id count and metadata. The answer comes from the row the snapshot resolved when
325
+ * it can: the row's summary of the generation it names, on an active row, in the shape the row's keys call for
326
+ * (a sealed one is opened with the segment's key), and one this process has not found to disagree with its
327
+ * object. That is one registry read when the snapshot is cold, none when it is warm, and no read of the object.
328
+ * With no summary it can use (an older row, one for another generation, one that does not open), it opens the
329
+ * generation and answers from the object. A row that names an object that is gone still answers, while a read
330
+ * of the object throws: the number is true of the generation the row names.
331
+ *
332
+ * What the row says is not confirmed here: a summary edited to agree with itself by whoever can write the row is
333
+ * believed until the next time the object is opened, which holds it against the summary.
334
+ */
335
+ summary(ref: SegmentRef): Promise<GenerationSummary | null>;
336
+ /** {@link summary} of a specific generation, from the object a pin holds. `held` as for {@link getChunkAt}. */
337
+ summaryAt(ref: SegmentRef, generation: number, held?: PinnedObject): Promise<GenerationSummary | null>;
296
338
  /**
297
339
  * Run `read` against the pinned snapshot, healing the torn-read window generation GC can open: if the
298
340
  * generation we resolved was superseded *and* swept (the grace window elapsed), the Storage driver throws
@@ -320,6 +362,17 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
320
362
  * `intersect` would do it N times. Gated in `tests/core/storage-source-heal-open.test.ts` by counting calls.
321
363
  */
322
364
  private withFreshSnapshot;
365
+ /**
366
+ * Whether `err`, raised by a read through the snapshot's reader, came from an object that is no longer the one the
367
+ * reader opened. A generation number can be taken again once its object is deleted (an erasure removes the
368
+ * generations above a rolled-back pointer that held the id, a collection pass can stop part-way, and a load then
369
+ * numbers `currentGen + 1` again), so a reader that still holds the old object's index reads the new object's
370
+ * bytes at the old offsets: a chunk checksum fails, or a range runs past the end. The object's footer says which it
371
+ * was, as it does for a pin ({@link CrbmReader.sameObject}): replaced, or gone, re-resolves; the same object means
372
+ * the error is real, and it stands. Only these two errors pay for the footer read, never a read that succeeded. A
373
+ * transient fault in the check is rethrown; a footer that cannot be read says nothing, and the error stands.
374
+ */
375
+ private replacedUnder;
323
376
  }
324
377
  /**
325
378
  * Write one immutable generation from in-memory bitmaps. Chunks are
@@ -333,9 +386,30 @@ export declare function writeCrbmGeneration(driver: IStorageDriver, key: GenKey,
333
386
  }>, options?: {
334
387
  crypto?: CrbmCrypto;
335
388
  clock?: Yielder;
389
+ metadata?: GenerationMetadata;
336
390
  }): Promise<{
337
391
  size: number;
338
392
  sha256: string;
393
+ fingerprint: string;
394
+ }>;
395
+ /**
396
+ * Whether the object under `key` is provably the one `fingerprint` names ({@link CrbmReader.fingerprint}), from one
397
+ * read of its footer. False when it is another object, when it is gone, and when the read fails in any way: a caller
398
+ * that deletes on `true` deletes nothing it cannot prove is its own.
399
+ */
400
+ export declare function holdsObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
401
+ /** Whether the object under `key` says it is encrypted, from one read of its footer, with no key. */
402
+ export declare function objectIsEncrypted(storage: IStorageDriver, key: GenKey): Promise<boolean>;
403
+ /**
404
+ * What one tail read of the object under `key` tells a rollback about it: whether the footer says it is encrypted, and,
405
+ * when the object and the key agree (an encrypted object opened with `crypto`, a cleartext one with none), the reader
406
+ * the open gives, which holds its count and metadata. No reader when they disagree, which the caller refuses as a
407
+ * target the row cannot use, and none for an encrypted object when the caller has no key. A footer that fails its own
408
+ * checks, an object another generation's, and an index or metadata that do not open are an {@link IntegrityError}.
409
+ */
410
+ export declare function openRollbackTarget(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined): Promise<{
411
+ encrypted: boolean;
412
+ reader: CrbmReader | undefined;
339
413
  }>;
340
414
  /** What {@link writeCrbmGenerationStream} wrote: the driver's `{ size, sha256 }` + a tally of the generation. */
341
415
  export interface StreamWriteResult {
@@ -345,6 +419,8 @@ export interface StreamWriteResult {
345
419
  readonly chunkKeys: number[];
346
420
  /** Total ids written. */
347
421
  readonly cardinality: number;
422
+ /** The fingerprint of what this call wrote ({@link CrbmReader.fingerprint}), which tells it from another object under its key. */
423
+ readonly fingerprint: string;
348
424
  }
349
425
  /**
350
426
  * Streaming variant of {@link writeCrbmGeneration} for a **constant-memory** rewrite (the erasure rewrite, a
@@ -360,6 +436,7 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
360
436
  }>, options?: {
361
437
  crypto?: CrbmCrypto;
362
438
  clock?: Yielder;
439
+ metadata?: GenerationMetadata;
363
440
  }): Promise<StreamWriteResult>;
364
441
  /**
365
442
  * Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
@@ -385,17 +462,43 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
385
462
  * so the check and the write see the same row.
386
463
  *
387
464
  * **`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
465
+ * generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
389
466
  * the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
390
467
  * `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
391
468
  * 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.
469
+ * OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
470
+ * under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
394
471
  *
395
472
  * Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
396
473
  * The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
397
474
  * all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
398
475
  * the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
476
+ *
477
+ * **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
478
+ * `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
479
+ * have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
480
+ *
481
+ * - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and
482
+ * `holdsOwnObject` proves the object under it the caller's: the write landed, and the publish returns `true`. Only
483
+ * the writer of a write-once object publishes its number, and a number is never taken while an object holds it;
484
+ * - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
485
+ * publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
486
+ * own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
487
+ * lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
488
+ * a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
489
+ * unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
490
+ * A caller must not delete its object on it;
491
+ * - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
492
+ * the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
493
+ * the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
494
+ * current re-publish.
495
+ *
496
+ * A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
497
+ * so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
498
+ *
499
+ * **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
500
+ * sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
501
+ * keys call for: sealed for an object written with a key, clear for one written without.
399
502
  */
400
503
  export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
401
504
  wrappedDeks?: readonly WrappedDek[];
@@ -417,7 +520,55 @@ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey
417
520
  * destination that does not exist yet is the ordinary first run of a pipeline.
418
521
  */
419
522
  expectAbsent?: boolean;
523
+ /**
524
+ * The generation's summary (see {@link RegistrySummary}), written by the write that moves the pointer to it. It
525
+ * names `key.generation`. Absent, the row is left with none for this generation.
526
+ */
527
+ summary?: RegistrySummary;
528
+ /**
529
+ * The segment's row as the caller already read it (`null`: it found none). The first attempt acts on it
530
+ * instead of reading the row again, which is sound because that attempt writes only under the row's own
531
+ * fence: its compare-and-swap carries this row's token, and with no row it creates create-only, so a row
532
+ * that changed or appeared since makes the write lose and the next attempt reads the row afresh. Only the
533
+ * idempotent "already current" answer writes nothing, so a reused row never gives it.
534
+ */
535
+ row?: RegistryRecord | null;
536
+ /**
537
+ * The object was written without encryption. Its publish is refused, with {@link KeyUnavailableError}, onto a row
538
+ * that carries key material: a reader of that segment opens every generation with its key, and a later
539
+ * `destroySegment` would attest that shredding it made a readable object unreadable. A caller decides its key
540
+ * from the row it read before the write, so this closes the window in which another writer creates the
541
+ * segment encrypted meanwhile.
542
+ */
543
+ cleartext?: boolean;
544
+ /**
545
+ * Whether the object under `key` is the one the caller wrote, and throws when it cannot tell. Asked only when a
546
+ * registry write failed and the row then names `key.generation`, to tell this publish's write from another
547
+ * writer's at the same number. Every caller that writes the object passes it. Without it, a write that ended
548
+ * without an answer over a row that names `key.generation` cannot be settled: the publish throws
549
+ * {@link TransientError} and the caller keeps its object.
550
+ */
551
+ holdsOwnObject?: () => Promise<boolean>;
552
+ /**
553
+ * What a publish waits on before it sends a fresh compare-and-swap, after a write that ended without an answer and
554
+ * left the row as it was. Without a `sleep`, such a write throws at once instead of being sent again.
555
+ */
556
+ clock?: Yielder;
557
+ /** Spreads that wait (full jitter) so concurrent publishers do not retry together. Without it the wait is the bound. */
558
+ rng?: Rng;
559
+ /**
560
+ * Called once for each registry write that ended without an answer, before the row is read back. A caller whose
561
+ * publish then answers `false` can say that it cannot tell whether its own write landed first: the row may have
562
+ * held its generation for a while before another writer moved on.
563
+ */
564
+ onUnanswered?: () => void;
420
565
  }): Promise<boolean>;
566
+ /**
567
+ * Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
568
+ * object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
569
+ * not look" for "not this object".
570
+ */
571
+ export declare function provesOwnObject(storage: IStorageDriver, key: GenKey, fingerprint: string): Promise<boolean>;
421
572
  /** What a bulk-load wrote — the driver's `{ size, sha256 }` plus a summary of the built generation. */
422
573
  export interface BulkLoadResult {
423
574
  /** Bytes written to the Storage object. */
@@ -436,8 +587,18 @@ export interface BulkLoadResult {
436
587
  * unreadable: the bytes are encrypted with a key nothing recorded.
437
588
  */
438
589
  readonly wrappedDeks?: readonly WrappedDek[];
590
+ /** Whether the object was written encrypted: under the segment's key, or under one this call minted. */
591
+ readonly encrypted: boolean;
592
+ /** The written object's fingerprint, which tells it from an object stored later under the same key. */
593
+ readonly fingerprint: string;
439
594
  /** Total distinct ids in the generation (post-dedup). */
440
595
  readonly cardinality: number;
596
+ /**
597
+ * The summary of what was written, for the write that makes it current: its id count and metadata, sealed under the
598
+ * segment's key when the object was written with one. A caller that defers the publish (`publish: false`) passes it
599
+ * to {@link publishGeneration}.
600
+ */
601
+ readonly summary: RegistrySummary;
441
602
  /**
442
603
  * Whether this generation is now the segment's **current** one — `undefined` when no `registry` was wired, so
443
604
  * there is no pointer and no publish step to report on.
@@ -467,11 +628,12 @@ export interface BulkLoadResult {
467
628
  * commits only after the callback resolves). An empty source writes a valid empty generation.
468
629
  *
469
630
  * 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
631
+ * computes one from the registry and the bucket, as `nextLoadGeneration` does for a load — and re-using an existing
632
+ * generation throws
471
633
  * {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
472
634
  * generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
473
635
  */
474
- export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number>, options?: {
636
+ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
475
637
  registry?: IRegistryDriver;
476
638
  keystore?: IKeystore;
477
639
  requireEncryption?: boolean;
@@ -502,6 +664,22 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
502
664
  * current.
503
665
  */
504
666
  publish?: boolean;
667
+ /**
668
+ * The generation's metadata (see {@link GenerationMetadata}), written into the object and carried in the summary.
669
+ * The caller has checked it against the metadata rules; the writer checks it again, and refuses a record that breaks
670
+ * one before a byte is written.
671
+ */
672
+ metadata?: GenerationMetadata;
673
+ /**
674
+ * The segment's row as the caller already read it (`null`: it found none), for a caller that defers the
675
+ * publish (`publish: false`) and fences it on that same row. A present cleartext row is then used as read,
676
+ * rather than read again: a row that changes in between (a publish, a drop, a purge and re-create) makes the
677
+ * fenced publish lose, so the object is never published on the strength of the stale read. A row read as
678
+ * absent, or one carrying key material, is read again after the ids, as with no row passed: a first load must
679
+ * see a row another writer created meanwhile, and an encrypted segment's key is unwrapped only from a row read
680
+ * after the ids, so a segment shredded while they streamed is refused before its key is used.
681
+ */
682
+ row?: RegistryRecord | null;
505
683
  }): Promise<BulkLoadResult>;
506
684
  /**
507
685
  * Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
@@ -511,12 +689,15 @@ export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenK
511
689
  export declare function openGenerationReader(storage: IStorageDriver, key: GenKey, crypto: CrbmCrypto | undefined, options?: Omit<CrbmReaderOptions, 'crypto'>): Promise<CrbmReader>;
512
690
  /**
513
691
  * 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.
692
+ * per-chunk key set *and* the same total cardinality (on top of the codec's own per-chunk CRC + footer checks), and the
693
+ * metadata it was given. `expected` is the streaming writer's tally (the stream is consumed, so it can't be
694
+ * re-iterated) — the key-set comparison catches a dropped/extra chunk that a cardinality-only check could miss when
695
+ * two errors cancel out. The count and the metadata are what the generation's row summary is about to claim, so the
696
+ * summary is held to the object it describes before it is published.
517
697
  * Throws {@link IntegrityError}: the object is on disk but must not be published.
518
698
  */
519
699
  export declare function verifyGeneration(storage: IStorageDriver, key: GenKey, expected: {
520
700
  readonly chunkKeys: readonly number[];
521
701
  readonly cardinality: number;
702
+ readonly metadata?: GenerationMetadata | undefined;
522
703
  }, crypto: CrbmCrypto | undefined): Promise<void>;
@@ -62,19 +62,23 @@ export interface IKeystore {
62
62
  }
63
63
  /**
64
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.
65
+ * given scope (a chunkKey, `'index'`, or `'metadata'` for the generation's metadata section). Built per
66
+ * (segment, generation) by the storage-source bridge so the codec itself stays segment-agnostic.
67
67
  */
68
68
  export interface CrbmCrypto {
69
69
  readonly aead: Aead;
70
- aadFor(scope: number | 'index'): Uint8Array;
70
+ aadFor(scope: number | 'index' | 'metadata'): Uint8Array;
71
71
  }
72
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.
73
+ * Build the AEAD associated-data binding a chunk, the index, the metadata section or a registry row's sealed summary
74
+ * to its exact location:
75
+ * `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`, where scope is
76
+ * `0` for a chunk, `1` for the index, `2` for the metadata and `3` for the row's summary, and chunkKey is `0` for
77
+ * the last three.
78
+ * Length-prefixed so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the
79
+ * authenticated label.
76
80
  */
77
81
  export declare function aadFor(ref: {
78
82
  readonly namespace?: string;
79
83
  readonly segment: string;
80
- }, generation: number, scope: number | 'index'): Uint8Array;
84
+ }, generation: number, scope: number | 'index' | 'metadata' | 'summary'): Uint8Array;
@@ -40,3 +40,10 @@ export interface Rng {
40
40
  /** A float in the half-open interval [0, 1). */
41
41
  next(): number;
42
42
  }
43
+ /**
44
+ * Injected entropy: `length` cryptographically strong random bytes, for an identity that must never repeat (a
45
+ * registry row's incarnation id, and the random part every registry write adds to its token). Deliberately not an
46
+ * {@link Rng}: an `Rng` is seedable for simulation, and a seeded source hands every process the same "random"
47
+ * identity. Production wiring uses the platform's Web Crypto; a test injects a deterministic source to replay a run.
48
+ */
49
+ export type Entropy = (length: number) => Uint8Array;