pactium 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +92 -0
  3. package/README.zh-CN.md +90 -0
  4. package/SECURITY.md +7 -0
  5. package/bin/pactium.mjs +121 -0
  6. package/docs/LICOLITE-ASPECT.md +57 -0
  7. package/docs/README.md +13 -0
  8. package/docs/TERM.md +289 -0
  9. package/docs/architecture/ARCHITECTURE.md +62 -0
  10. package/docs/protocols/PROFILE.md +124 -0
  11. package/docs/protocols/PROTOCOLS.md +62 -0
  12. package/examples/record-operation.mjs +26 -0
  13. package/package.json +69 -0
  14. package/src/README.md +13 -0
  15. package/src/aspects/licolite/aspect.js +278 -0
  16. package/src/aspects/licolite/constants.js +13 -0
  17. package/src/aspects/licolite/evidence.js +47 -0
  18. package/src/aspects/licolite/index.d.ts +51 -0
  19. package/src/aspects/licolite/index.js +19 -0
  20. package/src/aspects/licolite/signing.js +78 -0
  21. package/src/canonical/value.js +40 -0
  22. package/src/core/append-condition.js +102 -0
  23. package/src/core/pactium-core.js +1073 -0
  24. package/src/core/tracking-cursor.js +68 -0
  25. package/src/http.js +99 -0
  26. package/src/index-engine/snapshot-merkle-index.js +994 -0
  27. package/src/index.d.ts +244 -0
  28. package/src/index.js +73 -0
  29. package/src/ledger/signed-head.js +204 -0
  30. package/src/ledger/transparency-log.js +702 -0
  31. package/src/maintenance/task-engine.js +36 -0
  32. package/src/proof/bundle-format.js +265 -0
  33. package/src/proof/bundle.js +77 -0
  34. package/src/proof/envelope.js +548 -0
  35. package/src/proof/registry.js +18 -0
  36. package/src/protocol/constants.js +69 -0
  37. package/src/protocol/hashing.js +47 -0
  38. package/src/quality/profile-runner.js +291 -0
  39. package/src/repair/planner.js +62 -0
  40. package/src/shared/records.js +32 -0
  41. package/src/storage/local-json-storage-port.js +360 -0
  42. package/src/verification/failure.js +31 -0
@@ -0,0 +1,994 @@
1
+ import {
2
+ PACTIUM_INDEX_ENGINE,
3
+ PACTIUM_INDEX_SPLITTER,
4
+ PACTIUM_PROOF_TYPES,
5
+ PACTIUM_PROTOCOL,
6
+ PACTIUM_PROTOCOL_PROFILE,
7
+ PACTIUM_SCHEMA_VERSION
8
+ } from "../protocol/constants.js";
9
+ import { canonicalEncode, canonicalString, normalizeCanonicalValue } from "../canonical/value.js";
10
+ import { cidForCanonical, hexFromCid, protocolHashHex } from "../protocol/hashing.js";
11
+ import { createStoragePort } from "../storage/local-json-storage-port.js";
12
+ import { asArray, asRecord, safeToken } from "../shared/records.js";
13
+
14
+ const INDEX_NODE_TYPE = "pactium.index.node";
15
+ function compareIndexKeys(left, right) {
16
+ return String(left || "") < String(right || "")
17
+ ? -1
18
+ : String(left || "") > String(right || "")
19
+ ? 1
20
+ : 0;
21
+ }
22
+
23
+ function splitterConfig() {
24
+ return {
25
+ algorithm: PACTIUM_INDEX_SPLITTER,
26
+ minEntries: PACTIUM_PROTOCOL_PROFILE.indexEngine.chunking.minEntries,
27
+ targetEntries: PACTIUM_PROTOCOL_PROFILE.indexEngine.chunking.targetEntries,
28
+ maxEntries: PACTIUM_PROTOCOL_PROFILE.indexEngine.chunking.maxEntries,
29
+ boundaryMask: PACTIUM_PROTOCOL_PROFILE.indexEngine.chunking.boundaryMask
30
+ };
31
+ }
32
+
33
+ function indexLeafHash(entry) {
34
+ return protocolHashHex("index.leaf", {
35
+ key: entry.key,
36
+ valueRef: entry.valueRef,
37
+ valueHash: entry.valueHash || "",
38
+ metadata: asRecord(entry.metadata)
39
+ });
40
+ }
41
+
42
+ function normalizeIndexEntry(entry) {
43
+ return {
44
+ key: String(entry.key || ""),
45
+ valueRef: String(entry.valueRef || ""),
46
+ valueHash: String(entry.valueHash || ""),
47
+ metadata: normalizeCanonicalValue(asRecord(entry.metadata))
48
+ };
49
+ }
50
+
51
+ function normalizeEntries(entries) {
52
+ const normalizedEntries = asArray(entries)
53
+ .map(normalizeIndexEntry)
54
+ .filter((entry) => entry.key)
55
+ .sort((left, right) => compareIndexKeys(left.key, right.key));
56
+ const deduped = [];
57
+ for (const entry of normalizedEntries) {
58
+ if (deduped.length > 0 && deduped[deduped.length - 1].key === entry.key) deduped[deduped.length - 1] = entry;
59
+ else deduped.push(entry);
60
+ }
61
+ return deduped;
62
+ }
63
+
64
+ function rangeForEntries(entries) {
65
+ return {
66
+ min: entries[0]?.key || "",
67
+ max: entries[entries.length - 1]?.key || ""
68
+ };
69
+ }
70
+
71
+ function rangeForChildren(children) {
72
+ return {
73
+ min: children[0]?.keyRange?.min || "",
74
+ max: children[children.length - 1]?.keyRange?.max || ""
75
+ };
76
+ }
77
+
78
+ function descriptorFromNodePayload(payload, root = cidForCanonical(payload)) {
79
+ return {
80
+ root,
81
+ rootHash: hexFromCid(root),
82
+ level: Number(payload.level || 0),
83
+ count: Number(payload.count || 0),
84
+ keyRange: payload.keyRange || { min: "", max: "" }
85
+ };
86
+ }
87
+
88
+ function finalizeNodePayload(payload) {
89
+ const base = normalizeCanonicalValue({
90
+ ...payload,
91
+ byteLength: undefined
92
+ });
93
+ return {
94
+ ...base,
95
+ byteLength: canonicalEncode(base).length
96
+ };
97
+ }
98
+
99
+ function descriptorMatches(left, right) {
100
+ return left?.root === right?.root &&
101
+ left?.rootHash === right?.rootHash &&
102
+ Number(left?.level || 0) === Number(right?.level || 0) &&
103
+ Number(left?.count || 0) === Number(right?.count || 0) &&
104
+ left?.keyRange?.min === right?.keyRange?.min &&
105
+ left?.keyRange?.max === right?.keyRange?.max;
106
+ }
107
+
108
+ function entryBoundaryHash(domain, entry) {
109
+ return protocolHashHex("index.boundary", {
110
+ domain,
111
+ key: entry.key,
112
+ valueRef: entry.valueRef,
113
+ valueHash: entry.valueHash
114
+ });
115
+ }
116
+
117
+ function childBoundaryHash(domain, child) {
118
+ return protocolHashHex("index.boundary", {
119
+ domain,
120
+ key: child.keyRange?.max || "",
121
+ root: child.root,
122
+ rootHash: child.rootHash,
123
+ level: child.level,
124
+ count: child.count
125
+ });
126
+ }
127
+
128
+ function first32(hash) {
129
+ return Number.parseInt(String(hash || "").slice(0, 8), 16) || 0;
130
+ }
131
+
132
+ function shouldCutChunk({ size, boundaryHash, splitter }) {
133
+ if (size < splitter.minEntries) return false;
134
+ return (first32(boundaryHash) & splitter.boundaryMask) === 0 || size >= splitter.maxEntries;
135
+ }
136
+
137
+ function ensureSortedEntries(entries) {
138
+ for (let index = 1; index < entries.length; index += 1) {
139
+ if (compareIndexKeys(entries[index - 1].key, entries[index].key) >= 0) return false;
140
+ }
141
+ return true;
142
+ }
143
+
144
+ function validateLeafNodePayload(payload) {
145
+ if (!payload || payload.protocol !== PACTIUM_PROTOCOL || payload.nodeType !== INDEX_NODE_TYPE || payload.level !== 0) return false;
146
+ const entries = asArray(payload.entries).map(normalizeIndexEntry);
147
+ if (entries.length !== Number(payload.count || 0)) return false;
148
+ if (canonicalString(entries) !== canonicalString(asArray(payload.entries))) return false;
149
+ if (!ensureSortedEntries(entries)) return false;
150
+ return canonicalString(rangeForEntries(entries)) === canonicalString(payload.keyRange || {});
151
+ }
152
+
153
+ function validateInternalNodePayload(payload) {
154
+ if (!payload || payload.protocol !== PACTIUM_PROTOCOL || payload.nodeType !== INDEX_NODE_TYPE || Number(payload.level || 0) <= 0) return false;
155
+ const children = asArray(payload.children);
156
+ if (children.length === 0 || asArray(payload.entries).length > 0) return false;
157
+ const count = children.reduce((total, child) => total + Number(child.count || 0), 0);
158
+ if (count !== Number(payload.count || 0)) return false;
159
+ for (let index = 1; index < children.length; index += 1) {
160
+ if (compareIndexKeys(children[index - 1].keyRange?.max, children[index].keyRange?.min) >= 0) return false;
161
+ }
162
+ return canonicalString(rangeForChildren(children)) === canonicalString(payload.keyRange || {});
163
+ }
164
+
165
+ function verifyNodePayload(payload, expected = {}) {
166
+ const finalized = finalizeNodePayload(payload);
167
+ const root = cidForCanonical(finalized);
168
+ const validShape = finalized.level === 0 ? validateLeafNodePayload(finalized) : validateInternalNodePayload(finalized);
169
+ return validShape &&
170
+ (!expected.root || expected.root === root) &&
171
+ (!expected.rootHash || expected.rootHash === hexFromCid(root));
172
+ }
173
+
174
+ function findChildIndex(children, key) {
175
+ if (children.length === 0) return -1;
176
+ const normalizedKey = String(key || "");
177
+ const index = children.findIndex((child) => compareIndexKeys(normalizedKey, child.keyRange?.max) <= 0);
178
+ return index >= 0 ? index : children.length - 1;
179
+ }
180
+
181
+ function rangesIntersect(range, min, max) {
182
+ const rangeMin = String(range?.min || "");
183
+ const rangeMax = String(range?.max || "");
184
+ return (!max || compareIndexKeys(rangeMin, max) <= 0) && (!min || compareIndexKeys(rangeMax, min) >= 0);
185
+ }
186
+
187
+ function clampLimit(limit, fallback = 5000) {
188
+ const value = limit === undefined || limit === null || limit === "" ? fallback : Number(limit);
189
+ return Math.max(1, Math.min(Number.isFinite(value) ? value : fallback, 100000));
190
+ }
191
+
192
+ function proofEntryPath(path) {
193
+ return asArray(path).map((item) => ({
194
+ nodeRoot: String(item.nodeRoot || ""),
195
+ level: Number(item.level || 0),
196
+ keyRange: normalizeCanonicalValue(item.keyRange || { min: "", max: "" }),
197
+ siblingDescriptors: asArray(item.siblingDescriptors).map((child) => normalizeCanonicalValue(child)),
198
+ childIndex: Number(item.childIndex || 0),
199
+ nodeHash: String(item.nodeHash || "")
200
+ }));
201
+ }
202
+
203
+ function nodePayloadFromProofLeaf(proof) {
204
+ const leafNode = asRecord(proof.leafNode || proof.containingLeaf?.leafNode);
205
+ return finalizeNodePayload({
206
+ protocol: PACTIUM_PROTOCOL,
207
+ schema: PACTIUM_SCHEMA_VERSION,
208
+ nodeType: INDEX_NODE_TYPE,
209
+ domain: proof.domain,
210
+ level: 0,
211
+ keyRange: leafNode.keyRange || rangeForEntries(asArray(leafNode.entries).map(normalizeIndexEntry)),
212
+ count: Number(leafNode.count ?? asArray(leafNode.entries).length),
213
+ entries: asArray(leafNode.entries).map(normalizeIndexEntry),
214
+ children: [],
215
+ splitter: leafNode.splitter || splitterConfig()
216
+ });
217
+ }
218
+
219
+ function verifyPathToRoot({ proof, leafDescriptor, selectionKey = null }) {
220
+ let current = leafDescriptor;
221
+ const path = proofEntryPath(proof.path || proof.containingLeaf?.path);
222
+ for (const pathItem of path) {
223
+ const children = pathItem.siblingDescriptors.map((child) => ({ ...child }));
224
+ if (pathItem.childIndex < 0 || pathItem.childIndex >= children.length) return null;
225
+ if (selectionKey !== null && pathItem.childIndex !== findChildIndex(children, selectionKey)) return null;
226
+ if (!descriptorMatches(children[pathItem.childIndex], current)) return null;
227
+ const parentPayload = finalizeNodePayload({
228
+ protocol: PACTIUM_PROTOCOL,
229
+ schema: PACTIUM_SCHEMA_VERSION,
230
+ nodeType: INDEX_NODE_TYPE,
231
+ domain: proof.domain,
232
+ level: pathItem.level,
233
+ keyRange: pathItem.keyRange,
234
+ count: children.reduce((total, child) => total + Number(child.count || 0), 0),
235
+ entries: [],
236
+ children,
237
+ splitter: splitterConfig()
238
+ });
239
+ if (!verifyNodePayload(parentPayload, { rootHash: pathItem.nodeHash })) return null;
240
+ current = descriptorFromNodePayload(parentPayload);
241
+ if (current.root !== pathItem.nodeRoot) return null;
242
+ }
243
+ return current;
244
+ }
245
+
246
+ export function verifyIndexProof(proof) {
247
+ if (!proof || typeof proof !== "object") return false;
248
+ if (![PACTIUM_PROOF_TYPES.indexMembership, PACTIUM_PROOF_TYPES.indexNonMembership].includes(proof.proofType)) {
249
+ return false;
250
+ }
251
+ const leafPayload = nodePayloadFromProofLeaf(proof);
252
+ const leafRoot = cidForCanonical(leafPayload);
253
+ const leafRootHash = hexFromCid(leafRoot);
254
+ if (!verifyNodePayload(leafPayload, { root: proof.leafRoot || proof.containingLeaf?.leafRoot || leafRoot })) return false;
255
+ if ((proof.leafRoot && proof.leafRoot !== leafRoot) || (proof.leafRootHash && proof.leafRootHash !== leafRootHash)) return false;
256
+ const leafDescriptor = descriptorFromNodePayload(leafPayload, leafRoot);
257
+ const normalizedKey = String(proof.key || "");
258
+ const rootDescriptor = verifyPathToRoot({ proof, leafDescriptor, selectionKey: normalizedKey });
259
+ if (!rootDescriptor) return false;
260
+ if (rootDescriptor.root !== proof.indexRoot || rootDescriptor.rootHash !== proof.rootHash) return false;
261
+ const entries = asArray(leafPayload.entries).map(normalizeIndexEntry);
262
+ if (proof.proofType === PACTIUM_PROOF_TYPES.indexMembership) {
263
+ const entry = normalizeIndexEntry(proof.entry || {});
264
+ const found = entries.find((candidate) => candidate.key === normalizedKey);
265
+ return Boolean(found) &&
266
+ canonicalString(found) === canonicalString(entry) &&
267
+ indexLeafHash(entry) === proof.leafHash;
268
+ }
269
+ const containsKey = entries.some((entry) => entry.key === normalizedKey);
270
+ const insertionPoint = entries.findIndex((entry) => compareIndexKeys(entry.key, normalizedKey) > 0);
271
+ const left = insertionPoint < 0
272
+ ? entries[entries.length - 1] || null
273
+ : insertionPoint === 0
274
+ ? null
275
+ : entries[insertionPoint - 1];
276
+ const right = insertionPoint < 0 ? null : entries[insertionPoint];
277
+ return !containsKey &&
278
+ String(proof.leftBoundary || "") === (left?.key || "") &&
279
+ String(proof.rightBoundary || "") === (right?.key || "");
280
+ }
281
+
282
+ export function createVerifiableIndexEngine({ storage = createStoragePort({ inMemory: true }), domain = "generic" } = {}) {
283
+ const roots = new Map();
284
+ const nodes = new Map();
285
+ const snapshots = new Map();
286
+
287
+ async function putNode(payload) {
288
+ const finalized = finalizeNodePayload(payload);
289
+ const refs = asArray(finalized.children).map((child) => child.root).filter(Boolean);
290
+ const block = await storage.putBlock(finalized, { kind: `index-node:${finalized.domain}`, refs });
291
+ const descriptor = descriptorFromNodePayload(finalized, block.cid);
292
+ nodes.set(block.cid, finalized);
293
+ return { payload: finalized, descriptor };
294
+ }
295
+
296
+ async function readNode(root) {
297
+ if (!root) return null;
298
+ if (nodes.has(root)) return nodes.get(root);
299
+ const block = await storage.getBlock(root);
300
+ if (!block) throw new Error(`Index node missing for ${root}`);
301
+ const payload = normalizeCanonicalValue(JSON.parse(Buffer.from(block.payloadBase64, "base64").toString("utf8")));
302
+ if (!verifyNodePayload(payload, { root })) throw new Error(`Index node integrity failure for ${root}`);
303
+ nodes.set(root, payload);
304
+ return payload;
305
+ }
306
+
307
+ async function writeLeafNodes(entries, snapshotDomain) {
308
+ const splitter = splitterConfig();
309
+ const chunks = [];
310
+ let active = [];
311
+ for (const entry of entries) {
312
+ active.push(entry);
313
+ if (shouldCutChunk({ size: active.length, boundaryHash: entryBoundaryHash(snapshotDomain, entry), splitter })) {
314
+ chunks.push(active);
315
+ active = [];
316
+ }
317
+ }
318
+ if (active.length > 0 || chunks.length === 0) chunks.push(active);
319
+ const descriptors = [];
320
+ for (const chunk of chunks) {
321
+ const { descriptor } = await putNode({
322
+ protocol: PACTIUM_PROTOCOL,
323
+ schema: PACTIUM_SCHEMA_VERSION,
324
+ nodeType: INDEX_NODE_TYPE,
325
+ domain: snapshotDomain,
326
+ level: 0,
327
+ keyRange: rangeForEntries(chunk),
328
+ count: chunk.length,
329
+ entries: chunk,
330
+ children: [],
331
+ splitter
332
+ });
333
+ descriptors.push(descriptor);
334
+ }
335
+ return descriptors;
336
+ }
337
+
338
+ async function writeParentLevel(children, snapshotDomain, level) {
339
+ const splitter = splitterConfig();
340
+ const chunks = [];
341
+ let active = [];
342
+ for (const child of children) {
343
+ active.push(child);
344
+ if (shouldCutChunk({ size: active.length, boundaryHash: childBoundaryHash(snapshotDomain, child), splitter })) {
345
+ chunks.push(active);
346
+ active = [];
347
+ }
348
+ }
349
+ if (active.length > 0) chunks.push(active);
350
+ const descriptors = [];
351
+ for (const chunk of chunks) {
352
+ const { descriptor } = await putNode({
353
+ protocol: PACTIUM_PROTOCOL,
354
+ schema: PACTIUM_SCHEMA_VERSION,
355
+ nodeType: INDEX_NODE_TYPE,
356
+ domain: snapshotDomain,
357
+ level,
358
+ keyRange: rangeForChildren(chunk),
359
+ count: chunk.reduce((total, child) => total + Number(child.count || 0), 0),
360
+ entries: [],
361
+ children: chunk,
362
+ splitter
363
+ });
364
+ descriptors.push(descriptor);
365
+ }
366
+ return descriptors;
367
+ }
368
+
369
+ async function writeIndexRoot(entries, snapshotDomain = domain) {
370
+ const normalizedEntries = normalizeEntries(entries);
371
+ let descriptors = await writeLeafNodes(normalizedEntries, snapshotDomain);
372
+ let height = 0;
373
+ while (descriptors.length > 1) {
374
+ height += 1;
375
+ descriptors = await writeParentLevel(descriptors, snapshotDomain, height);
376
+ }
377
+ const rootDescriptor = descriptors[0];
378
+ const indexRoot = {
379
+ protocol: PACTIUM_PROTOCOL,
380
+ schema: PACTIUM_SCHEMA_VERSION,
381
+ engine: PACTIUM_INDEX_ENGINE,
382
+ domain: snapshotDomain,
383
+ root: rootDescriptor.root,
384
+ rootHash: rootDescriptor.rootHash,
385
+ count: rootDescriptor.count,
386
+ keyRange: rootDescriptor.keyRange,
387
+ height: rootDescriptor.level,
388
+ splitter: splitterConfig()
389
+ };
390
+ roots.set(rootDescriptor.root, indexRoot);
391
+ await storage.putProtocolObject("index", `${safeToken(snapshotDomain)}-${rootDescriptor.rootHash}`, indexRoot);
392
+ if (snapshotDomain !== domain) {
393
+ await storage.putProtocolObject("index", `${safeToken(domain)}-${rootDescriptor.rootHash}`, indexRoot);
394
+ }
395
+ snapshots.delete(rootDescriptor.root);
396
+ return indexRoot;
397
+ }
398
+
399
+ async function writeIndexRootFromDescriptor(rootDescriptor, snapshotDomain = domain) {
400
+ const indexRoot = {
401
+ protocol: PACTIUM_PROTOCOL,
402
+ schema: PACTIUM_SCHEMA_VERSION,
403
+ engine: PACTIUM_INDEX_ENGINE,
404
+ domain: snapshotDomain,
405
+ root: rootDescriptor.root,
406
+ rootHash: rootDescriptor.rootHash,
407
+ count: rootDescriptor.count,
408
+ keyRange: rootDescriptor.keyRange,
409
+ height: rootDescriptor.level,
410
+ splitter: splitterConfig()
411
+ };
412
+ roots.set(rootDescriptor.root, indexRoot);
413
+ await storage.putProtocolObject("index", `${safeToken(snapshotDomain)}-${rootDescriptor.rootHash}`, indexRoot);
414
+ if (snapshotDomain !== domain) {
415
+ await storage.putProtocolObject("index", `${safeToken(domain)}-${rootDescriptor.rootHash}`, indexRoot);
416
+ }
417
+ snapshots.delete(rootDescriptor.root);
418
+ return indexRoot;
419
+ }
420
+
421
+ async function readIndexRoot(root) {
422
+ if (!root) {
423
+ const empty = await writeIndexRoot([], domain);
424
+ return empty;
425
+ }
426
+ if (roots.has(root)) return roots.get(root);
427
+ const object = await storage.getProtocolObject("index", `${safeToken(domain)}-${hexFromCid(root)}`, null);
428
+ if (!object) throw new Error(`Index snapshot missing for ${root}`);
429
+ roots.set(root, object);
430
+ return object;
431
+ }
432
+
433
+ async function collectEntriesFromDescriptor(descriptor) {
434
+ if (!descriptor?.root) return [];
435
+ const payload = await readNode(descriptor.root);
436
+ if (Number(payload.level || 0) === 0) return asArray(payload.entries).map(normalizeIndexEntry);
437
+ const nested = [];
438
+ for (const child of asArray(payload.children)) {
439
+ nested.push(...await collectEntriesFromDescriptor(child));
440
+ }
441
+ return nested;
442
+ }
443
+
444
+ async function collectEntries(root) {
445
+ const indexRoot = await readIndexRoot(root);
446
+ return collectEntriesFromDescriptor({
447
+ root: indexRoot.root,
448
+ rootHash: indexRoot.rootHash,
449
+ level: indexRoot.height,
450
+ count: indexRoot.count,
451
+ keyRange: indexRoot.keyRange
452
+ });
453
+ }
454
+
455
+ async function rangeEntriesFromDescriptor(descriptor, {
456
+ min = "",
457
+ max = "\uffff",
458
+ after = "",
459
+ limit = 5000,
460
+ predicate = () => true
461
+ } = {}, output = []) {
462
+ if (!descriptor?.root || output.length >= limit) return output;
463
+ if (!rangesIntersect(descriptor.keyRange, min, max)) return output;
464
+ if (after && compareIndexKeys(descriptor.keyRange?.max, after) <= 0) return output;
465
+ const payload = await readNode(descriptor.root);
466
+ if (Number(payload.level || 0) === 0) {
467
+ for (const entry of asArray(payload.entries).map(normalizeIndexEntry)) {
468
+ if (output.length >= limit) break;
469
+ if (after && compareIndexKeys(entry.key, after) <= 0) continue;
470
+ if (compareIndexKeys(entry.key, min) < 0 || compareIndexKeys(entry.key, max) > 0) continue;
471
+ if (predicate(entry)) output.push(entry);
472
+ }
473
+ return output;
474
+ }
475
+ for (const child of asArray(payload.children)) {
476
+ if (output.length >= limit) break;
477
+ await rangeEntriesFromDescriptor(child, { min, max, after, limit, predicate }, output);
478
+ }
479
+ return output;
480
+ }
481
+
482
+ async function rangeEntries(root, options = {}) {
483
+ const indexRoot = await readIndexRoot(root);
484
+ return rangeEntriesFromDescriptor({
485
+ root: indexRoot.root,
486
+ rootHash: indexRoot.rootHash,
487
+ level: indexRoot.height,
488
+ count: indexRoot.count,
489
+ keyRange: indexRoot.keyRange
490
+ }, options);
491
+ }
492
+
493
+ async function readSnapshot(root) {
494
+ if (!root) {
495
+ const empty = await writeIndexRoot([], domain);
496
+ root = empty.root;
497
+ }
498
+ if (snapshots.has(root)) return snapshots.get(root);
499
+ const indexRoot = await readIndexRoot(root);
500
+ const entries = await collectEntries(root);
501
+ const leafHashes = entries.map(indexLeafHash);
502
+ const snapshot = {
503
+ ...indexRoot,
504
+ entries,
505
+ leafHashes,
506
+ chunkBoundaries: await chunkBoundaries(root)
507
+ };
508
+ snapshots.set(root, snapshot);
509
+ return snapshot;
510
+ }
511
+
512
+ async function chunkBoundaries(root) {
513
+ const indexRoot = await readIndexRoot(root);
514
+ const output = [];
515
+ async function visit(descriptor) {
516
+ const payload = await readNode(descriptor.root);
517
+ if (Number(payload.level || 0) === 0) {
518
+ output.push({
519
+ startKey: payload.keyRange?.min || "",
520
+ endKey: payload.keyRange?.max || "",
521
+ count: Number(payload.count || 0),
522
+ root: descriptor.root,
523
+ rootHash: descriptor.rootHash
524
+ });
525
+ return;
526
+ }
527
+ for (const child of asArray(payload.children)) await visit(child);
528
+ }
529
+ if (indexRoot.root) await visit({
530
+ root: indexRoot.root,
531
+ rootHash: indexRoot.rootHash,
532
+ level: indexRoot.height,
533
+ count: indexRoot.count,
534
+ keyRange: indexRoot.keyRange
535
+ });
536
+ return output;
537
+ }
538
+
539
+ async function createIndex(entries = [], options = {}) {
540
+ return writeIndexRoot(entries, options.domain || domain);
541
+ }
542
+
543
+ async function rechunkToSingleRoot(descriptors, snapshotDomain) {
544
+ let current = descriptors;
545
+ while (current.length > 1) {
546
+ current = await writeParentLevel(current, snapshotDomain, Number(current[0]?.level || 0) + 1);
547
+ }
548
+ return current[0];
549
+ }
550
+
551
+ async function rewritePathWithDescriptors(path, replacementDescriptors, snapshotDomain) {
552
+ let currentDescriptors = replacementDescriptors;
553
+ let replaceStart = null;
554
+ let replaceEnd = null;
555
+ for (const pathItem of asArray(path)) {
556
+ const children = asArray(pathItem.siblingDescriptors);
557
+ const start = replaceStart ?? Number(pathItem.replaceStart ?? pathItem.childIndex);
558
+ const end = replaceEnd ?? Number(pathItem.replaceEnd ?? pathItem.childIndex);
559
+ const nextChildren = [
560
+ ...children.slice(0, start),
561
+ ...currentDescriptors,
562
+ ...children.slice(end + 1)
563
+ ];
564
+ currentDescriptors = await writeParentLevel(nextChildren, snapshotDomain, Number(pathItem.level || 1));
565
+ replaceStart = null;
566
+ replaceEnd = null;
567
+ }
568
+ return rechunkToSingleRoot(currentDescriptors, snapshotDomain);
569
+ }
570
+
571
+ async function mutateLocal(root, key, mutation, options = {}) {
572
+ const indexRoot = await readIndexRoot(root);
573
+ const snapshotDomain = options.domain || indexRoot.domain || domain;
574
+ const normalizedKey = String(key || "");
575
+ if (!normalizedKey) return indexRoot;
576
+ const found = await findLeaf(root, normalizedKey);
577
+ const immediateParent = found.path[0] || null;
578
+ let localDescriptors = [found.leafDescriptor];
579
+ let replaceStart = 0;
580
+ let replaceEnd = 0;
581
+ if (immediateParent) {
582
+ const siblings = asArray(immediateParent.siblingDescriptors);
583
+ replaceStart = Math.max(0, Number(immediateParent.childIndex || 0) - 1);
584
+ replaceEnd = Math.min(siblings.length - 1, Number(immediateParent.childIndex || 0) + 1);
585
+ localDescriptors = siblings.slice(replaceStart, replaceEnd + 1);
586
+ immediateParent.replaceStart = replaceStart;
587
+ immediateParent.replaceEnd = replaceEnd;
588
+ }
589
+ const localEntries = [];
590
+ for (const descriptor of localDescriptors) {
591
+ localEntries.push(...await collectEntriesFromDescriptor(descriptor));
592
+ }
593
+ const mutatedEntries = mutation(normalizeEntries(localEntries)).sort((left, right) => compareIndexKeys(left.key, right.key));
594
+ const replacementLeafDescriptors = await writeLeafNodes(mutatedEntries, snapshotDomain);
595
+ const rootDescriptor = immediateParent
596
+ ? await rewritePathWithDescriptors(found.path, replacementLeafDescriptors, snapshotDomain)
597
+ : await rechunkToSingleRoot(replacementLeafDescriptors, snapshotDomain);
598
+ return writeIndexRootFromDescriptor(rootDescriptor, snapshotDomain);
599
+ }
600
+
601
+ async function put(root, key, value, options = {}) {
602
+ const indexRoot = await readIndexRoot(root);
603
+ const valueBlock = value?.valueRef
604
+ ? { cid: value.valueRef, payloadHash: value.valueHash || "" }
605
+ : await storage.putBlock(value, { kind: `index-value:${options.domain || indexRoot.domain || domain}` });
606
+ return mutateLocal(root, key, (entries) => [
607
+ ...entries.filter((entry) => entry.key !== String(key || "")),
608
+ {
609
+ key: String(key || ""),
610
+ valueRef: valueBlock.cid,
611
+ valueHash: valueBlock.payloadHash || "",
612
+ metadata: asRecord(value?.metadata)
613
+ }
614
+ ], options);
615
+ }
616
+
617
+ async function deleteKey(root, key, options = {}) {
618
+ return mutateLocal(root, key, (entries) => entries.filter((entry) => entry.key !== String(key || "")), options);
619
+ }
620
+
621
+ async function findLeaf(root, key) {
622
+ const indexRoot = await readIndexRoot(root);
623
+ let descriptor = {
624
+ root: indexRoot.root,
625
+ rootHash: indexRoot.rootHash,
626
+ level: indexRoot.height,
627
+ count: indexRoot.count,
628
+ keyRange: indexRoot.keyRange
629
+ };
630
+ const path = [];
631
+ while (descriptor.level > 0) {
632
+ const payload = await readNode(descriptor.root);
633
+ const children = asArray(payload.children);
634
+ const childIndex = findChildIndex(children, key);
635
+ path.push({
636
+ nodeRoot: descriptor.root,
637
+ level: descriptor.level,
638
+ keyRange: descriptor.keyRange,
639
+ siblingDescriptors: children,
640
+ childIndex,
641
+ nodeHash: descriptor.rootHash
642
+ });
643
+ descriptor = children[childIndex];
644
+ }
645
+ const leafNode = await readNode(descriptor.root);
646
+ return { indexRoot, leafDescriptor: descriptor, leafNode, path: path.reverse() };
647
+ }
648
+
649
+ async function get(root, key) {
650
+ const { leafNode } = await findLeaf(root, key);
651
+ return asArray(leafNode.entries).find((entry) => entry.key === String(key || "")) || null;
652
+ }
653
+
654
+ async function prove(root, key) {
655
+ const normalizedKey = String(key || "");
656
+ const { indexRoot, leafDescriptor, leafNode, path } = await findLeaf(root, normalizedKey);
657
+ const entries = asArray(leafNode.entries).map(normalizeIndexEntry);
658
+ const entry = entries.find((candidate) => candidate.key === normalizedKey);
659
+ const leafNodeProof = {
660
+ keyRange: leafNode.keyRange,
661
+ count: leafNode.count,
662
+ entries,
663
+ splitter: leafNode.splitter
664
+ };
665
+ if (entry) {
666
+ return {
667
+ protocol: PACTIUM_PROTOCOL,
668
+ proofType: PACTIUM_PROOF_TYPES.indexMembership,
669
+ domain: indexRoot.domain,
670
+ indexRoot: indexRoot.root,
671
+ rootHash: indexRoot.rootHash,
672
+ key: normalizedKey,
673
+ entry,
674
+ leafHash: indexLeafHash(entry),
675
+ leafRoot: leafDescriptor.root,
676
+ leafRootHash: leafDescriptor.rootHash,
677
+ leafNode: leafNodeProof,
678
+ path
679
+ };
680
+ }
681
+ const sortedEntries = entries.sort((left, right) => compareIndexKeys(left.key, right.key));
682
+ const insertionPoint = sortedEntries.findIndex((candidate) => compareIndexKeys(candidate.key, normalizedKey) > 0);
683
+ const left = insertionPoint < 0
684
+ ? sortedEntries[sortedEntries.length - 1] || null
685
+ : insertionPoint === 0
686
+ ? null
687
+ : sortedEntries[insertionPoint - 1];
688
+ const right = insertionPoint < 0 ? null : sortedEntries[insertionPoint];
689
+ return {
690
+ protocol: PACTIUM_PROTOCOL,
691
+ proofType: PACTIUM_PROOF_TYPES.indexNonMembership,
692
+ domain: indexRoot.domain,
693
+ indexRoot: indexRoot.root,
694
+ rootHash: indexRoot.rootHash,
695
+ key: normalizedKey,
696
+ containingLeaf: {
697
+ keyRange: leafNode.keyRange,
698
+ entries: sortedEntries,
699
+ leafRoot: leafDescriptor.root,
700
+ leafRootHash: leafDescriptor.rootHash,
701
+ leafNode: leafNodeProof,
702
+ path
703
+ },
704
+ leftBoundary: left?.key || "",
705
+ rightBoundary: right?.key || ""
706
+ };
707
+ }
708
+
709
+ function verifyProof(proof) {
710
+ return verifyIndexProof(proof);
711
+ }
712
+
713
+ async function scan(root, { min = "", max = "\uffff", limit = 5000, after = "" } = {}) {
714
+ if (compareIndexKeys(min, max) > 0) return [];
715
+ return rangeEntries(root, {
716
+ min: String(min || ""),
717
+ max: String(max || "\uffff"),
718
+ after: String(after || ""),
719
+ limit: clampLimit(limit)
720
+ });
721
+ }
722
+
723
+ async function prefix(root, keyPrefix = "", options = {}) {
724
+ const normalizedPrefix = String(keyPrefix || "");
725
+ return rangeEntries(root, {
726
+ min: normalizedPrefix,
727
+ max: normalizedPrefix ? `${normalizedPrefix}\uffff` : "\uffff",
728
+ after: String(options.after || ""),
729
+ limit: clampLimit(options.limit),
730
+ predicate: (entry) => !normalizedPrefix || entry.key === normalizedPrefix || entry.key.startsWith(normalizedPrefix)
731
+ });
732
+ }
733
+
734
+ async function diffEntries(leftEntries, rightEntries) {
735
+ const leftMap = new Map(leftEntries.map((entry) => [entry.key, entry]));
736
+ const rightMap = new Map(rightEntries.map((entry) => [entry.key, entry]));
737
+ return [...new Set([...leftMap.keys(), ...rightMap.keys()])]
738
+ .sort(compareIndexKeys)
739
+ .map((key) => {
740
+ const before = leftMap.get(key) || null;
741
+ const after = rightMap.get(key) || null;
742
+ return canonicalString(before) === canonicalString(after)
743
+ ? null
744
+ : { key, action: before && after ? "update" : before ? "delete" : "create", before, after };
745
+ })
746
+ .filter(Boolean);
747
+ }
748
+
749
+ async function collectDescriptorEntries(descriptor) {
750
+ return descriptor ? collectEntriesFromDescriptor(descriptor) : [];
751
+ }
752
+
753
+ function compareKeys(left, right) {
754
+ return compareIndexKeys(left, right);
755
+ }
756
+
757
+ function descriptorMaxBefore(leftDescriptor, rightDescriptor) {
758
+ return compareKeys(leftDescriptor?.keyRange?.max, rightDescriptor?.keyRange?.min) < 0;
759
+ }
760
+
761
+ function sameDescriptorRange(leftDescriptor, rightDescriptor) {
762
+ return String(leftDescriptor?.keyRange?.min || "") === String(rightDescriptor?.keyRange?.min || "") &&
763
+ String(leftDescriptor?.keyRange?.max || "") === String(rightDescriptor?.keyRange?.max || "");
764
+ }
765
+
766
+ function maxKey(left, right) {
767
+ return compareKeys(left, right) >= 0 ? String(left || "") : String(right || "");
768
+ }
769
+
770
+ async function descriptorActionChanges(descriptor, action) {
771
+ const entries = await collectDescriptorEntries(descriptor);
772
+ return entries.map((entry) => action === "create"
773
+ ? { key: entry.key, action, before: null, after: entry }
774
+ : { key: entry.key, action, before: entry, after: null });
775
+ }
776
+
777
+ async function collectGroupedEntries(descriptors) {
778
+ const entries = [];
779
+ for (const descriptor of descriptors) entries.push(...await collectDescriptorEntries(descriptor));
780
+ return normalizeEntries(entries);
781
+ }
782
+
783
+ async function expandDescriptorGroup(descriptors) {
784
+ const expanded = [];
785
+ let allLeaves = true;
786
+ for (const descriptor of descriptors) {
787
+ const node = await readNode(descriptor.root);
788
+ if (Number(node.level || 0) === 0) {
789
+ expanded.push(descriptor);
790
+ } else {
791
+ allLeaves = false;
792
+ expanded.push(...asArray(node.children));
793
+ }
794
+ }
795
+ return { descriptors: expanded, allLeaves };
796
+ }
797
+
798
+ async function diffDescriptorGroups(leftGroup, rightGroup) {
799
+ const [leftExpanded, rightExpanded] = await Promise.all([
800
+ expandDescriptorGroup(leftGroup),
801
+ expandDescriptorGroup(rightGroup)
802
+ ]);
803
+ if (leftExpanded.allLeaves && rightExpanded.allLeaves) {
804
+ return diffEntries(
805
+ await collectGroupedEntries(leftExpanded.descriptors),
806
+ await collectGroupedEntries(rightExpanded.descriptors)
807
+ );
808
+ }
809
+ return diffChildDescriptors(leftExpanded.descriptors, rightExpanded.descriptors);
810
+ }
811
+
812
+ async function diffChildDescriptors(leftChildren, rightChildren) {
813
+ const changes = [];
814
+ let leftIndex = 0;
815
+ let rightIndex = 0;
816
+ while (leftIndex < leftChildren.length || rightIndex < rightChildren.length) {
817
+ const leftChild = leftChildren[leftIndex] || null;
818
+ const rightChild = rightChildren[rightIndex] || null;
819
+ if (!leftChild) {
820
+ changes.push(...await descriptorActionChanges(rightChild, "create"));
821
+ rightIndex += 1;
822
+ continue;
823
+ }
824
+ if (!rightChild) {
825
+ changes.push(...await descriptorActionChanges(leftChild, "delete"));
826
+ leftIndex += 1;
827
+ continue;
828
+ }
829
+ if (leftChild.root === rightChild.root) {
830
+ leftIndex += 1;
831
+ rightIndex += 1;
832
+ continue;
833
+ }
834
+ if (descriptorMaxBefore(leftChild, rightChild)) {
835
+ changes.push(...await descriptorActionChanges(leftChild, "delete"));
836
+ leftIndex += 1;
837
+ continue;
838
+ }
839
+ if (descriptorMaxBefore(rightChild, leftChild)) {
840
+ changes.push(...await descriptorActionChanges(rightChild, "create"));
841
+ rightIndex += 1;
842
+ continue;
843
+ }
844
+
845
+ let groupMax = maxKey(leftChild.keyRange?.max, rightChild.keyRange?.max);
846
+ const leftGroup = [];
847
+ const rightGroup = [];
848
+ let expanded = true;
849
+ while (expanded) {
850
+ expanded = false;
851
+ while (leftIndex < leftChildren.length && compareKeys(leftChildren[leftIndex].keyRange?.min, groupMax) <= 0) {
852
+ const child = leftChildren[leftIndex];
853
+ leftGroup.push(child);
854
+ groupMax = maxKey(groupMax, child.keyRange?.max);
855
+ leftIndex += 1;
856
+ expanded = true;
857
+ }
858
+ while (rightIndex < rightChildren.length && compareKeys(rightChildren[rightIndex].keyRange?.min, groupMax) <= 0) {
859
+ const child = rightChildren[rightIndex];
860
+ rightGroup.push(child);
861
+ groupMax = maxKey(groupMax, child.keyRange?.max);
862
+ rightIndex += 1;
863
+ expanded = true;
864
+ }
865
+ }
866
+
867
+ if (leftGroup.length === 1 && rightGroup.length === 1 && sameDescriptorRange(leftGroup[0], rightGroup[0])) {
868
+ changes.push(...await diffDescriptors(leftGroup[0], rightGroup[0]));
869
+ } else {
870
+ changes.push(...await diffDescriptorGroups(leftGroup, rightGroup));
871
+ }
872
+ }
873
+ return changes;
874
+ }
875
+
876
+ async function diffDescriptors(leftDescriptor, rightDescriptor) {
877
+ if (!leftDescriptor && !rightDescriptor) return [];
878
+ if (leftDescriptor?.root && rightDescriptor?.root && leftDescriptor.root === rightDescriptor.root) return [];
879
+ if (!leftDescriptor) return descriptorActionChanges(rightDescriptor, "create");
880
+ if (!rightDescriptor) return descriptorActionChanges(leftDescriptor, "delete");
881
+ if (descriptorMaxBefore(leftDescriptor, rightDescriptor)) {
882
+ return [
883
+ ...await descriptorActionChanges(leftDescriptor, "delete"),
884
+ ...await descriptorActionChanges(rightDescriptor, "create")
885
+ ].sort((left, right) => compareIndexKeys(left.key, right.key));
886
+ }
887
+ if (descriptorMaxBefore(rightDescriptor, leftDescriptor)) {
888
+ return [
889
+ ...await descriptorActionChanges(leftDescriptor, "delete"),
890
+ ...await descriptorActionChanges(rightDescriptor, "create")
891
+ ].sort((left, right) => compareIndexKeys(left.key, right.key));
892
+ }
893
+ const [leftNode, rightNode] = await Promise.all([readNode(leftDescriptor.root), readNode(rightDescriptor.root)]);
894
+ if (Number(leftNode.level || 0) === 0 && Number(rightNode.level || 0) === 0) {
895
+ return diffEntries(await collectDescriptorEntries(leftDescriptor), await collectDescriptorEntries(rightDescriptor));
896
+ }
897
+ const changes = await diffDescriptorGroups([leftDescriptor], [rightDescriptor]);
898
+ return changes.sort((left, right) => compareIndexKeys(left.key, right.key));
899
+ }
900
+
901
+ async function diff(leftRoot, rightRoot) {
902
+ const [left, right] = await Promise.all([readIndexRoot(leftRoot), readIndexRoot(rightRoot)]);
903
+ return diffDescriptors(
904
+ {
905
+ root: left.root,
906
+ rootHash: left.rootHash,
907
+ level: left.height,
908
+ count: left.count,
909
+ keyRange: left.keyRange
910
+ },
911
+ {
912
+ root: right.root,
913
+ rootHash: right.rootHash,
914
+ level: right.height,
915
+ count: right.count,
916
+ keyRange: right.keyRange
917
+ }
918
+ );
919
+ }
920
+
921
+ async function retainedNodeRootsFor(retainedRoots = []) {
922
+ const retained = new Set();
923
+ async function visit(descriptor) {
924
+ if (!descriptor?.root || retained.has(descriptor.root)) return;
925
+ retained.add(descriptor.root);
926
+ const payload = await readNode(descriptor.root);
927
+ for (const child of asArray(payload.children)) await visit(child);
928
+ }
929
+ for (const root of asArray(retainedRoots).map(String).filter(Boolean)) {
930
+ const indexRoot = await readIndexRoot(root);
931
+ await visit({
932
+ root: indexRoot.root,
933
+ rootHash: indexRoot.rootHash,
934
+ level: indexRoot.height,
935
+ count: indexRoot.count,
936
+ keyRange: indexRoot.keyRange
937
+ });
938
+ }
939
+ return retained;
940
+ }
941
+
942
+ async function pruneCache({ roots: retainedRoots = [] } = {}) {
943
+ const retainedRootSet = new Set(asArray(retainedRoots).map(String).filter(Boolean));
944
+ const retainedNodeRoots = await retainedNodeRootsFor([...retainedRootSet]);
945
+ let prunedNodes = 0;
946
+ let prunedRoots = 0;
947
+ let prunedSnapshots = 0;
948
+ for (const root of nodes.keys()) {
949
+ if (!retainedNodeRoots.has(root)) {
950
+ nodes.delete(root);
951
+ prunedNodes += 1;
952
+ }
953
+ }
954
+ for (const root of roots.keys()) {
955
+ if (!retainedRootSet.has(root)) {
956
+ roots.delete(root);
957
+ prunedRoots += 1;
958
+ }
959
+ }
960
+ for (const root of snapshots.keys()) {
961
+ if (!retainedRootSet.has(root)) {
962
+ snapshots.delete(root);
963
+ prunedSnapshots += 1;
964
+ }
965
+ }
966
+ return {
967
+ retainedRoots: [...retainedRootSet],
968
+ retainedNodeRoots: [...retainedNodeRoots],
969
+ prunedNodes,
970
+ prunedRoots,
971
+ prunedSnapshots
972
+ };
973
+ }
974
+
975
+ return Object.freeze({
976
+ protocol: PACTIUM_PROTOCOL,
977
+ engine: PACTIUM_INDEX_ENGINE,
978
+ domain,
979
+ createIndex,
980
+ put,
981
+ delete: deleteKey,
982
+ get,
983
+ prove,
984
+ verifyProof,
985
+ verifyIndexProof,
986
+ scan,
987
+ prefix,
988
+ diff,
989
+ pruneCache,
990
+ readSnapshot,
991
+ readIndexRoot,
992
+ readNode
993
+ });
994
+ }