@noy-db/hub 0.2.0-pre.30 → 0.2.0-pre.31

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 (143) hide show
  1. package/dist/adapter/index.d.ts +9 -0
  2. package/dist/adapter/index.js +14 -0
  3. package/dist/attestation/index.d.ts +1 -1
  4. package/dist/attestation/index.js +3 -3
  5. package/dist/blobs/index.d.ts +3 -3
  6. package/dist/bundle/index.d.ts +3 -3
  7. package/dist/bundle/index.js +1 -1
  8. package/dist/{chunk-Y6YUWZTS.js → chunk-L5HHBOMS.js} +2 -2
  9. package/dist/{chunk-BVKLJBJJ.js → chunk-W5NVNJDX.js} +2 -1
  10. package/dist/{chunk-BVKLJBJJ.js.map → chunk-W5NVNJDX.js.map} +1 -1
  11. package/dist/consent/index.d.ts +2 -2
  12. package/dist/{decrypt-partition-Mhjh6nnG.d.ts → decrypt-partition-CXS5KopB.d.ts} +1 -1
  13. package/dist/derivations/index.d.ts +3 -3
  14. package/dist/{dev-unlock-DJ3IhgY9.d.ts → dev-unlock-CnHTTVea.d.ts} +1 -1
  15. package/dist/guards/index.d.ts +3 -3
  16. package/dist/{hash-DLwkYf1d.d.ts → hash-oc5paYl0.d.ts} +1 -1
  17. package/dist/history/index.d.ts +3 -3
  18. package/dist/i18n/index.d.ts +2 -2
  19. package/dist/{index-CMRIx_zx.d.ts → index-BmhGztUi.d.ts} +1 -1
  20. package/dist/index.d.ts +11 -11
  21. package/dist/index.js +2 -2
  22. package/dist/kernel/index.d.ts +2 -2
  23. package/dist/materialized-views/index.d.ts +3 -3
  24. package/dist/{mime-magic-CacDBwYp.d.ts → mime-magic-JeLKHRng.d.ts} +1 -1
  25. package/dist/{noydb-5MIBD77B.js → noydb-NAU2DTFP.js} +2 -2
  26. package/dist/noydb-NAU2DTFP.js.map +1 -0
  27. package/dist/overlay-views/index.d.ts +3 -3
  28. package/dist/periods/index.d.ts +2 -2
  29. package/dist/session/index.d.ts +3 -3
  30. package/dist/shadow/index.d.ts +2 -2
  31. package/dist/snapshots/index.d.ts +2 -2
  32. package/dist/store/index.d.ts +2 -2
  33. package/dist/sync/index.d.ts +1 -1
  34. package/dist/team/index.d.ts +2 -2
  35. package/dist/{transition-guard-J04-jime.d.ts → transition-guard-Dy3NioQr.d.ts} +1 -1
  36. package/dist/tx/index.d.ts +2 -2
  37. package/dist/{with-materialized-view-DNfzC5lH.d.ts → with-materialized-view-DIVeez0W.d.ts} +1 -1
  38. package/dist/{with-overlayed-view-qk3lbhOd.d.ts → with-overlayed-view-DVZrc5Hb.d.ts} +1 -1
  39. package/dist/{with-rollup-Dd3_ZG4b.d.ts → with-rollup-CbU-O4wP.d.ts} +1 -1
  40. package/package.json +62 -221
  41. package/dist/aggregate/index.cjs +0 -1144
  42. package/dist/aggregate/index.cjs.map +0 -1
  43. package/dist/aggregate/index.d.cts +0 -39
  44. package/dist/attestation/index.cjs +0 -305
  45. package/dist/attestation/index.cjs.map +0 -1
  46. package/dist/attestation/index.d.cts +0 -54
  47. package/dist/blobs/index.cjs +0 -1967
  48. package/dist/blobs/index.cjs.map +0 -1
  49. package/dist/blobs/index.d.cts +0 -48
  50. package/dist/bundle/index.cjs +0 -41045
  51. package/dist/bundle/index.cjs.map +0 -1
  52. package/dist/bundle/index.d.cts +0 -187
  53. package/dist/consent/index.cjs +0 -204
  54. package/dist/consent/index.cjs.map +0 -1
  55. package/dist/consent/index.d.cts +0 -27
  56. package/dist/crdt/index.cjs +0 -152
  57. package/dist/crdt/index.cjs.map +0 -1
  58. package/dist/crdt/index.d.cts +0 -30
  59. package/dist/decrypt-partition-C0iDpbSd.d.cts +0 -558
  60. package/dist/derivations/index.cjs +0 -469
  61. package/dist/derivations/index.cjs.map +0 -1
  62. package/dist/derivations/index.d.cts +0 -74
  63. package/dist/dev-unlock-0Z6yxfS7.d.cts +0 -263
  64. package/dist/discriminant-BN9REW3o.d.cts +0 -60
  65. package/dist/errors-BAWO5Z5a.d.cts +0 -1500
  66. package/dist/forget/index.cjs +0 -43
  67. package/dist/forget/index.cjs.map +0 -1
  68. package/dist/forget/index.d.cts +0 -1
  69. package/dist/guards/index.cjs +0 -455
  70. package/dist/guards/index.cjs.map +0 -1
  71. package/dist/guards/index.d.cts +0 -39
  72. package/dist/hash-Db8ncXGr.d.cts +0 -63
  73. package/dist/history/index.cjs +0 -1245
  74. package/dist/history/index.cjs.map +0 -1
  75. package/dist/history/index.d.cts +0 -65
  76. package/dist/i18n/index.cjs +0 -1280
  77. package/dist/i18n/index.cjs.map +0 -1
  78. package/dist/i18n/index.d.cts +0 -41
  79. package/dist/index-BMmajblo.d.cts +0 -362
  80. package/dist/index-C1SC1EPe.d.cts +0 -1325
  81. package/dist/index-xJEPkvMb.d.cts +0 -93
  82. package/dist/index.cjs +0 -48100
  83. package/dist/index.cjs.map +0 -1
  84. package/dist/index.d.cts +0 -1050
  85. package/dist/indexing/index.cjs +0 -803
  86. package/dist/indexing/index.cjs.map +0 -1
  87. package/dist/indexing/index.d.cts +0 -36
  88. package/dist/kernel/index.cjs +0 -751
  89. package/dist/kernel/index.cjs.map +0 -1
  90. package/dist/kernel/index.d.cts +0 -11
  91. package/dist/lazy-builder-eYZzLEL1.d.cts +0 -304
  92. package/dist/materialized-views/index.cjs +0 -1518
  93. package/dist/materialized-views/index.cjs.map +0 -1
  94. package/dist/materialized-views/index.d.cts +0 -187
  95. package/dist/mime-magic-sdMzsfVK.d.cts +0 -103
  96. package/dist/overlay-views/index.cjs +0 -399
  97. package/dist/overlay-views/index.cjs.map +0 -1
  98. package/dist/overlay-views/index.d.cts +0 -102
  99. package/dist/periods/index.cjs +0 -1041
  100. package/dist/periods/index.cjs.map +0 -1
  101. package/dist/periods/index.d.cts +0 -24
  102. package/dist/predicate-BmhBSPCH.d.cts +0 -267
  103. package/dist/query/index.cjs +0 -3480
  104. package/dist/query/index.cjs.map +0 -1
  105. package/dist/query/index.d.cts +0 -4
  106. package/dist/sealed-record/index.cjs +0 -139
  107. package/dist/sealed-record/index.cjs.map +0 -1
  108. package/dist/sealed-record/index.d.cts +0 -123
  109. package/dist/session/index.cjs +0 -495
  110. package/dist/session/index.cjs.map +0 -1
  111. package/dist/session/index.d.cts +0 -48
  112. package/dist/shadow/index.cjs +0 -133
  113. package/dist/shadow/index.cjs.map +0 -1
  114. package/dist/shadow/index.d.cts +0 -19
  115. package/dist/snapshots/index.cjs +0 -937
  116. package/dist/snapshots/index.cjs.map +0 -1
  117. package/dist/snapshots/index.d.cts +0 -30
  118. package/dist/store/index.cjs +0 -1091
  119. package/dist/store/index.cjs.map +0 -1
  120. package/dist/store/index.d.cts +0 -501
  121. package/dist/strategy-BSxFXGzb.d.cts +0 -110
  122. package/dist/strategy-mJExw4LQ.d.cts +0 -1069
  123. package/dist/sync/index.cjs +0 -1062
  124. package/dist/sync/index.cjs.map +0 -1
  125. package/dist/sync/index.d.cts +0 -45
  126. package/dist/team/index.cjs +0 -2805
  127. package/dist/team/index.cjs.map +0 -1
  128. package/dist/team/index.d.cts +0 -120
  129. package/dist/transition-guard-tWZIsaXe.d.cts +0 -165
  130. package/dist/tx/index.cjs +0 -614
  131. package/dist/tx/index.cjs.map +0 -1
  132. package/dist/tx/index.d.cts +0 -39
  133. package/dist/types-CdVfiQgt.d.cts +0 -15275
  134. package/dist/ulid-DRH25k3y.d.cts +0 -66
  135. package/dist/util/index.cjs +0 -237
  136. package/dist/util/index.cjs.map +0 -1
  137. package/dist/util/index.d.cts +0 -79
  138. package/dist/with-materialized-view-DfgPcvi9.d.cts +0 -27
  139. package/dist/with-overlayed-view-Cvfkx1lg.d.cts +0 -13
  140. package/dist/with-rollup-nIxo7h9z.d.cts +0 -47
  141. /package/dist/{noydb-5MIBD77B.js.map → adapter/index.js.map} +0 -0
  142. /package/dist/{chunk-Y6YUWZTS.js.map → chunk-L5HHBOMS.js.map} +0 -0
  143. /package/dist/{types-DHn9lEP0.d.ts → index-DHn9lEP0.d.ts} +0 -0
@@ -1,1245 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/history/index.ts
21
- var history_exports = {};
22
- __export(history_exports, {
23
- CollectionInstant: () => CollectionInstant,
24
- LEDGER_COLLECTION: () => LEDGER_COLLECTION,
25
- LEDGER_DELTAS_COLLECTION: () => LEDGER_DELTAS_COLLECTION,
26
- LedgerStore: () => LedgerStore,
27
- VaultInstant: () => VaultInstant,
28
- applyPatch: () => applyPatch,
29
- canonicalJson: () => canonicalJson,
30
- clearHistory: () => clearHistory,
31
- computePatch: () => computePatch,
32
- diff: () => diff,
33
- envelopePayloadHash: () => envelopePayloadHash,
34
- formatDiff: () => formatDiff,
35
- getHistory: () => getHistory,
36
- getVersionEnvelope: () => getVersionEnvelope,
37
- hashEntry: () => hashEntry,
38
- paddedIndex: () => paddedIndex,
39
- parseIndex: () => parseIndex,
40
- pruneHistory: () => pruneHistory,
41
- saveHistory: () => saveHistory,
42
- sha256Hex: () => sha256Hex,
43
- withHistory: () => withHistory
44
- });
45
- module.exports = __toCommonJS(history_exports);
46
-
47
- // src/types.ts
48
- var NOYDB_FORMAT_VERSION = 1;
49
-
50
- // src/history/history.ts
51
- var HISTORY_COLLECTION = "_history";
52
- var VERSION_PAD = 10;
53
- function historyId(collection, recordId, version) {
54
- return `${collection}:${recordId}:${String(version).padStart(VERSION_PAD, "0")}`;
55
- }
56
- function matchesPrefix(id, collection, recordId) {
57
- if (recordId) {
58
- return id.startsWith(`${collection}:${recordId}:`);
59
- }
60
- return id.startsWith(`${collection}:`);
61
- }
62
- async function saveHistory(adapter, vault, collection, recordId, envelope) {
63
- const id = historyId(collection, recordId, envelope._v);
64
- await adapter.put(vault, HISTORY_COLLECTION, id, envelope);
65
- }
66
- async function getHistory(adapter, vault, collection, recordId, options) {
67
- const allIds = await adapter.list(vault, HISTORY_COLLECTION);
68
- const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId)).sort().reverse();
69
- const entries = [];
70
- for (const id of matchingIds) {
71
- const envelope = await adapter.get(vault, HISTORY_COLLECTION, id);
72
- if (!envelope) continue;
73
- if (options?.from && envelope._ts < options.from) continue;
74
- if (options?.to && envelope._ts > options.to) continue;
75
- entries.push(envelope);
76
- if (options?.limit && entries.length >= options.limit) break;
77
- }
78
- return entries;
79
- }
80
- async function getVersionEnvelope(adapter, vault, collection, recordId, version) {
81
- const id = historyId(collection, recordId, version);
82
- return adapter.get(vault, HISTORY_COLLECTION, id);
83
- }
84
- async function pruneHistory(adapter, vault, collection, recordId, options) {
85
- const allIds = await adapter.list(vault, HISTORY_COLLECTION);
86
- const matchingIds = allIds.filter((id) => recordId ? matchesPrefix(id, collection, recordId) : matchesPrefix(id, collection)).sort();
87
- let toDelete = [];
88
- if (options.keepVersions !== void 0) {
89
- const keep = options.keepVersions;
90
- if (matchingIds.length > keep) {
91
- toDelete = matchingIds.slice(0, matchingIds.length - keep);
92
- }
93
- }
94
- if (options.beforeDate) {
95
- for (const id of matchingIds) {
96
- if (toDelete.includes(id)) continue;
97
- const envelope = await adapter.get(vault, HISTORY_COLLECTION, id);
98
- if (envelope && envelope._ts < options.beforeDate) {
99
- toDelete.push(id);
100
- }
101
- }
102
- }
103
- const uniqueDeletes = [...new Set(toDelete)];
104
- for (const id of uniqueDeletes) {
105
- await adapter.delete(vault, HISTORY_COLLECTION, id);
106
- }
107
- return uniqueDeletes.length;
108
- }
109
- async function clearHistory(adapter, vault, collection, recordId) {
110
- const allIds = await adapter.list(vault, HISTORY_COLLECTION);
111
- let toDelete;
112
- if (collection && recordId) {
113
- toDelete = allIds.filter((id) => matchesPrefix(id, collection, recordId));
114
- } else if (collection) {
115
- toDelete = allIds.filter((id) => matchesPrefix(id, collection));
116
- } else {
117
- toDelete = allIds;
118
- }
119
- for (const id of toDelete) {
120
- await adapter.delete(vault, HISTORY_COLLECTION, id);
121
- }
122
- return toDelete.length;
123
- }
124
- async function tombstoneHistory(adapter, vault, collection, recordId, actor) {
125
- const allIds = await adapter.list(vault, HISTORY_COLLECTION);
126
- const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId));
127
- const now = (/* @__PURE__ */ new Date()).toISOString();
128
- let count = 0;
129
- for (const id of matchingIds) {
130
- const env = await adapter.get(vault, HISTORY_COLLECTION, id);
131
- if (!env) continue;
132
- if (!env._data && env._cek === void 0) continue;
133
- const tombstone = {
134
- _noydb: NOYDB_FORMAT_VERSION,
135
- _v: env._v,
136
- _ts: now,
137
- _iv: "",
138
- _data: "",
139
- ...actor ? { _by: actor } : {}
140
- };
141
- await adapter.put(vault, HISTORY_COLLECTION, id, tombstone);
142
- count++;
143
- }
144
- return count;
145
- }
146
-
147
- // src/history/diff.ts
148
- function diff(oldObj, newObj, basePath = "") {
149
- const changes = [];
150
- if (oldObj === newObj) return changes;
151
- if (oldObj == null && newObj != null) {
152
- return [{ path: basePath || "(root)", type: "added", to: newObj }];
153
- }
154
- if (oldObj != null && newObj == null) {
155
- return [{ path: basePath || "(root)", type: "removed", from: oldObj }];
156
- }
157
- if (typeof oldObj !== typeof newObj) {
158
- return [{ path: basePath || "(root)", type: "changed", from: oldObj, to: newObj }];
159
- }
160
- if (typeof oldObj !== "object") {
161
- return [{ path: basePath || "(root)", type: "changed", from: oldObj, to: newObj }];
162
- }
163
- if (Array.isArray(oldObj) && Array.isArray(newObj)) {
164
- const maxLen = Math.max(oldObj.length, newObj.length);
165
- for (let i = 0; i < maxLen; i++) {
166
- const p = basePath ? `${basePath}[${i}]` : `[${i}]`;
167
- if (i >= oldObj.length) {
168
- changes.push({ path: p, type: "added", to: newObj[i] });
169
- } else if (i >= newObj.length) {
170
- changes.push({ path: p, type: "removed", from: oldObj[i] });
171
- } else {
172
- changes.push(...diff(oldObj[i], newObj[i], p));
173
- }
174
- }
175
- return changes;
176
- }
177
- const oldRecord = oldObj;
178
- const newRecord = newObj;
179
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldRecord), ...Object.keys(newRecord)]);
180
- for (const key of allKeys) {
181
- const p = basePath ? `${basePath}.${key}` : key;
182
- if (!(key in oldRecord)) {
183
- changes.push({ path: p, type: "added", to: newRecord[key] });
184
- } else if (!(key in newRecord)) {
185
- changes.push({ path: p, type: "removed", from: oldRecord[key] });
186
- } else {
187
- changes.push(...diff(oldRecord[key], newRecord[key], p));
188
- }
189
- }
190
- return changes;
191
- }
192
- function formatDiff(changes) {
193
- if (changes.length === 0) return "(no changes)";
194
- return changes.map((c) => {
195
- switch (c.type) {
196
- case "added":
197
- return `+ ${c.path}: ${JSON.stringify(c.to)}`;
198
- case "removed":
199
- return `- ${c.path}: ${JSON.stringify(c.from)}`;
200
- case "changed":
201
- return `~ ${c.path}: ${JSON.stringify(c.from)} \u2192 ${JSON.stringify(c.to)}`;
202
- }
203
- }).join("\n");
204
- }
205
-
206
- // src/errors.ts
207
- var NoydbError = class extends Error {
208
- /** Machine-readable error code. Stable across library versions. */
209
- code;
210
- constructor(code, message) {
211
- super(message);
212
- this.name = "NoydbError";
213
- this.code = code;
214
- }
215
- };
216
- var DecryptionError = class extends NoydbError {
217
- constructor(message = "Decryption failed") {
218
- super("DECRYPTION_FAILED", message);
219
- this.name = "DecryptionError";
220
- }
221
- };
222
- var TamperedError = class extends NoydbError {
223
- constructor(message = "Data integrity check failed \u2014 record may have been tampered with") {
224
- super("TAMPERED", message);
225
- this.name = "TamperedError";
226
- }
227
- };
228
- var ReadOnlyAtInstantError = class extends NoydbError {
229
- constructor(operation, timestamp) {
230
- super(
231
- "READ_ONLY_AT_INSTANT",
232
- `Cannot ${operation}() on a vault view anchored at ${timestamp} \u2014 time-machine views are read-only`
233
- );
234
- this.name = "ReadOnlyAtInstantError";
235
- }
236
- };
237
- var ConflictError = class extends NoydbError {
238
- /** The actual stored version at the time of conflict. */
239
- version;
240
- constructor(version, message = "Version conflict") {
241
- super("CONFLICT", message);
242
- this.name = "ConflictError";
243
- this.version = version;
244
- }
245
- };
246
- var LedgerContentionError = class extends NoydbError {
247
- attempts;
248
- constructor(attempts) {
249
- super(
250
- "LEDGER_CONTENTION",
251
- `LedgerStore.append: failed to claim a chain slot after ${attempts} optimistic-CAS retries`
252
- );
253
- this.name = "LedgerContentionError";
254
- this.attempts = attempts;
255
- }
256
- };
257
-
258
- // src/crypto.ts
259
- var IV_BYTES = 12;
260
- var subtle = globalThis.crypto.subtle;
261
- async function encrypt(plaintext, dek) {
262
- const iv = generateIV();
263
- const encoded = new TextEncoder().encode(plaintext);
264
- const ciphertext = await subtle.encrypt(
265
- { name: "AES-GCM", iv },
266
- dek,
267
- encoded
268
- );
269
- return {
270
- iv: bufferToBase64(iv),
271
- data: bufferToBase64(ciphertext)
272
- };
273
- }
274
- async function decrypt(ivBase64, dataBase64, dek) {
275
- const iv = base64ToBuffer(ivBase64);
276
- const ciphertext = base64ToBuffer(dataBase64);
277
- try {
278
- const plaintext = await subtle.decrypt(
279
- { name: "AES-GCM", iv },
280
- dek,
281
- ciphertext
282
- );
283
- return new TextDecoder().decode(plaintext);
284
- } catch (err) {
285
- if (err instanceof Error && err.name === "OperationError") {
286
- throw new TamperedError();
287
- }
288
- throw new DecryptionError(
289
- err instanceof Error ? err.message : "Decryption failed"
290
- );
291
- }
292
- }
293
- function generateIV() {
294
- return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
295
- }
296
- function bufferToBase64(buffer) {
297
- const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
298
- let binary = "";
299
- for (let i = 0; i < bytes.length; i++) {
300
- binary += String.fromCharCode(bytes[i]);
301
- }
302
- return btoa(binary);
303
- }
304
- function base64ToBuffer(base64) {
305
- const binary = atob(base64);
306
- const bytes = new Uint8Array(binary.length);
307
- for (let i = 0; i < binary.length; i++) {
308
- bytes[i] = binary.charCodeAt(i);
309
- }
310
- return bytes;
311
- }
312
-
313
- // src/history/ledger/entry.ts
314
- function canonicalJson(value) {
315
- if (value === null) return "null";
316
- if (typeof value === "boolean") return value ? "true" : "false";
317
- if (typeof value === "number") {
318
- if (!Number.isFinite(value)) {
319
- throw new Error(
320
- `canonicalJson: refusing to encode non-finite number ${String(value)}`
321
- );
322
- }
323
- return JSON.stringify(value);
324
- }
325
- if (typeof value === "string") return JSON.stringify(value);
326
- if (typeof value === "bigint") {
327
- throw new Error("canonicalJson: BigInt is not JSON-serializable");
328
- }
329
- if (typeof value === "undefined" || typeof value === "function") {
330
- throw new Error(
331
- `canonicalJson: refusing to encode ${typeof value} \u2014 include all fields explicitly`
332
- );
333
- }
334
- if (Array.isArray(value)) {
335
- return "[" + value.map((v) => canonicalJson(v)).join(",") + "]";
336
- }
337
- if (typeof value === "object") {
338
- const obj = value;
339
- const keys = Object.keys(obj).sort();
340
- const parts = [];
341
- for (const key of keys) {
342
- parts.push(JSON.stringify(key) + ":" + canonicalJson(obj[key]));
343
- }
344
- return "{" + parts.join(",") + "}";
345
- }
346
- throw new Error(`canonicalJson: unexpected value type: ${typeof value}`);
347
- }
348
- async function sha256Hex(input) {
349
- const bytes = new TextEncoder().encode(input);
350
- const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
351
- return bytesToHex(new Uint8Array(digest));
352
- }
353
- async function hashEntry(entry) {
354
- return sha256Hex(canonicalJson(entry));
355
- }
356
- function bytesToHex(bytes) {
357
- const hex = new Array(bytes.length);
358
- for (let i = 0; i < bytes.length; i++) {
359
- hex[i] = (bytes[i] ?? 0).toString(16).padStart(2, "0");
360
- }
361
- return hex.join("");
362
- }
363
- function paddedIndex(index) {
364
- return String(index).padStart(10, "0");
365
- }
366
- function parseIndex(key) {
367
- return Number.parseInt(key, 10);
368
- }
369
-
370
- // src/history/ledger/patch.ts
371
- function computePatch(prev, next) {
372
- const ops = [];
373
- diff2(prev, next, "", ops);
374
- return ops;
375
- }
376
- function diff2(prev, next, path, out) {
377
- if (prev === next) return;
378
- if (prev === null || next === null) {
379
- out.push({ op: "replace", path, value: next });
380
- return;
381
- }
382
- const prevIsArray = Array.isArray(prev);
383
- const nextIsArray = Array.isArray(next);
384
- const prevIsObject = typeof prev === "object" && !prevIsArray;
385
- const nextIsObject = typeof next === "object" && !nextIsArray;
386
- if (prevIsArray !== nextIsArray || prevIsObject !== nextIsObject) {
387
- out.push({ op: "replace", path, value: next });
388
- return;
389
- }
390
- if (prevIsArray && nextIsArray) {
391
- if (!arrayDeepEqual(prev, next)) {
392
- out.push({ op: "replace", path, value: next });
393
- }
394
- return;
395
- }
396
- if (prevIsObject && nextIsObject) {
397
- const prevObj = prev;
398
- const nextObj = next;
399
- const prevKeys = Object.keys(prevObj);
400
- const nextKeys = Object.keys(nextObj);
401
- for (const key of prevKeys) {
402
- const childPath = path + "/" + escapePathSegment(key);
403
- if (!(key in nextObj)) {
404
- out.push({ op: "remove", path: childPath });
405
- } else {
406
- diff2(prevObj[key], nextObj[key], childPath, out);
407
- }
408
- }
409
- for (const key of nextKeys) {
410
- if (!(key in prevObj)) {
411
- out.push({
412
- op: "add",
413
- path: path + "/" + escapePathSegment(key),
414
- value: nextObj[key]
415
- });
416
- }
417
- }
418
- return;
419
- }
420
- out.push({ op: "replace", path, value: next });
421
- }
422
- function arrayDeepEqual(a, b) {
423
- if (a.length !== b.length) return false;
424
- for (let i = 0; i < a.length; i++) {
425
- if (!deepEqual(a[i], b[i])) return false;
426
- }
427
- return true;
428
- }
429
- function deepEqual(a, b) {
430
- if (a === b) return true;
431
- if (a === null || b === null) return false;
432
- if (typeof a !== typeof b) return false;
433
- if (typeof a !== "object") return false;
434
- const aArray = Array.isArray(a);
435
- const bArray = Array.isArray(b);
436
- if (aArray !== bArray) return false;
437
- if (aArray && bArray) return arrayDeepEqual(a, b);
438
- const aObj = a;
439
- const bObj = b;
440
- const aKeys = Object.keys(aObj);
441
- const bKeys = Object.keys(bObj);
442
- if (aKeys.length !== bKeys.length) return false;
443
- for (const key of aKeys) {
444
- if (!(key in bObj)) return false;
445
- if (!deepEqual(aObj[key], bObj[key])) return false;
446
- }
447
- return true;
448
- }
449
- function applyPatch(base, patch) {
450
- let result = clone(base);
451
- for (const op of patch) {
452
- result = applyOp(result, op);
453
- }
454
- return result;
455
- }
456
- function applyOp(doc, op) {
457
- if (op.path === "") {
458
- if (op.op === "remove") return null;
459
- return clone(op.value);
460
- }
461
- const segments = parsePath(op.path);
462
- return walkAndApply(doc, segments, op);
463
- }
464
- function walkAndApply(doc, segments, op) {
465
- if (segments.length === 0) {
466
- throw new Error("walkAndApply: empty segments (internal error)");
467
- }
468
- const [head, ...rest] = segments;
469
- if (head === void 0) throw new Error("walkAndApply: undefined segment");
470
- if (rest.length === 0) {
471
- return applyAtTerminal(doc, head, op);
472
- }
473
- if (Array.isArray(doc)) {
474
- const idx = parseArrayIndex(head, doc.length);
475
- const child = doc[idx];
476
- const newChild = walkAndApply(child, rest, op);
477
- const next = doc.slice();
478
- next[idx] = newChild;
479
- return next;
480
- }
481
- if (doc !== null && typeof doc === "object") {
482
- const obj = doc;
483
- if (!(head in obj)) {
484
- throw new Error(`applyPatch: path segment "${head}" not found in object`);
485
- }
486
- const newChild = walkAndApply(obj[head], rest, op);
487
- return { ...obj, [head]: newChild };
488
- }
489
- throw new Error(
490
- `applyPatch: cannot step into ${typeof doc} at segment "${head}"`
491
- );
492
- }
493
- function applyAtTerminal(doc, segment, op) {
494
- if (Array.isArray(doc)) {
495
- const idx = segment === "-" ? doc.length : parseArrayIndex(segment, doc.length + 1);
496
- const next = doc.slice();
497
- if (op.op === "remove") {
498
- next.splice(idx, 1);
499
- return next;
500
- }
501
- if (op.op === "add") {
502
- next.splice(idx, 0, clone(op.value));
503
- return next;
504
- }
505
- if (op.op === "replace") {
506
- if (idx >= doc.length) {
507
- throw new Error(
508
- `applyPatch: replace at out-of-bounds array index ${idx}`
509
- );
510
- }
511
- next[idx] = clone(op.value);
512
- return next;
513
- }
514
- }
515
- if (doc !== null && typeof doc === "object") {
516
- const obj = doc;
517
- if (op.op === "remove") {
518
- if (!(segment in obj)) {
519
- throw new Error(
520
- `applyPatch: remove on missing key "${segment}"`
521
- );
522
- }
523
- const next = { ...obj };
524
- delete next[segment];
525
- return next;
526
- }
527
- if (op.op === "add") {
528
- return { ...obj, [segment]: clone(op.value) };
529
- }
530
- if (op.op === "replace") {
531
- if (!(segment in obj)) {
532
- throw new Error(
533
- `applyPatch: replace on missing key "${segment}"`
534
- );
535
- }
536
- return { ...obj, [segment]: clone(op.value) };
537
- }
538
- }
539
- throw new Error(
540
- `applyPatch: cannot apply ${op.op} at terminal segment "${segment}"`
541
- );
542
- }
543
- function escapePathSegment(segment) {
544
- return segment.replace(/~/g, "~0").replace(/\//g, "~1");
545
- }
546
- function unescapePathSegment(segment) {
547
- return segment.replace(/~1/g, "/").replace(/~0/g, "~");
548
- }
549
- function parsePath(path) {
550
- if (!path.startsWith("/")) {
551
- throw new Error(`applyPatch: path must start with '/', got "${path}"`);
552
- }
553
- return path.slice(1).split("/").map(unescapePathSegment);
554
- }
555
- function parseArrayIndex(segment, max) {
556
- if (!/^\d+$/.test(segment)) {
557
- throw new Error(
558
- `applyPatch: array index must be a non-negative integer, got "${segment}"`
559
- );
560
- }
561
- const idx = Number.parseInt(segment, 10);
562
- if (idx < 0 || idx > max) {
563
- throw new Error(
564
- `applyPatch: array index ${idx} out of range [0, ${max}]`
565
- );
566
- }
567
- return idx;
568
- }
569
- function clone(value) {
570
- if (value === null || value === void 0) return value;
571
- if (typeof value !== "object") return value;
572
- return JSON.parse(JSON.stringify(value));
573
- }
574
-
575
- // src/history/ledger/constants.ts
576
- var LEDGER_COLLECTION = "_ledger";
577
- var LEDGER_DELTAS_COLLECTION = "_ledger_deltas";
578
-
579
- // src/history/ledger/hash.ts
580
- async function envelopePayloadHash(envelope) {
581
- if (!envelope) return "";
582
- return sha256Hex(envelope._data);
583
- }
584
-
585
- // src/history/ledger/store.ts
586
- var MAX_APPEND_ATTEMPTS = 8;
587
- var LedgerStore = class {
588
- adapter;
589
- vault;
590
- encrypted;
591
- getDEK;
592
- actor;
593
- /**
594
- * In-memory cache of the chain head — the most recently appended
595
- * entry along with its precomputed hash. Without this, every
596
- * `append()` would re-load every prior entry to recompute the
597
- * prevHash, making N puts O(N²) — a 1K-record stress test goes from
598
- * < 100ms to a multi-second timeout.
599
- *
600
- * The cache is populated on first read (`append`, `head`, `verify`)
601
- * and updated in-place on every successful `append`. Single-writer
602
- * usage (the assumption) keeps it consistent. A second
603
- * LedgerStore instance writing to the same vault would not
604
- * see the first instance's appends in its cached state — that's the
605
- * concurrency caveat documented at the class level.
606
- *
607
- * Sentinel `undefined` means "not yet loaded"; an explicit `null`
608
- * value means "loaded and confirmed empty" — distinguishing these
609
- * matters because an empty ledger is a valid state (genesis prevHash
610
- * is the empty string), and we don't want to re-scan the adapter
611
- * just because the chain is freshly initialized.
612
- */
613
- headCache = void 0;
614
- constructor(opts) {
615
- this.adapter = opts.adapter;
616
- this.vault = opts.vault;
617
- this.encrypted = opts.encrypted;
618
- this.getDEK = opts.getDEK;
619
- this.actor = opts.actor;
620
- }
621
- /**
622
- * Lazily load (or return cached) the current chain head. The cache
623
- * sentinel is `undefined` until first access; after the first call,
624
- * the cache holds either a `{ entry, hash }` for non-empty ledgers
625
- * or `null` for empty ones.
626
- */
627
- async getCachedHead() {
628
- if (this.headCache !== void 0) return this.headCache;
629
- const entries = await this.loadAllEntries();
630
- const last = entries[entries.length - 1];
631
- if (!last) {
632
- this.headCache = null;
633
- return null;
634
- }
635
- this.headCache = { entry: last, hash: await hashEntry(last) };
636
- return this.headCache;
637
- }
638
- /**
639
- * Append a new entry to the ledger. Returns the full entry that was
640
- * written (with its assigned index and computed prevHash) so the
641
- * caller can use the hash for downstream purposes (e.g., embedding
642
- * in a verifiable backup).
643
- *
644
- * This is the **only** way to add entries. Direct adapter writes to
645
- * `_ledger/` would bypass the chain math and would be caught by the
646
- * next `verify()` call as a divergence.
647
- *
648
- * ## Multi-writer correctness
649
- *
650
- * Append is implemented as an optimistic-CAS retry loop. On every
651
- * attempt:
652
- *
653
- * 1. Read fresh head (cache invalidated on retry).
654
- * 2. Compute `nextIndex = head.index + 1`, `prevHash = hash(head)`.
655
- * 3. Encrypt delta payload IN MEMORY (no adapter write yet) so we
656
- * can compute `deltaHash` before claiming the chain slot.
657
- * 4. Build + encrypt the entry envelope.
658
- * 5. `adapter.put(_ledger, paddedIndex, envelope, expectedVersion: 0)`
659
- * — the `expectedVersion: 0` asserts "this slot must not exist."
660
- * Stores with `casAtomic: true` honor the CAS check; under
661
- * contention the second writer's put throws `ConflictError`.
662
- * 6. On `ConflictError`: invalidate the head cache, sleep with
663
- * bounded backoff + jitter, retry. After `MAX_APPEND_ATTEMPTS`
664
- * retries throw {@link LedgerContentionError}.
665
- * 7. On success: write the delta envelope (if any) at the same
666
- * index. Update the head cache.
667
- *
668
- * Entry-first ordering matters: writing the delta first under
669
- * contention would orphan delta records at indices the writer never
670
- * actually claimed. The deltaHash is computed off the encrypted
671
- * envelope's `_data` field, which doesn't require the envelope to
672
- * be persisted.
673
- *
674
- * Stores with `casAtomic: false` (file, s3, r2 by default) silently
675
- * accept the `expectedVersion: 0` argument and proceed without a
676
- * CAS check. Concurrent appends against those stores remain
677
- * best-effort — pair them with an advisory lock or with sync
678
- * single-writer discipline.
679
- */
680
- async append(input) {
681
- let lastConflict;
682
- for (let attempt = 0; attempt < MAX_APPEND_ATTEMPTS; attempt++) {
683
- if (attempt > 0) {
684
- this.headCache = void 0;
685
- }
686
- try {
687
- return await this.appendOnce(input);
688
- } catch (err) {
689
- if (err instanceof ConflictError) {
690
- lastConflict = err;
691
- if (attempt < MAX_APPEND_ATTEMPTS - 1) {
692
- await sleepBackoff(attempt);
693
- }
694
- continue;
695
- }
696
- throw err;
697
- }
698
- }
699
- void lastConflict;
700
- throw new LedgerContentionError(MAX_APPEND_ATTEMPTS);
701
- }
702
- /**
703
- * One attempt at the append cycle. Throws `ConflictError` when the
704
- * CAS check on the entry put fails — `append()` catches that and
705
- * retries. Any other error propagates to the caller.
706
- */
707
- async appendOnce(input) {
708
- const cached = await this.getCachedHead();
709
- const lastEntry = cached?.entry;
710
- const prevHash = cached?.hash ?? "";
711
- const nextIndex = lastEntry ? lastEntry.index + 1 : 0;
712
- let deltaEnvelope;
713
- let deltaHash;
714
- if (input.delta !== void 0) {
715
- deltaEnvelope = await this.encryptDelta(input.delta);
716
- deltaHash = await sha256Hex(deltaEnvelope._data);
717
- }
718
- const entryBase = {
719
- index: nextIndex,
720
- prevHash,
721
- op: input.op,
722
- collection: input.collection,
723
- id: input.id,
724
- version: input.version,
725
- ts: (/* @__PURE__ */ new Date()).toISOString(),
726
- actor: input.actor === "" ? this.actor : input.actor,
727
- payloadHash: input.payloadHash
728
- };
729
- const entry = {
730
- ...entryBase,
731
- ...deltaHash !== void 0 ? { deltaHash } : {},
732
- ...input.amendment !== void 0 ? { amendment: input.amendment } : {},
733
- ...input.reason !== void 0 ? { reason: input.reason } : {}
734
- };
735
- const envelope = await this.encryptEntry(entry);
736
- await this.adapter.put(
737
- this.vault,
738
- LEDGER_COLLECTION,
739
- paddedIndex(entry.index),
740
- envelope,
741
- 0
742
- );
743
- if (deltaEnvelope) {
744
- await this.adapter.put(
745
- this.vault,
746
- LEDGER_DELTAS_COLLECTION,
747
- paddedIndex(entry.index),
748
- deltaEnvelope,
749
- 0
750
- );
751
- }
752
- this.headCache = { entry, hash: await hashEntry(entry) };
753
- return entry;
754
- }
755
- /**
756
- * Load a delta payload by its entry index. Returns `null` if the
757
- * entry at that index doesn't reference a delta (genesis puts and
758
- * deletes leave the slot empty) or if the delta row is missing
759
- * (possible after a `pruneHistory` fold).
760
- *
761
- * The caller is responsible for deciding what to do with a missing
762
- * delta — `ledger.reconstruct()` uses it as a "stop walking
763
- * backward" signal and falls back to the on-disk current value.
764
- */
765
- async loadDelta(index) {
766
- const envelope = await this.adapter.get(
767
- this.vault,
768
- LEDGER_DELTAS_COLLECTION,
769
- paddedIndex(index)
770
- );
771
- if (!envelope) return null;
772
- if (!this.encrypted) {
773
- return JSON.parse(envelope._data);
774
- }
775
- const dek = await this.getDEK(LEDGER_COLLECTION);
776
- const json = await decrypt(envelope._iv, envelope._data, dek);
777
- return JSON.parse(json);
778
- }
779
- /** Encrypt a JSON Patch into an envelope for storage. Mirrors encryptEntry. */
780
- async encryptDelta(patch) {
781
- const json = JSON.stringify(patch);
782
- if (!this.encrypted) {
783
- return {
784
- _noydb: NOYDB_FORMAT_VERSION,
785
- _v: 1,
786
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
787
- _iv: "",
788
- _data: json,
789
- _by: this.actor
790
- };
791
- }
792
- const dek = await this.getDEK(LEDGER_COLLECTION);
793
- const { iv, data } = await encrypt(json, dek);
794
- return {
795
- _noydb: NOYDB_FORMAT_VERSION,
796
- _v: 1,
797
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
798
- _iv: iv,
799
- _data: data,
800
- _by: this.actor
801
- };
802
- }
803
- /**
804
- * Read all entries in ascending-index order. Used internally by
805
- * `append()`, `head()`, `verify()`, and `entries()`. Decryption is
806
- * serial because the entries are tiny and the overhead of a Promise
807
- * pool would dominate at realistic chain lengths (< 100K entries).
808
- */
809
- async loadAllEntries() {
810
- const keys = await this.adapter.list(this.vault, LEDGER_COLLECTION);
811
- keys.sort();
812
- const entries = [];
813
- for (const key of keys) {
814
- const envelope = await this.adapter.get(
815
- this.vault,
816
- LEDGER_COLLECTION,
817
- key
818
- );
819
- if (!envelope) continue;
820
- entries.push(await this.decryptEntry(envelope));
821
- }
822
- return entries;
823
- }
824
- /**
825
- * Return the current head of the ledger: the last entry, its hash,
826
- * and the total chain length. `null` on an empty ledger so callers
827
- * can distinguish "no history yet" from "empty history".
828
- */
829
- async head() {
830
- const cached = await this.getCachedHead();
831
- if (!cached) return null;
832
- return {
833
- entry: cached.entry,
834
- hash: cached.hash,
835
- length: cached.entry.index + 1
836
- };
837
- }
838
- /**
839
- * Return entries in the requested half-open range `[from, to)`.
840
- * Defaults: `from = 0`, `to = length`. The indices are clipped to
841
- * the valid range; no error is thrown for out-of-range queries.
842
- */
843
- async entries(opts = {}) {
844
- const all = await this.loadAllEntries();
845
- const from = Math.max(0, opts.from ?? 0);
846
- const to = Math.min(all.length, opts.to ?? all.length);
847
- return all.slice(from, to);
848
- }
849
- /**
850
- * Reconstruct a record's state at a given historical version by
851
- * walking the ledger's delta chain backward from the current state.
852
- *
853
- * ## Algorithm
854
- *
855
- * Ledger deltas are stored in **reverse** form — each entry's
856
- * patch describes how to undo that put, transforming the new
857
- * record back into the previous one. `reconstruct` exploits this
858
- * by:
859
- *
860
- * 1. Finding every ledger entry for `(collection, id)` in the
861
- * chain, sorted by index ascending.
862
- * 2. Starting from `current` (the present value of the record,
863
- * as held by the caller — typically fetched via
864
- * `Collection.get()`).
865
- * 3. Walking entries in **descending** index order and applying
866
- * each entry's reverse patch, stopping when we reach the
867
- * entry whose version equals `atVersion`.
868
- *
869
- * The result is the record as it existed immediately AFTER the
870
- * put at `atVersion`. To get the state at the genesis put
871
- * (version 1), the walk runs all the way back through every put
872
- * after the first.
873
- *
874
- * ## Caveats
875
- *
876
- * - **Delete entries** break the walk: once we see a delete, the
877
- * record didn't exist before that point, so there's nothing to
878
- * reconstruct. We return `null` in that case.
879
- * - **Missing deltas** (e.g., after `pruneHistory` folds old
880
- * entries into a base snapshot) also stop the walk. does
881
- * not ship pruneHistory, so today this only happens if an entry
882
- * was deleted out-of-band.
883
- * - The caller MUST pass the correct current value. Passing a
884
- * mutated object would corrupt the reconstruction — the patch
885
- * chain is only valid against the exact state that was in
886
- * effect when the most recent put happened.
887
- *
888
- * For, `reconstruct` is the only way to read a historical
889
- * version via deltas. The legacy `_history` collection still
890
- * holds full snapshots and `Collection.getVersion()` still reads
891
- * from there — the two paths coexist until pruneHistory lands in
892
- * a follow-up and delta becomes the default.
893
- */
894
- async reconstruct(collection, id, current, atVersion) {
895
- const all = await this.loadAllEntries();
896
- const matching = all.filter(
897
- (e) => e.collection === collection && e.id === id
898
- );
899
- if (matching.length === 0) {
900
- return null;
901
- }
902
- let state = current;
903
- for (let i = matching.length - 1; i >= 0; i--) {
904
- const entry = matching[i];
905
- if (!entry) continue;
906
- if (entry.op !== "put" && entry.op !== "delete") continue;
907
- if (entry.version === atVersion && entry.op !== "delete") {
908
- return state;
909
- }
910
- if (entry.op === "delete") {
911
- return null;
912
- }
913
- if (entry.deltaHash === void 0) {
914
- if (entry.version === atVersion) return state;
915
- return null;
916
- }
917
- const patch = await this.loadDelta(entry.index);
918
- if (!patch) {
919
- return null;
920
- }
921
- if (state === null) {
922
- return null;
923
- }
924
- state = applyPatch(state, patch);
925
- }
926
- return null;
927
- }
928
- /**
929
- * Walk the chain from genesis forward and verify every link.
930
- *
931
- * Returns `{ ok: true, head, length }` if every entry's `prevHash`
932
- * matches the recomputed hash of its predecessor (and the genesis
933
- * entry's `prevHash` is the empty string).
934
- *
935
- * Returns `{ ok: false, divergedAt, expected, actual }` on the first
936
- * mismatch. `divergedAt` is the 0-based index of the BROKEN entry
937
- * — entries before that index still verify cleanly; entries at and
938
- * after `divergedAt` are untrustworthy.
939
- *
940
- * This method detects:
941
- * - Mutated entry content (fields changed)
942
- * - Reordered entries (if any adjacent pair swaps, the prevHash
943
- * of the second no longer matches)
944
- * - Inserted entries (the inserted entry's prevHash likely fails,
945
- * and the following entry's prevHash definitely fails)
946
- * - Deleted entries (the entry after the deletion sees a wrong
947
- * prevHash)
948
- *
949
- * It does NOT detect:
950
- * - Tampering with the DATA collections that bypassed the ledger
951
- * entirely (e.g., an attacker who modifies records without
952
- * appending matching ledger entries — this is why we also
953
- * plan a `verifyIntegrity()` helper in a follow-up)
954
- * - Truncation of the chain at the tail (dropping the last N
955
- * entries leaves a shorter but still consistent chain). External
956
- * anchoring of `head.hash` to a trusted service is the defense
957
- * against this.
958
- */
959
- async verify() {
960
- const entries = await this.loadAllEntries();
961
- let expectedPrevHash = "";
962
- for (let i = 0; i < entries.length; i++) {
963
- const entry = entries[i];
964
- if (!entry) continue;
965
- if (entry.prevHash !== expectedPrevHash) {
966
- return {
967
- ok: false,
968
- divergedAt: i,
969
- expected: expectedPrevHash,
970
- actual: entry.prevHash
971
- };
972
- }
973
- if (entry.index !== i) {
974
- return {
975
- ok: false,
976
- divergedAt: i,
977
- expected: `index=${i}`,
978
- actual: `index=${entry.index}`
979
- };
980
- }
981
- expectedPrevHash = await hashEntry(entry);
982
- }
983
- return {
984
- ok: true,
985
- head: expectedPrevHash,
986
- length: entries.length
987
- };
988
- }
989
- // ─── Encryption plumbing ─────────────────────────────────────────
990
- /**
991
- * Serialize + encrypt a ledger entry into an EncryptedEnvelope. The
992
- * envelope's `_v` field is set to `entry.index + 1` so the usual
993
- * optimistic-concurrency machinery has a reasonable version number
994
- * to compare against (the ledger is append-only, so concurrent
995
- * writes should always bump the index).
996
- */
997
- async encryptEntry(entry) {
998
- const json = canonicalJson(entry);
999
- if (!this.encrypted) {
1000
- return {
1001
- _noydb: NOYDB_FORMAT_VERSION,
1002
- _v: entry.index + 1,
1003
- _ts: entry.ts,
1004
- _iv: "",
1005
- _data: json,
1006
- _by: entry.actor
1007
- };
1008
- }
1009
- const dek = await this.getDEK(LEDGER_COLLECTION);
1010
- const { iv, data } = await encrypt(json, dek);
1011
- return {
1012
- _noydb: NOYDB_FORMAT_VERSION,
1013
- _v: entry.index + 1,
1014
- _ts: entry.ts,
1015
- _iv: iv,
1016
- _data: data,
1017
- _by: entry.actor
1018
- };
1019
- }
1020
- /** Decrypt an envelope into a LedgerEntry. Throws on bad key / tamper. */
1021
- async decryptEntry(envelope) {
1022
- if (!this.encrypted) {
1023
- return JSON.parse(envelope._data);
1024
- }
1025
- const dek = await this.getDEK(LEDGER_COLLECTION);
1026
- const json = await decrypt(envelope._iv, envelope._data, dek);
1027
- return JSON.parse(json);
1028
- }
1029
- };
1030
- function sleepBackoff(attempt) {
1031
- const base = 5 * Math.pow(2, attempt);
1032
- const jitter = Math.random() * base;
1033
- return new Promise((resolve) => setTimeout(resolve, base + jitter));
1034
- }
1035
-
1036
- // src/history/time-machine.ts
1037
- var VaultInstant = class {
1038
- constructor(engine, timestamp) {
1039
- this.engine = engine;
1040
- this.timestamp = timestamp;
1041
- }
1042
- engine;
1043
- timestamp;
1044
- /** Get a point-in-time view of a collection. */
1045
- collection(name) {
1046
- return new CollectionInstant(this.engine, this.timestamp, name);
1047
- }
1048
- };
1049
- var CollectionInstant = class {
1050
- constructor(engine, targetTs, name) {
1051
- this.engine = engine;
1052
- this.targetTs = targetTs;
1053
- this.name = name;
1054
- }
1055
- engine;
1056
- targetTs;
1057
- name;
1058
- /**
1059
- * Return the record as it existed at the target timestamp, or
1060
- * `null` if the record had not been created yet or had already been
1061
- * deleted by then.
1062
- */
1063
- async get(id) {
1064
- const envelope = await this.resolveEnvelope(id);
1065
- if (!envelope) return null;
1066
- const plaintext = this.engine.encrypted ? await decrypt(envelope._iv, envelope._data, await this.engine.getDEK(this.name)) : envelope._data;
1067
- return JSON.parse(plaintext);
1068
- }
1069
- /**
1070
- * IDs of records that existed (had at least one `put` and were not
1071
- * subsequently deleted) at the target timestamp.
1072
- *
1073
- * Implemented as a linear scan over history + ledger. Performance
1074
- * is bounded by total history size (not live-vault size), so the
1075
- * memory-first vault-scale cap (1K–50K records × average history
1076
- * depth) still applies.
1077
- */
1078
- async list() {
1079
- const historyIds = await collectHistoryIds(this.engine.adapter, this.engine.name, this.name);
1080
- const liveIds = await this.engine.adapter.list(this.engine.name, this.name);
1081
- const candidateIds = /* @__PURE__ */ new Set([...historyIds, ...liveIds]);
1082
- const alive = [];
1083
- for (const id of candidateIds) {
1084
- const env = await this.resolveEnvelope(id);
1085
- if (env) alive.push(id);
1086
- }
1087
- return alive.sort();
1088
- }
1089
- // ── write guards ───────────────────────────────────────────────────
1090
- async put(_id, _record) {
1091
- throw new ReadOnlyAtInstantError("put", this.targetTs);
1092
- }
1093
- async delete(_id) {
1094
- throw new ReadOnlyAtInstantError("delete", this.targetTs);
1095
- }
1096
- async update(_id, _patch) {
1097
- throw new ReadOnlyAtInstantError("update", this.targetTs);
1098
- }
1099
- // ── internals ─────────────────────────────────────────────────────
1100
- /**
1101
- * Return the envelope that represents the record's state at
1102
- * `targetTs`, accounting for deletes. `null` if the record didn't
1103
- * exist at that instant.
1104
- *
1105
- * ## Why we use the ledger as the authoritative timeline
1106
- *
1107
- * The per-version history snapshots saved by `saveHistory()` do
1108
- * carry a `_ts` field, but that timestamp is the moment the
1109
- * snapshot was *captured* (i.e. the instant right before the
1110
- * subsequent overwrite), not the original write time. The ledger,
1111
- * by contrast, records `ts` at the moment of each `put` / `delete`
1112
- * — it's the only source that tracks the real timeline. So:
1113
- *
1114
- * 1. Walk the ledger; find the latest entry for `(collection, id)`
1115
- * with `ts ≤ targetTs`.
1116
- * 2. If that entry is a `delete`, the record was gone at the
1117
- * target instant — return null.
1118
- * 3. Otherwise it's a `put` with a specific `version`. Load the
1119
- * envelope for that version from history, falling back to the
1120
- * live collection for the most recent version.
1121
- *
1122
- * ## Fallback when the ledger is disabled
1123
- *
1124
- * If the vault has history disabled, `getLedger()` returns null and
1125
- * we fall back to comparing envelope `_ts` fields. This is
1126
- * approximate and gets the *last write* right but may confuse the
1127
- * intermediate versions; adopters needing accurate time-machine
1128
- * reads should leave history enabled.
1129
- */
1130
- async resolveEnvelope(id) {
1131
- const ledger = this.engine.getLedger();
1132
- if (ledger) {
1133
- return this.resolveViaLedger(id, ledger);
1134
- }
1135
- return this.resolveViaEnvelopeTs(id);
1136
- }
1137
- async resolveViaLedger(id, ledger) {
1138
- const entries = await ledger.entries();
1139
- let latest = null;
1140
- for (const e of entries) {
1141
- if (e.collection !== this.name || e.id !== id) continue;
1142
- if (e.ts > this.targetTs) break;
1143
- if (e.op === "amendment" || e.op === "lifecycle" || e.op === "forget") continue;
1144
- latest = { op: e.op === "migration" ? "put" : e.op, version: e.version };
1145
- }
1146
- if (!latest) return null;
1147
- if (latest.op === "delete") return null;
1148
- return this.loadVersion(id, latest.version);
1149
- }
1150
- async resolveViaEnvelopeTs(id) {
1151
- const history = await getHistory(
1152
- this.engine.adapter,
1153
- this.engine.name,
1154
- this.name,
1155
- id
1156
- );
1157
- const live = await this.engine.adapter.get(this.engine.name, this.name, id);
1158
- const byVersion = /* @__PURE__ */ new Map();
1159
- for (const e of history) byVersion.set(e._v, e);
1160
- if (live) byVersion.set(live._v, live);
1161
- const sorted = [...byVersion.values()].sort(
1162
- (a, b) => a._ts < b._ts ? 1 : a._ts > b._ts ? -1 : 0
1163
- );
1164
- return sorted.find((e) => e._ts <= this.targetTs) ?? null;
1165
- }
1166
- /**
1167
- * Fetch the envelope for a specific version. The live record (most
1168
- * recent put) lives in the main collection; prior versions live in
1169
- * `_history`. We check live first because the common case after a
1170
- * delete is that we're trying to load the last-live version from
1171
- * history, and skipping live for the current-version case avoids a
1172
- * redundant lookup.
1173
- */
1174
- async loadVersion(id, version) {
1175
- const live = await this.engine.adapter.get(this.engine.name, this.name, id);
1176
- if (live && live._v === version) return live;
1177
- const historyId2 = `${this.name}:${id}:${String(version).padStart(10, "0")}`;
1178
- return await this.engine.adapter.get(this.engine.name, "_history", historyId2);
1179
- }
1180
- };
1181
- async function collectHistoryIds(adapter, vault, collection) {
1182
- const all = await adapter.list(vault, "_history");
1183
- const prefix = `${collection}:`;
1184
- const seen = /* @__PURE__ */ new Set();
1185
- for (const key of all) {
1186
- if (!key.startsWith(prefix)) continue;
1187
- const lastColon = key.lastIndexOf(":");
1188
- if (lastColon <= prefix.length) continue;
1189
- const middle = key.slice(prefix.length, lastColon);
1190
- seen.add(middle);
1191
- }
1192
- return [...seen];
1193
- }
1194
-
1195
- // src/history/active.ts
1196
- function withHistory() {
1197
- return {
1198
- saveHistory,
1199
- getHistoryEntries: getHistory,
1200
- getVersionEnvelope,
1201
- pruneHistory,
1202
- clearHistory,
1203
- tombstoneHistory,
1204
- envelopePayloadHash,
1205
- computePatch,
1206
- diff,
1207
- buildLedger(opts) {
1208
- return new LedgerStore({
1209
- adapter: opts.adapter,
1210
- vault: opts.vault,
1211
- encrypted: opts.encrypted,
1212
- getDEK: opts.getDEK,
1213
- actor: opts.actor
1214
- });
1215
- },
1216
- buildVaultInstant(engine, timestamp) {
1217
- return new VaultInstant(engine, timestamp);
1218
- }
1219
- };
1220
- }
1221
- // Annotate the CommonJS export names for ESM import in node:
1222
- 0 && (module.exports = {
1223
- CollectionInstant,
1224
- LEDGER_COLLECTION,
1225
- LEDGER_DELTAS_COLLECTION,
1226
- LedgerStore,
1227
- VaultInstant,
1228
- applyPatch,
1229
- canonicalJson,
1230
- clearHistory,
1231
- computePatch,
1232
- diff,
1233
- envelopePayloadHash,
1234
- formatDiff,
1235
- getHistory,
1236
- getVersionEnvelope,
1237
- hashEntry,
1238
- paddedIndex,
1239
- parseIndex,
1240
- pruneHistory,
1241
- saveHistory,
1242
- sha256Hex,
1243
- withHistory
1244
- });
1245
- //# sourceMappingURL=index.cjs.map