@cloudbitmaps/core 0.1.0-rc.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 (76) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/README.md +22 -0
  4. package/dist/azure/index.cjs +368 -0
  5. package/dist/azure/index.cjs.map +1 -0
  6. package/dist/azure/index.d.cts +48 -0
  7. package/dist/azure/index.d.ts +48 -0
  8. package/dist/azure/index.js +304 -0
  9. package/dist/azure/index.js.map +1 -0
  10. package/dist/cassandra/index.cjs +265 -0
  11. package/dist/cassandra/index.cjs.map +1 -0
  12. package/dist/cassandra/index.d.cts +52 -0
  13. package/dist/cassandra/index.d.ts +52 -0
  14. package/dist/cassandra/index.js +204 -0
  15. package/dist/cassandra/index.js.map +1 -0
  16. package/dist/chunk-2YDULGXS.js +203 -0
  17. package/dist/chunk-2YDULGXS.js.map +1 -0
  18. package/dist/chunk-7LMLYSVJ.js +43 -0
  19. package/dist/chunk-7LMLYSVJ.js.map +1 -0
  20. package/dist/chunk-AS6ODRLT.js +6 -0
  21. package/dist/chunk-AS6ODRLT.js.map +1 -0
  22. package/dist/chunk-NUIDEEFZ.js +91 -0
  23. package/dist/chunk-NUIDEEFZ.js.map +1 -0
  24. package/dist/chunk-SNJVZ227.js +35 -0
  25. package/dist/chunk-SNJVZ227.js.map +1 -0
  26. package/dist/dynamodb/index.cjs +731 -0
  27. package/dist/dynamodb/index.cjs.map +1 -0
  28. package/dist/dynamodb/index.d.cts +106 -0
  29. package/dist/dynamodb/index.d.ts +106 -0
  30. package/dist/dynamodb/index.js +460 -0
  31. package/dist/dynamodb/index.js.map +1 -0
  32. package/dist/gcs/index.cjs +343 -0
  33. package/dist/gcs/index.cjs.map +1 -0
  34. package/dist/gcs/index.d.cts +46 -0
  35. package/dist/gcs/index.d.ts +46 -0
  36. package/dist/gcs/index.js +279 -0
  37. package/dist/gcs/index.js.map +1 -0
  38. package/dist/index.cjs +4031 -0
  39. package/dist/index.cjs.map +1 -0
  40. package/dist/index.d.cts +1945 -0
  41. package/dist/index.d.ts +1945 -0
  42. package/dist/index.js +3642 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/mongodb/index.cjs +260 -0
  45. package/dist/mongodb/index.cjs.map +1 -0
  46. package/dist/mongodb/index.d.cts +45 -0
  47. package/dist/mongodb/index.d.ts +45 -0
  48. package/dist/mongodb/index.js +199 -0
  49. package/dist/mongodb/index.js.map +1 -0
  50. package/dist/mysql/index.cjs +281 -0
  51. package/dist/mysql/index.cjs.map +1 -0
  52. package/dist/mysql/index.d.cts +56 -0
  53. package/dist/mysql/index.d.ts +56 -0
  54. package/dist/mysql/index.js +211 -0
  55. package/dist/mysql/index.js.map +1 -0
  56. package/dist/ports-D3BrJ6ax.d.cts +357 -0
  57. package/dist/ports-D3BrJ6ax.d.ts +357 -0
  58. package/dist/postgres/index.cjs +273 -0
  59. package/dist/postgres/index.cjs.map +1 -0
  60. package/dist/postgres/index.d.cts +54 -0
  61. package/dist/postgres/index.d.ts +54 -0
  62. package/dist/postgres/index.js +203 -0
  63. package/dist/postgres/index.js.map +1 -0
  64. package/dist/redis/index.cjs +257 -0
  65. package/dist/redis/index.cjs.map +1 -0
  66. package/dist/redis/index.d.cts +42 -0
  67. package/dist/redis/index.d.ts +42 -0
  68. package/dist/redis/index.js +197 -0
  69. package/dist/redis/index.js.map +1 -0
  70. package/dist/s3/index.cjs +891 -0
  71. package/dist/s3/index.cjs.map +1 -0
  72. package/dist/s3/index.d.cts +112 -0
  73. package/dist/s3/index.d.ts +112 -0
  74. package/dist/s3/index.js +555 -0
  75. package/dist/s3/index.js.map +1 -0
  76. package/package.json +172 -0
@@ -0,0 +1,211 @@
1
+ import { NO_ROW } from '../chunk-AS6ODRLT.js';
2
+ import { ValidationError, validateChunkRef, namespacePart, WriteConflictError, isWriteConflictError, validateSegmentRef, IntegrityError, TransientError } from '../chunk-NUIDEEFZ.js';
3
+ import { randomUUID } from 'crypto';
4
+
5
+ // src/drivers/mysql/keys.ts
6
+ var IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
7
+ function validateAndQuoteTable(table) {
8
+ const parts = table.split(".");
9
+ if (parts.length > 2 || parts.some((p) => !IDENT.test(p))) {
10
+ throw new ValidationError(
11
+ `invalid table name ${JSON.stringify(table)} \u2014 expected an identifier or "db.table" (letters, digits, underscore; \u226464 chars; leading letter/underscore)`
12
+ );
13
+ }
14
+ return parts.map((p) => `\`${p}\``).join(".");
15
+ }
16
+ var MAX_KEY_PREFIX_CHARS = 191;
17
+ function normalizeKeyPrefix(prefix) {
18
+ if (prefix === void 0 || prefix === "") return "";
19
+ let chars = 0;
20
+ for (const ch of prefix) {
21
+ chars++;
22
+ if (ch.charCodeAt(0) < 32) {
23
+ throw new ValidationError("keyPrefix must not contain control characters");
24
+ }
25
+ }
26
+ if (chars > MAX_KEY_PREFIX_CHARS) {
27
+ throw new ValidationError(
28
+ `keyPrefix must be at most ${MAX_KEY_PREFIX_CHARS} characters (the key_prefix column width); got ${chars}`
29
+ );
30
+ }
31
+ return prefix;
32
+ }
33
+
34
+ // src/drivers/mysql/mysql-errors.ts
35
+ function errno(err) {
36
+ const n = err?.errno;
37
+ return typeof n === "number" ? n : void 0;
38
+ }
39
+ function code(err) {
40
+ const c = err?.code;
41
+ return typeof c === "string" ? c : void 0;
42
+ }
43
+ function isDuplicateKey(err) {
44
+ return errno(err) === 1062 || code(err) === "ER_DUP_ENTRY";
45
+ }
46
+ function isTransient(err) {
47
+ const n = errno(err);
48
+ if (n === 1205 || // ER_LOCK_WAIT_TIMEOUT
49
+ n === 1213 || // ER_LOCK_DEADLOCK
50
+ n === 1040 || // ER_CON_COUNT_ERROR (too many connections)
51
+ n === 1203 || // ER_TOO_MANY_USER_CONNECTIONS
52
+ n === 1053 || // ER_SERVER_SHUTDOWN
53
+ n === 1927 || // ER_CONNECTION_KILLED
54
+ n === 2006 || // CR_SERVER_GONE_ERROR
55
+ n === 2013) {
56
+ return true;
57
+ }
58
+ const c = code(err);
59
+ return c === "PROTOCOL_CONNECTION_LOST" || c === "PROTOCOL_SEQUENCE_TIMEOUT" || c === "CR_SERVER_GONE_ERROR" || c === "CR_SERVER_LOST" || c === "ECONNRESET" || c === "ECONNABORTED" || c === "ETIMEDOUT" || c === "ESOCKETTIMEDOUT" || c === "ECONNREFUSED" || c === "EHOSTUNREACH" || c === "ENETUNREACH" || c === "EPIPE" || c === "EAI_AGAIN" || c === "ENOTFOUND";
60
+ }
61
+
62
+ // src/drivers/mysql/warm.ts
63
+ var DEFAULT_TABLE = "cloud_roaring_warm";
64
+ var DEFAULT_LIST_BATCH = 1e3;
65
+ function mysqlWarmTableDDL(table = DEFAULT_TABLE) {
66
+ const t = validateAndQuoteTable(table);
67
+ return `CREATE TABLE IF NOT EXISTS ${t} (
68
+ key_prefix VARCHAR(191) NOT NULL,
69
+ namespace VARCHAR(256) NOT NULL,
70
+ segment VARCHAR(256) NOT NULL,
71
+ chunk_key INT NOT NULL,
72
+ token CHAR(36) NOT NULL,
73
+ payload LONGBLOB NOT NULL,
74
+ PRIMARY KEY (key_prefix, namespace, segment, chunk_key)
75
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;`;
76
+ }
77
+ var MysqlWarmDriver = class {
78
+ pool;
79
+ table;
80
+ // validated + backtick-quoted for interpolation
81
+ keyPrefix;
82
+ listBatch;
83
+ constructor(options) {
84
+ this.pool = options.pool;
85
+ this.table = validateAndQuoteTable(options.table ?? DEFAULT_TABLE);
86
+ this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);
87
+ const batch = options.listPageSize ?? DEFAULT_LIST_BATCH;
88
+ if (!Number.isSafeInteger(batch) || batch < 1) {
89
+ throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);
90
+ }
91
+ this.listBatch = batch;
92
+ }
93
+ // Reads are always strongly consistent (single primary), so the optional `WarmReadOptions` hint would be a
94
+ // no-op — the structurally-optional param is simply omitted (still satisfies IWarmDriver), as in LocalFs/pg.
95
+ async get(ref) {
96
+ validateChunkRef(ref);
97
+ try {
98
+ const [rows] = await this.pool.query(
99
+ `SELECT token, payload FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ?`,
100
+ [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey]
101
+ );
102
+ const row = rows[0];
103
+ return row === void 0 ? null : this.rowFrom(row, ref.chunkKey);
104
+ } catch (err) {
105
+ throw this.mapError(err);
106
+ }
107
+ }
108
+ async putConditional(ref, bytes, expected) {
109
+ validateChunkRef(ref);
110
+ const token = randomUUID();
111
+ const payload = Buffer.from(bytes);
112
+ const ns = namespacePart(ref.namespace);
113
+ try {
114
+ if (expected === NO_ROW) {
115
+ await this.pool.query(
116
+ `INSERT INTO ${this.table} (key_prefix, namespace, segment, chunk_key, token, payload) VALUES (?, ?, ?, ?, ?, ?)`,
117
+ [this.keyPrefix, ns, ref.segment, ref.chunkKey, token, payload]
118
+ );
119
+ } else {
120
+ const [res] = await this.pool.query(
121
+ `UPDATE ${this.table} SET token = ?, payload = ? WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,
122
+ [token, payload, this.keyPrefix, ns, ref.segment, ref.chunkKey, expected]
123
+ );
124
+ if (res.affectedRows !== 1) {
125
+ throw new WriteConflictError(`OCC conflict on chunk ${ref.chunkKey}`);
126
+ }
127
+ }
128
+ return { token };
129
+ } catch (err) {
130
+ if (isWriteConflictError(err)) throw err;
131
+ if (expected === NO_ROW && isDuplicateKey(err)) {
132
+ throw new WriteConflictError(`chunk ${ref.chunkKey} already exists (create-if-absent)`);
133
+ }
134
+ throw this.mapError(err);
135
+ }
136
+ }
137
+ async deleteConditional(ref, expected) {
138
+ validateChunkRef(ref);
139
+ try {
140
+ const [res] = await this.pool.query(
141
+ `DELETE FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,
142
+ [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey, expected]
143
+ );
144
+ if (res.affectedRows !== 1) {
145
+ throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);
146
+ }
147
+ } catch (err) {
148
+ if (isWriteConflictError(err)) throw err;
149
+ throw this.mapError(err);
150
+ }
151
+ }
152
+ async *listChunks(ref) {
153
+ validateSegmentRef(ref);
154
+ const ns = namespacePart(ref.namespace);
155
+ let after = -1;
156
+ for (; ; ) {
157
+ let rows;
158
+ try {
159
+ const [res] = await this.pool.query(
160
+ `SELECT chunk_key, token, payload FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key > ? ORDER BY chunk_key ASC LIMIT ?`,
161
+ [this.keyPrefix, ns, ref.segment, after, this.listBatch]
162
+ );
163
+ rows = res;
164
+ } catch (err) {
165
+ throw this.mapError(err);
166
+ }
167
+ for (const row of rows) {
168
+ const chunkKey = row.chunk_key;
169
+ if (typeof chunkKey !== "number") {
170
+ throw new IntegrityError("warm row is missing its chunk_key");
171
+ }
172
+ yield { chunkKey, ...this.rowFrom(row, chunkKey) };
173
+ after = chunkKey;
174
+ }
175
+ if (rows.length < this.listBatch) return;
176
+ }
177
+ }
178
+ /**
179
+ * Build a {@link WarmRow} from a raw row. `token` must be a non-empty string and `payload` a Buffer — a
180
+ * missing/typo'd column means a corrupt or foreign row, which we reject (untrusted-data posture) rather
181
+ * than paper over. The returned bytes are a fresh copy (driver-owned, read-only per the contract).
182
+ */
183
+ rowFrom(row, chunkKey) {
184
+ if (typeof row.token !== "string" || row.token === "") {
185
+ throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);
186
+ }
187
+ if (!(row.payload instanceof Uint8Array)) {
188
+ throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);
189
+ }
190
+ return { token: row.token, bytes: new Uint8Array(row.payload) };
191
+ }
192
+ /**
193
+ * Reclassify a transient MySQL fault (lock-wait/deadlock, connection lost, too-many-connections, dropped
194
+ * socket) as a retryable {@link TransientError}; everything else propagates unchanged. Applied at every
195
+ * query site so callers + the retry decorator only ever see typed errors. A deterministic OCC/duplicate-key
196
+ * conflict is signalled by the driver itself — never reclassified here.
197
+ */
198
+ mapError(err) {
199
+ if (isTransient(err)) {
200
+ return new TransientError(
201
+ `transient MySQL fault: ${err?.code ?? err?.errno ?? "unknown"}`,
202
+ { cause: err }
203
+ );
204
+ }
205
+ return err;
206
+ }
207
+ };
208
+
209
+ export { MysqlWarmDriver, mysqlWarmTableDDL };
210
+ //# sourceMappingURL=index.js.map
211
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/mysql/keys.ts","../../src/drivers/mysql/mysql-errors.ts","../../src/drivers/mysql/warm.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Identifier + key validation for {@link MysqlWarmDriver} (Phase 7).\n *\n * Pure, SDK-free string logic — unit-testable without a live MySQL. Like the Postgres driver, the logical ref\n * is stored across **parameterized columns** (`namespace`, `segment`, `chunk_key`, `key_prefix`), so ref/prefix\n * values are never concatenated into SQL — `mysql2`'s `?` placeholders bind them as data (no injection surface).\n * The one value that CANNOT be a bind parameter is the **table name** (an identifier, not a value), so it is\n * validated against a strict grammar and **backtick-quoted** before interpolation — that is why this exists.\n */\nimport { ValidationError } from '@/core/errors';\n\n/** A SQL identifier segment: leading letter/underscore, then letters/digits/underscores (ASCII, ≤64 = MySQL max). */\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;\n\n/**\n * Validate a table name — optionally database-qualified (`db.table`) — and return it **safely quoted** for\n * interpolation (each part wrapped in backticks, MySQL's identifier quote). A table name is an identifier, so\n * it can't be a bind parameter; validating against {@link IDENT} (which excludes backticks, whitespace, `;`,\n * `-`, etc.) and quoting closes the only SQL-injection vector the driver has. Rejects anything else, fail-fast.\n */\nexport function validateAndQuoteTable(table: string): string {\n const parts = table.split('.');\n if (parts.length > 2 || parts.some((p) => !IDENT.test(p))) {\n throw new ValidationError(\n `invalid table name ${JSON.stringify(table)} — expected an identifier or \"db.table\" ` +\n `(letters, digits, underscore; ≤64 chars; leading letter/underscore)`,\n );\n }\n return parts.map((p) => `\\`${p}\\``).join('.');\n}\n\n/**\n * The `key_prefix` column is `VARCHAR(191)` (sized so the composite primary key fits InnoDB's 3072-byte index\n * limit under utf8mb4). A longer prefix must be rejected **fail-fast**: under a non-strict `sql_mode` MySQL\n * would otherwise *silently truncate* it, and two logical stores whose prefixes share the same first 191\n * characters would then collide on the same rows — defeating the isolation `keyPrefix` exists to provide.\n */\nconst MAX_KEY_PREFIX_CHARS = 191;\n\n/**\n * Validate a caller-supplied `keyPrefix` (lets several logical stores share one table). It's bound as a `?`\n * value, not concatenated, so the character check is a light sanity guard (no control characters), not an\n * anti-injection boundary; the length check, by contrast, is a real correctness guard (see\n * {@link MAX_KEY_PREFIX_CHARS}). Empty/undefined means no prefix (stored as the empty string).\n */\nexport function normalizeKeyPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return '';\n let chars = 0;\n for (const ch of prefix) {\n chars++; // `for…of` iterates Unicode code points — matches MySQL's VARCHAR character count\n if (ch.charCodeAt(0) < 0x20) {\n throw new ValidationError('keyPrefix must not contain control characters');\n }\n }\n if (chars > MAX_KEY_PREFIX_CHARS) {\n throw new ValidationError(\n `keyPrefix must be at most ${MAX_KEY_PREFIX_CHARS} characters (the key_prefix column width); got ${chars}`,\n );\n }\n return prefix;\n}\n","/**\n * Pure helpers for classifying MySQL (`mysql2`) errors (Phase 7; transient class mirrors S3/GCS/Azure/Postgres).\n *\n * SDK-free + side-effect-free — they read only structural shapes off the thrown value (`err.errno`, a MySQL\n * error number; and `err.code`, a `mysql2`/Node string code), so the translation is unit-testable without a\n * live MySQL. Two classifiers: {@link isDuplicateKey} (the create-if-absent conflict signal, MySQL errno 1062)\n * and {@link isTransient} (retryable). Everything else propagates unchanged so a real bug is never blind-retried.\n */\n\n/** The MySQL error number off a thrown `mysql2` error, if present. */\nfunction errno(err: unknown): number | undefined {\n const n = (err as { errno?: unknown } | null)?.errno;\n return typeof n === 'number' ? n : undefined;\n}\n\n/** The string `code` off a thrown `mysql2`/Node error (e.g. `ER_DUP_ENTRY`, `PROTOCOL_CONNECTION_LOST`, `ECONNRESET`). */\nfunction code(err: unknown): string | undefined {\n const c = (err as { code?: unknown } | null)?.code;\n return typeof c === 'string' ? c : undefined;\n}\n\n/**\n * A duplicate-key violation (MySQL errno **1062** / `ER_DUP_ENTRY`). The driver's create-if-absent path is a\n * plain `INSERT`; a pre-existing row surfaces as this, which the driver maps to a `WriteConflictError`. Kept\n * separate from {@link isTransient} — a duplicate key is a **deterministic** conflict, never retried.\n */\nexport function isDuplicateKey(err: unknown): boolean {\n return errno(err) === 1062 || code(err) === 'ER_DUP_ENTRY';\n}\n\n/**\n * A transient MySQL fault that is safe to retry: a lock-wait timeout / deadlock (1205 / 1213), too-many-\n * connections (1040 / 1203), a server shutdown / killed connection (1053 / 1927), the client's\n * server-gone / connection-lost codes (2006 / 2013, `PROTOCOL_CONNECTION_LOST`, `PROTOCOL_SEQUENCE_TIMEOUT`),\n * or a dropped/timed-out socket. Deterministic, caller-meaningful outcomes (duplicate key, unknown table,\n * syntax errors, etc.) are NOT transient — they must surface, never be blind-retried.\n */\nexport function isTransient(err: unknown): boolean {\n const n = errno(err);\n if (\n n === 1205 || // ER_LOCK_WAIT_TIMEOUT\n n === 1213 || // ER_LOCK_DEADLOCK\n n === 1040 || // ER_CON_COUNT_ERROR (too many connections)\n n === 1203 || // ER_TOO_MANY_USER_CONNECTIONS\n n === 1053 || // ER_SERVER_SHUTDOWN\n n === 1927 || // ER_CONNECTION_KILLED\n n === 2006 || // CR_SERVER_GONE_ERROR\n n === 2013 // CR_SERVER_LOST\n ) {\n return true;\n }\n const c = code(err);\n return (\n c === 'PROTOCOL_CONNECTION_LOST' ||\n c === 'PROTOCOL_SEQUENCE_TIMEOUT' ||\n c === 'CR_SERVER_GONE_ERROR' ||\n c === 'CR_SERVER_LOST' ||\n c === 'ECONNRESET' ||\n c === 'ECONNABORTED' ||\n c === 'ETIMEDOUT' ||\n c === 'ESOCKETTIMEDOUT' ||\n c === 'ECONNREFUSED' ||\n c === 'EHOSTUNREACH' ||\n c === 'ENETUNREACH' ||\n c === 'EPIPE' ||\n c === 'EAI_AGAIN' ||\n c === 'ENOTFOUND'\n );\n}\n","/**\n * `MysqlWarmDriver` — an {@link IWarmDriver} over MySQL / MariaDB (Phase 7).\n *\n * \"No DynamoDB — use the MySQL you already run.\" Uses the official `mysql2` (its promise API), an **optional\n * peer dependency** — only consumers of `cloud-roaring/mysql` install it. A `mysql2` `Pool` is **injected**\n * (dependency injection): the driver owns no connection/credential logic, so it's thin, reuses the caller's\n * pool, and is testable against a MySQL container.\n *\n * Each chunk is one row in a single table (`PRIMARY KEY (key_prefix, namespace, segment, chunk_key)`), with an\n * opaque OCC **token** (a random UUID minted per write) and the delta `payload` (LONGBLOB). Optimistic\n * concurrency is real, cross-process, and server-side:\n * - **create-if-absent** = a plain `INSERT`; a pre-existing row raises `ER_DUP_ENTRY` (errno 1062) ⇒\n * {@link WriteConflictError}.\n * - **token-fenced update / delete** = `UPDATE … / DELETE … WHERE … AND token = ?`; `affectedRows !== 1` ⇒\n * the stored token moved (or the row is gone) ⇒ {@link WriteConflictError}.\n *\n * **Why `affectedRows` is reliable here despite MySQL counting *changed* (not matched) rows by default:** the\n * new token is a fresh random UUID on every write, so a matched fenced `UPDATE` **always** changes the `token`\n * column ⇒ `affectedRows === 1` on a match and `0` on a miss, regardless of the connection's\n * `CLIENT_FOUND_ROWS` flag. (The driver never sets a value equal to what's stored, so the \"unchanged row ⇒ 0\n * affected\" MySQL quirk cannot mask a real match.)\n *\n * **Collation matters for correctness:** the table is `utf8mb4_bin`, so `key_prefix` / `namespace` / `segment`\n * / `token` compare **byte-exact and case-sensitive**. MySQL's *default* collation (`utf8mb4_0900_ai_ci`) is\n * case- and accent-**insensitive**, which would make `segment = 'A'` wrongly match a row keyed `'a'` — a\n * correctness hole. {@link mysqlWarmTableDDL} pins `utf8mb4_bin` to close it; a caller supplying their own\n * table must do the same.\n *\n * Tokens are not reused across delete→recreate (ABA-safe, D3): a delete removes the row and a recreate mints a\n * fresh random UUID, so a token from before the delete won't match. (Probabilistic — a random 122-bit UUIDv4,\n * collision odds negligible — vs the DynamoDB driver's structural monotonic counter; the hard-delete model\n * can't offer the latter, and conformance D3 pins the behavior. See DECISIONS #57.) Reads are strongly\n * consistent (single primary), so the optional `WarmReadOptions` consistency hint is a no-op, like the\n * in-memory / LocalFs / Postgres drivers. Drivers may use `node:crypto`; only `core/` is determinism-bound.\n */\nimport { randomUUID } from 'node:crypto';\nimport type { Pool, ResultSetHeader, RowDataPacket } from 'mysql2/promise';\nimport {\n IntegrityError,\n TransientError,\n ValidationError,\n WriteConflictError,\n isWriteConflictError,\n} from '@/core/errors';\nimport { NO_ROW } from '@/core/ports';\nimport type { ChunkRef, IWarmDriver, NoRow, SegmentRef, Token, WarmRow } from '@/core/ports';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport { namespacePart } from '../_shared/keys';\nimport { normalizeKeyPrefix, validateAndQuoteTable } from './keys';\nimport { isDuplicateKey, isTransient } from './mysql-errors';\n\nconst DEFAULT_TABLE = 'cloud_roaring_warm';\n/** Default listChunks keyset-pagination batch — bounds peak memory to ~this many rows regardless of width. */\nconst DEFAULT_LIST_BATCH = 1000;\n\nexport interface MysqlWarmDriverOptions {\n /**\n * A constructed `mysql2` promise `Pool` (or a pool-compatible client exposing `query`). The driver never\n * opens/closes connections — the caller owns the pool's lifecycle. It **must target the primary**: reads are\n * treated as strongly consistent (the OCC read-modify-write needs read-your-writes), so a pool pointed at a\n * read replica could serve stale rows and silently break OCC.\n */\n readonly pool: Pool;\n /** Warm table name — identifier or `db.table` (default `cloud_roaring_warm`). Must already exist. */\n readonly table?: string;\n /** Optional key-prefix column value so several logical stores can share one table (bound as data). */\n readonly keyPrefix?: string;\n /** Advanced: `listChunks` keyset-pagination page size (default 1000). Tunes peak memory on wide segments. */\n readonly listPageSize?: number;\n}\n\n/** One raw warm row as selected. */\ninterface RawRow extends RowDataPacket {\n chunk_key?: number;\n token?: unknown;\n payload?: unknown;\n}\n\n/**\n * The idempotent DDL for the warm table (identifier-validated + backtick-quoted). The driver does **not**\n * create schema (it stays thin + needs no DDL privileges at runtime); run this once at deploy time, e.g.\n * `await pool.query(mysqlWarmTableDDL())`. The `utf8mb4_bin` collation is **required for correctness** (see the\n * class doc): it makes the key columns compare byte-exact and case-sensitive, unlike MySQL's default ci\n * collation. Column lengths keep the composite primary key within InnoDB's 3072-byte index limit under utf8mb4.\n */\nexport function mysqlWarmTableDDL(table: string = DEFAULT_TABLE): string {\n const t = validateAndQuoteTable(table);\n return (\n `CREATE TABLE IF NOT EXISTS ${t} (\\n` +\n ` key_prefix VARCHAR(191) NOT NULL,\\n` +\n ` namespace VARCHAR(256) NOT NULL,\\n` +\n ` segment VARCHAR(256) NOT NULL,\\n` +\n ` chunk_key INT NOT NULL,\\n` +\n ` token CHAR(36) NOT NULL,\\n` +\n ` payload LONGBLOB NOT NULL,\\n` +\n ` PRIMARY KEY (key_prefix, namespace, segment, chunk_key)\\n` +\n `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;`\n );\n}\n\nexport class MysqlWarmDriver implements IWarmDriver {\n private readonly pool: Pool;\n private readonly table: string; // validated + backtick-quoted for interpolation\n private readonly keyPrefix: string;\n private readonly listBatch: number;\n\n constructor(options: MysqlWarmDriverOptions) {\n this.pool = options.pool;\n this.table = validateAndQuoteTable(options.table ?? DEFAULT_TABLE);\n this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);\n const batch = options.listPageSize ?? DEFAULT_LIST_BATCH;\n if (!Number.isSafeInteger(batch) || batch < 1) {\n throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);\n }\n this.listBatch = batch;\n }\n\n // Reads are always strongly consistent (single primary), so the optional `WarmReadOptions` hint would be a\n // no-op — the structurally-optional param is simply omitted (still satisfies IWarmDriver), as in LocalFs/pg.\n async get(ref: ChunkRef): Promise<WarmRow | null> {\n validateChunkRef(ref);\n try {\n const [rows] = await this.pool.query<RawRow[]>(\n `SELECT token, payload FROM ${this.table} ` +\n `WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ?`,\n [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey],\n );\n const row = rows[0];\n return row === undefined ? null : this.rowFrom(row, ref.chunkKey);\n } catch (err) {\n throw this.mapError(err);\n }\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n validateChunkRef(ref);\n const token = randomUUID();\n // Copy the payload into a Buffer for the LONGBLOB binding — severs any caller-owned/reused input buffer.\n const payload = Buffer.from(bytes);\n const ns = namespacePart(ref.namespace);\n try {\n if (expected === NO_ROW) {\n // create-if-absent: a plain INSERT. A pre-existing PK raises ER_DUP_ENTRY (1062), caught below as a\n // deterministic conflict — never an INSERT IGNORE (which would also swallow unrelated errors).\n await this.pool.query<ResultSetHeader>(\n `INSERT INTO ${this.table} (key_prefix, namespace, segment, chunk_key, token, payload) ` +\n `VALUES (?, ?, ?, ?, ?, ?)`,\n [this.keyPrefix, ns, ref.segment, ref.chunkKey, token, payload],\n );\n } else {\n const [res] = await this.pool.query<ResultSetHeader>(\n `UPDATE ${this.table} SET token = ?, payload = ? ` +\n `WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,\n [token, payload, this.keyPrefix, ns, ref.segment, ref.chunkKey, expected],\n );\n // The fresh token always changes the row, so affectedRows reflects the MATCH: 1 = fenced write applied,\n // anything else = the stored token moved or the row is gone (a conflict), never a silent false-success.\n if (res.affectedRows !== 1) {\n throw new WriteConflictError(`OCC conflict on chunk ${ref.chunkKey}`);\n }\n }\n return { token };\n } catch (err) {\n if (isWriteConflictError(err)) throw err; // our own deterministic conflict — never a transient\n if (expected === NO_ROW && isDuplicateKey(err)) {\n throw new WriteConflictError(`chunk ${ref.chunkKey} already exists (create-if-absent)`);\n }\n throw this.mapError(err);\n }\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n validateChunkRef(ref);\n try {\n const [res] = await this.pool.query<ResultSetHeader>(\n `DELETE FROM ${this.table} ` +\n `WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,\n [this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey, expected],\n );\n if (res.affectedRows !== 1) {\n throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);\n }\n } catch (err) {\n if (isWriteConflictError(err)) throw err;\n throw this.mapError(err);\n }\n }\n\n async *listChunks(ref: SegmentRef): AsyncIterable<{ chunkKey: number } & WarmRow> {\n validateSegmentRef(ref);\n const ns = namespacePart(ref.namespace);\n // Keyset pagination on chunk_key (ascending) bounds peak memory to one page WITHIN a single enumeration,\n // regardless of how wide the segment is. (It does NOT resume across a transient fault — the retry decorator\n // re-enumerates a streaming method from the start and buffers, see drivers/retry/retrying-drivers.ts.)\n let after = -1;\n for (;;) {\n let rows: RawRow[];\n try {\n const [res] = await this.pool.query<RawRow[]>(\n `SELECT chunk_key, token, payload FROM ${this.table} ` +\n `WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key > ? ` +\n `ORDER BY chunk_key ASC LIMIT ?`,\n [this.keyPrefix, ns, ref.segment, after, this.listBatch],\n );\n rows = res;\n } catch (err) {\n throw this.mapError(err);\n }\n for (const row of rows) {\n const chunkKey = row.chunk_key;\n if (typeof chunkKey !== 'number') {\n throw new IntegrityError('warm row is missing its chunk_key');\n }\n yield { chunkKey, ...this.rowFrom(row, chunkKey) };\n after = chunkKey;\n }\n if (rows.length < this.listBatch) return;\n }\n }\n\n /**\n * Build a {@link WarmRow} from a raw row. `token` must be a non-empty string and `payload` a Buffer — a\n * missing/typo'd column means a corrupt or foreign row, which we reject (untrusted-data posture) rather\n * than paper over. The returned bytes are a fresh copy (driver-owned, read-only per the contract).\n */\n private rowFrom(row: RawRow, chunkKey: number): WarmRow {\n if (typeof row.token !== 'string' || row.token === '') {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);\n }\n if (!(row.payload instanceof Uint8Array)) {\n throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);\n }\n return { token: row.token, bytes: new Uint8Array(row.payload) };\n }\n\n /**\n * Reclassify a transient MySQL fault (lock-wait/deadlock, connection lost, too-many-connections, dropped\n * socket) as a retryable {@link TransientError}; everything else propagates unchanged. Applied at every\n * query site so callers + the retry decorator only ever see typed errors. A deterministic OCC/duplicate-key\n * conflict is signalled by the driver itself — never reclassified here.\n */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient MySQL fault: ${(err as { code?: unknown } | null)?.code ?? (err as { errno?: unknown } | null)?.errno ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n"]}
@@ -0,0 +1,357 @@
1
+ /** Streaming append target for a single object being written. */
2
+ interface BlobSink {
3
+ write(bytes: Uint8Array): Promise<void>;
4
+ }
5
+ /**
6
+ * Random/tail read access to a single finished object. The total size comes back from `getTail` (the
7
+ * codec's entry point), so there's no separate `size()` — keeping the seam minimal for driver authors.
8
+ */
9
+ interface BlobReader {
10
+ /** Exactly `length` bytes starting at `offset`. Out-of-range is a `ValidationError`, never a short read. */
11
+ getRange(offset: number, length: number): Promise<Uint8Array>;
12
+ /** The last `min(maxBytes, size)` bytes plus the total size — the one-GET footer+index path. */
13
+ getTail(maxBytes: number): Promise<{
14
+ bytes: Uint8Array;
15
+ size: number;
16
+ }>;
17
+ }
18
+ /** In-memory `BlobSink` that accumulates written bytes; `bytes()` returns the concatenation. */
19
+ declare class BufferSink implements BlobSink {
20
+ private readonly chunks;
21
+ private length;
22
+ write(bytes: Uint8Array): Promise<void>;
23
+ bytes(): Uint8Array;
24
+ }
25
+ /** In-memory `BlobReader` over a fixed byte buffer. */
26
+ declare class BufferReader implements BlobReader {
27
+ private readonly buffer;
28
+ constructor(buffer: Uint8Array);
29
+ getRange(offset: number, length: number): Promise<Uint8Array>;
30
+ getTail(maxBytes: number): Promise<{
31
+ bytes: Uint8Array;
32
+ size: number;
33
+ }>;
34
+ }
35
+
36
+ /**
37
+ * Encryption seams — the **pure** interfaces `core/` depends on so the codec can encrypt/decrypt without
38
+ * importing `node:crypto` (lint-banned here, like `Clock`/`Rng`/drivers). Concrete AES-256-GCM lives outside
39
+ * `core/` (`src/drivers/crypto.ts`) and is injected. The envelope scheme: a per-segment DEK wrapped under an
40
+ * operator-held KEK, so discarding the DEK crypto-shreds the segment,
41
+ * [DECISIONS #19]. Per-chunk AEAD over the `.crbm` payloads + index; a per-segment **DEK** wrapped (envelope
42
+ * encryption) under one or more **KEK**s by an {@link IKeystore}; the wrapped DEK(s) live in the registry, so
43
+ * crypto-shred = delete them.
44
+ */
45
+ /** One AEAD result: a fresh per-message nonce, the ciphertext, and the authentication tag. */
46
+ interface AeadSealed {
47
+ readonly nonce: Uint8Array;
48
+ readonly ciphertext: Uint8Array;
49
+ readonly tag: Uint8Array;
50
+ }
51
+ /**
52
+ * Authenticated encryption bound to a single key (a segment's DEK). `aad` (associated data) is authenticated
53
+ * but not encrypted — the codec passes each chunk's `(namespace, segment, generation, chunkKey)` identity
54
+ * ({@link aadFor}), so a chunk's ciphertext can't be silently relocated to another segment/generation/chunk or
55
+ * the index, even by someone who can rewrite Cold objects. The seam is symmetric and self-framing-agnostic:
56
+ * the caller decides where the nonce/tag go (inline with a chunk, or in the footer for the index).
57
+ */
58
+ interface Aead {
59
+ /** Encrypt `plaintext`, authenticating `aad`. Returns a fresh nonce + ciphertext + tag. */
60
+ seal(plaintext: Uint8Array, aad: Uint8Array): AeadSealed;
61
+ /**
62
+ * Decrypt + verify. Throws {@link IntegrityError} on **any** mismatch (wrong key, tampered ciphertext/tag,
63
+ * or `aad` that doesn't match what was sealed) — never returns wrong-but-plausible plaintext.
64
+ */
65
+ open(sealed: AeadSealed, aad: Uint8Array): Uint8Array;
66
+ }
67
+ /**
68
+ * A DEK wrapped under one KEK. A segment stores a **list** of these (one per KEK it was wrapped under — e.g.
69
+ * an active KEK plus an offline recovery KEK), so losing one KEK isn't fatal and KEK rotation needs no
70
+ * data re-encryption. `keyId` names which KEK; `wrapped` is the opaque wrapped-key blob (base64). Carries no
71
+ * plaintext key material — safe to store in the registry next to the (encrypted) data.
72
+ */
73
+ interface WrappedDek {
74
+ readonly keyId: string;
75
+ readonly wrapped: string;
76
+ }
77
+ /**
78
+ * Envelope key management — the injected seam between the codec and however the operator holds key material
79
+ * (in-process BYOK by default; KMS/Vault as optional adapters later). Async so a remote keystore (KMS) fits
80
+ * the same interface. Never exposes raw key bytes to `core/`: it returns an {@link Aead} bound to the DEK.
81
+ */
82
+ interface IKeystore {
83
+ /**
84
+ * Mint a fresh per-segment DEK, wrapped under the active KEK (and any recovery KEK). Returns the wrapping(s)
85
+ * to persist in the registry plus an {@link Aead} to encrypt this segment's chunks with.
86
+ */
87
+ createDek(): Promise<{
88
+ wrapped: WrappedDek[];
89
+ aead: Aead;
90
+ }>;
91
+ /**
92
+ * Reconstruct the {@link Aead} from a segment's stored wrappings by unwrapping with any KEK currently held.
93
+ * Throws {@link KeyUnavailableError} if it holds none of the referenced KEKs (lost key, or a shredded
94
+ * segment whose wrappings were deleted).
95
+ */
96
+ openDek(wrapped: readonly WrappedDek[]): Promise<Aead>;
97
+ }
98
+ /**
99
+ * What the encrypting codec needs: the {@link Aead} for this segment's DEK plus a way to build the AAD for a
100
+ * given scope (`'index'` or a chunkKey). Built per (segment, generation) by the cold-source bridge so the
101
+ * codec itself stays segment-agnostic.
102
+ */
103
+ interface CrbmCrypto {
104
+ readonly aead: Aead;
105
+ aadFor(scope: number | 'index'): Uint8Array;
106
+ }
107
+ /**
108
+ * Build the AEAD associated-data binding a chunk (or the index) to its exact location:
109
+ * `v1 ‖ len(namespace) ‖ namespace ‖ len(segment) ‖ segment ‖ generation ‖ scope ‖ chunkKey`. Length-prefixed
110
+ * so distinct `(namespace, segment)` pairs can never collide. Pure (no crypto) — just the authenticated label.
111
+ */
112
+ declare function aadFor(ref: {
113
+ readonly namespace?: string;
114
+ readonly segment: string;
115
+ }, generation: number, scope: number | 'index'): Uint8Array;
116
+
117
+ /**
118
+ * Storage-driver contracts the engine depends on.
119
+ *
120
+ * Phase 1 uses the subset needed by the in-memory engine: the per-chunk Warm store (under OCC) and a
121
+ * per-chunk read view of Cold. Drivers move opaque bytes + an OCC token; they never understand roaring,
122
+ * `adds`/`removes`, or the `.crbm` layout. The full `IColdDriver` (blob/range) + `IRegistryDriver` arrive
123
+ * with generations in Phase 2/3 — the `.crbm` reader will implement `ColdChunkSource`.
124
+ */
125
+
126
+ /** Opaque optimistic-concurrency token — unique per write, compared by equality only. */
127
+ type Token = string;
128
+ /** Sentinel for `putConditional` meaning "the row must not exist yet" (create). */
129
+ declare const NO_ROW: unique symbol;
130
+ type NoRow = typeof NO_ROW;
131
+ interface SegmentRef {
132
+ readonly namespace?: string;
133
+ readonly segment: string;
134
+ }
135
+ interface ChunkRef extends SegmentRef {
136
+ readonly chunkKey: number;
137
+ }
138
+ /** Identifies one immutable `.crbm` object — a single generation of a segment. */
139
+ interface GenKey extends SegmentRef {
140
+ readonly generation: number;
141
+ }
142
+ interface WarmRow {
143
+ readonly token: Token;
144
+ /** Read-only: owned by the driver. Callers must NOT mutate the buffer (re-encode to write). */
145
+ readonly bytes: Uint8Array;
146
+ }
147
+ /**
148
+ * Read consistency for a Warm fetch (gap #9). `consistent: true` (the default — omitted ⇒ true) is a
149
+ * strong, read-your-writes read, as the OCC read-modify-write path requires. `consistent: false` requests an
150
+ * **eventually-consistent** read — on DynamoDB that is ~½ the RCU cost; a driver that is always strongly
151
+ * consistent (in-memory, LocalFs) ignores it. The engine uses it only on read paths (`has`/`count`/`iterate`/
152
+ * `intersect`) when the store opts into `warmReadConsistency: 'eventual'`, trading read-after-write for cost.
153
+ */
154
+ interface WarmReadOptions {
155
+ readonly consistent?: boolean;
156
+ }
157
+ /** Per-chunk Warm store under optimistic concurrency. Returned byte buffers are read-only. */
158
+ interface IWarmDriver {
159
+ get(ref: ChunkRef, opts?: WarmReadOptions): Promise<WarmRow | null>;
160
+ /** OCC write. `expected` = `NO_ROW` to create, else the token previously read. Throws `WriteConflictError` on mismatch. */
161
+ putConditional(ref: ChunkRef, bytes: Uint8Array, expected: Token | NoRow): Promise<{
162
+ token: Token;
163
+ }>;
164
+ /**
165
+ * Fenced delete — only if the stored token still equals `expected`; throws `WriteConflictError`
166
+ * otherwise. **Used by compaction (Phase 4); the Phase-1 engine never calls it** — drivers still
167
+ * implement it to satisfy the conformance suite.
168
+ */
169
+ deleteConditional(ref: ChunkRef, expected: Token): Promise<void>;
170
+ /** All dirty chunks of a segment, ascending `chunkKey`. Yielded `bytes` are read-only. */
171
+ listChunks(ref: SegmentRef, opts?: WarmReadOptions): AsyncIterable<{
172
+ chunkKey: number;
173
+ } & WarmRow>;
174
+ }
175
+ /**
176
+ * A segment's grounded on-disk footprint (Phase 5b) — the current generation's Cold object bytes, read
177
+ * cheaply from the `.crbm` footer/index (no payload reads). Powers the grounded `costReport()`.
178
+ */
179
+ interface SegmentSize {
180
+ readonly sizeBytes: number;
181
+ }
182
+ /** Per-chunk read view of the immutable Cold tier (implemented by the `.crbm` reader from Phase 2). */
183
+ interface ColdChunkSource {
184
+ /** Read-only bytes for the chunk, or `null` if absent. Callers must not mutate the buffer. */
185
+ getChunk(ref: ChunkRef): Promise<Uint8Array | null>;
186
+ listChunkKeys(ref: SegmentRef): Promise<number[]>;
187
+ /**
188
+ * Optional (Phase 5b): the current generation's grounded size, cheaply (from the already-parsed
189
+ * `.crbm` index — no payload reads), or `null` if the segment has no Cold generation. Powers the
190
+ * grounded `costReport()`.
191
+ */
192
+ sizeOf?(ref: SegmentRef): Promise<SegmentSize | null>;
193
+ /**
194
+ * Optional (Phase 5c): the current generation's **per-chunk cardinality** (`chunkKey → count`), read
195
+ * from the already-parsed `.crbm` index with **no payload reads**, or `null` if the segment has no Cold
196
+ * generation. Powers the cheap `count()` — a warm-delta-free chunk is counted straight from the index
197
+ * instead of fetching + deserializing it. A source with no index (e.g. the in-memory source) omits this,
198
+ * and `count()` falls back to fetching + merging every chunk.
199
+ */
200
+ cardinalities?(ref: SegmentRef): Promise<ReadonlyMap<number, number> | null>;
201
+ /**
202
+ * Optional (Phase B, gap #4): the segment's **current generation number** as this source resolves it right now
203
+ * (registry `currentGen`, or the highest cold generation), or `null` if the segment has no Cold generation. The
204
+ * engine keys its HOT chunk cache by this so a generation bump (a compaction commit) is observed — a new
205
+ * generation misses the cache instead of serving a stale decoded chunk, and an erased id can't resurrect from a
206
+ * cached pre-compaction chunk. Cheap: served from the source's own (short-TTL-refreshed) snapshot, **not** a
207
+ * fresh backend read per call. A source that pins one immutable generation for its whole lifetime and never
208
+ * refreshes may omit this — the engine then keys the cache without a generation, exactly as before.
209
+ */
210
+ currentGeneration?(ref: SegmentRef): Promise<number | null>;
211
+ }
212
+ /** Capabilities a Cold driver advertises; validated at wiring time, fail-fast. */
213
+ interface ColdCaps {
214
+ /** REQUIRED — the format relies on byte-range reads. */
215
+ readonly rangeRead: true;
216
+ /** Largest single object the backend accepts (informs single-object-vs-shard, B7 — future). */
217
+ readonly maxObjectBytes: number;
218
+ /** Optional: enables the pure-object `LATEST`-pointer registry variant (S3 conditional put). */
219
+ readonly conditionalPut?: boolean;
220
+ }
221
+ /**
222
+ * Immutable object storage for `.crbm` generations. A "dumb byte mover": it
223
+ * understands neither roaring nor the `.crbm` layout, only opaque bytes addressed by a {@link GenKey}.
224
+ * The core never reuses a key, so puts are write-once.
225
+ */
226
+ interface IColdDriver {
227
+ capabilities(): ColdCaps;
228
+ /**
229
+ * Stream a new immutable generation. The driver opens a destination, hands `write` a {@link BlobSink},
230
+ * then atomically commits (and computes the content hash). Throws if the key already exists (write-once).
231
+ */
232
+ putImmutable(key: GenKey, write: (sink: BlobSink) => Promise<void>): Promise<{
233
+ size: number;
234
+ sha256: string;
235
+ }>;
236
+ /** Range read; the caller bounds-checks. Out-of-range is rejected, never a short/adjacent read. */
237
+ getRange(key: GenKey, offset: number, length: number): Promise<Uint8Array>;
238
+ /** Speculative tail read: the last `min(maxBytes, size)` bytes + the total object size. */
239
+ getTail(key: GenKey, maxBytes: number): Promise<{
240
+ bytes: Uint8Array;
241
+ size: number;
242
+ }>;
243
+ delete(key: GenKey): Promise<void>;
244
+ /** Enumerate the generations present for a segment (orphan sweep / latest-gen resolution). */
245
+ list(ref: SegmentRef): AsyncIterable<GenKey>;
246
+ }
247
+ /**
248
+ * Lifecycle status of a segment. `active` is the steady state;
249
+ * `compacting`/`erasing` are transient flags a daemon sets while it works (Phase 4d/4e); `destroyed` is the
250
+ * post-crypto-shred tombstone (the row is kept for audit but the segment is logically gone). The 4c registry
251
+ * stores and round-trips the field; the *transitions* are driven by their owning features.
252
+ */
253
+ type RegistryStatus = 'active' | 'compacting' | 'erasing' | 'destroyed';
254
+ /** Free-form, JSON-serializable governance metadata (retention/residency policy); shape lands in Phase 6. */
255
+ type GovernanceMeta = Record<string, unknown>;
256
+ /**
257
+ * One registry row — the authoritative per-segment record. Exactly one per segment.
258
+ */
259
+ interface RegistryRecord extends SegmentRef {
260
+ /** **The** authoritative LATEST pointer: which immutable Cold generation is current. */
261
+ readonly currentGen: number;
262
+ /**
263
+ * Per-segment data-key (DEK) wrappings for encryption-at-rest (Phase 4e): the DEK envelope-wrapped under one
264
+ * or more KEKs (active + optional recovery). Reading unwraps with any held KEK; **crypto-shred deletes this
265
+ * whole list**, making the segment's at-rest bytes permanently unrecoverable. Absent ⇒ the segment is
266
+ * cleartext. See [DECISIONS #19].
267
+ */
268
+ readonly wrappedDeks?: readonly WrappedDek[];
269
+ /**
270
+ * Optional **external** key reference (reserved) — e.g. a KMS key ARN for a future KMS keystore adapter that
271
+ * keeps wrapped material in the KMS rather than in-band {@link wrappedDeks}. Unused by the in-process BYOK
272
+ * keystore. Clearable on crypto-shred.
273
+ */
274
+ readonly keyId?: string;
275
+ /**
276
+ * Dirty-row hint written by `findCompactable`. Discovery still recomputes the live dirty count by scanning
277
+ * Warm each cycle (the count that decides candidacy), but it now **reads** this hint to gate a change-guarded
278
+ * CAS — the hint is only rewritten when the count actually moved, so an all-idle fleet issues no per-segment
279
+ * registry writes. It does not yet drive candidacy — turning the Warm scan itself into O(dirty) is a
280
+ * deferred cheap-enumeration fix.
281
+ */
282
+ readonly dirtyChunkCount: number;
283
+ readonly status: RegistryStatus;
284
+ /**
285
+ * Daemon health (Phase D, gap #2): epoch-ms of the last **successfully committed** compaction, and the
286
+ * count of **consecutive** compaction failures (reset to 0 by a successful commit). `lastCompactedAt`
287
+ * powers a dead-man's-switch / staleness alarm; `consecutiveFailures` drives **poison-segment quarantine** —
288
+ * discovery skips a segment past a failure threshold so one corrupt Warm row can't freeze the compaction of a
289
+ * segment **that has a committed generation** forever (with no alarm) while its Warm backlog grows unbounded.
290
+ * (A segment whose *very first* compaction keeps failing has no registry row to count against yet —
291
+ * bootstrap faults are surfaced as `error` results but not yet quarantined; gap #2.) Both
292
+ * **optional** (absent on rows written before Phase D, and on a segment never compacted) — read them as
293
+ * `lastCompactedAt ?? undefined`, `consecutiveFailures ?? 0`.
294
+ */
295
+ readonly lastCompactedAt?: number;
296
+ readonly consecutiveFailures?: number;
297
+ /**
298
+ * Compaction lease (Phase 4d): the opaque id of the worker currently compacting this segment, and the
299
+ * epoch-ms at which its lease expires. `status === 'compacting'` with `leaseExpiresAt` in the future means
300
+ * a live daemon owns it; once `leaseExpiresAt` is past, another worker may **steal** the lease (the prior
301
+ * holder crashed). Both absent in the steady (`active`) state. The lease is an efficiency guard only — the
302
+ * 2-phase commit is correct without it (OCC-fenced swap + write-once generations + version-fenced purge).
303
+ */
304
+ readonly leaseOwner?: string;
305
+ readonly leaseExpiresAt?: number;
306
+ /** Governance policy (Phase 6); stored + round-tripped now, semantics later. */
307
+ readonly retention?: GovernanceMeta;
308
+ readonly residency?: GovernanceMeta;
309
+ /** Epoch-ms of creation / last mutation (from the driver's injected clock). */
310
+ readonly createdAt: number;
311
+ readonly updatedAt: number;
312
+ /** Opaque OCC token — compare-by-equality, never reused (ABA-safe), exactly like {@link WarmRow.token}. */
313
+ readonly token: Token;
314
+ }
315
+ /** The caller-settable fields at {@link IRegistryDriver.create} (audit + token are driver-managed). */
316
+ interface NewRegistryRecord {
317
+ readonly currentGen: number;
318
+ readonly wrappedDeks?: readonly WrappedDek[];
319
+ readonly keyId?: string;
320
+ /** Defaults to 0. */
321
+ readonly dirtyChunkCount?: number;
322
+ /** Defaults to `'active'`. */
323
+ readonly status?: RegistryStatus;
324
+ readonly retention?: GovernanceMeta;
325
+ readonly residency?: GovernanceMeta;
326
+ }
327
+ /** Fields a {@link IRegistryDriver.compareAndSwap} may mutate (identity + audit + token are off-limits). */
328
+ type RegistryPatch = Partial<Pick<RegistryRecord, 'currentGen' | 'wrappedDeks' | 'keyId' | 'dirtyChunkCount' | 'status' | 'leaseOwner' | 'leaseExpiresAt' | 'lastCompactedAt' | 'consecutiveFailures' | 'retention' | 'residency'>>;
329
+ /** Capabilities a registry driver advertises; validated fail-fast at wiring time. */
330
+ interface RegCaps {
331
+ /** REQUIRED — `currentGen` feeds read correctness + compaction CAS, so reads must be strongly consistent. */
332
+ readonly strongRead: true;
333
+ }
334
+ /**
335
+ * Per-segment registry — the authoritative source of `currentGen`, the discovery index, and (Phase 4e) the
336
+ * wrapped-DEK holder. One row per segment under OCC: a
337
+ * never-reused, equality-compared {@link Token} (ABA-safe across delete→recreate, like the Warm tier).
338
+ */
339
+ interface IRegistryDriver {
340
+ capabilities(): RegCaps;
341
+ /** The segment's record, or `null` if it doesn't exist (or was deleted). */
342
+ get(ref: SegmentRef): Promise<RegistryRecord | null>;
343
+ /** Create the row; throws {@link WriteConflictError} if it already exists (use CAS to mutate). */
344
+ create(ref: SegmentRef, record: NewRegistryRecord): Promise<{
345
+ token: Token;
346
+ }>;
347
+ /** Server-side compare-and-set: apply `patch` iff the stored token equals `expected`, else `WriteConflictError`. */
348
+ compareAndSwap(ref: SegmentRef, expected: Token, patch: RegistryPatch): Promise<{
349
+ token: Token;
350
+ }>;
351
+ /** Discovery: every live record, optionally scoped to one namespace. Order is unspecified. */
352
+ list(namespace?: string): AsyncIterable<RegistryRecord>;
353
+ /** Remove the row (tombstoned for ABA-safety — a later `create` still gets a fresh, greater token). */
354
+ delete(ref: SegmentRef): Promise<void>;
355
+ }
356
+
357
+ export { type Aead as A, type BlobSink as B, type ColdCaps as C, type GenKey as G, type IColdDriver as I, type NoRow as N, type RegCaps as R, type SegmentRef as S, type Token as T, type WarmReadOptions as W, type IWarmDriver as a, type ChunkRef as b, type WarmRow as c, type IRegistryDriver as d, type RegistryRecord as e, type NewRegistryRecord as f, type RegistryPatch as g, type ColdChunkSource as h, type SegmentSize as i, type IKeystore as j, type CrbmCrypto as k, type BlobReader as l, type WrappedDek as m, type AeadSealed as n, BufferReader as o, BufferSink as p, type GovernanceMeta as q, NO_ROW as r, type RegistryStatus as s, aadFor as t };