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.
- package/LICENSE +674 -0
- package/README.md +92 -0
- package/README.zh-CN.md +90 -0
- package/SECURITY.md +7 -0
- package/bin/pactium.mjs +121 -0
- package/docs/LICOLITE-ASPECT.md +57 -0
- package/docs/README.md +13 -0
- package/docs/TERM.md +289 -0
- package/docs/architecture/ARCHITECTURE.md +62 -0
- package/docs/protocols/PROFILE.md +124 -0
- package/docs/protocols/PROTOCOLS.md +62 -0
- package/examples/record-operation.mjs +26 -0
- package/package.json +69 -0
- package/src/README.md +13 -0
- package/src/aspects/licolite/aspect.js +278 -0
- package/src/aspects/licolite/constants.js +13 -0
- package/src/aspects/licolite/evidence.js +47 -0
- package/src/aspects/licolite/index.d.ts +51 -0
- package/src/aspects/licolite/index.js +19 -0
- package/src/aspects/licolite/signing.js +78 -0
- package/src/canonical/value.js +40 -0
- package/src/core/append-condition.js +102 -0
- package/src/core/pactium-core.js +1073 -0
- package/src/core/tracking-cursor.js +68 -0
- package/src/http.js +99 -0
- package/src/index-engine/snapshot-merkle-index.js +994 -0
- package/src/index.d.ts +244 -0
- package/src/index.js +73 -0
- package/src/ledger/signed-head.js +204 -0
- package/src/ledger/transparency-log.js +702 -0
- package/src/maintenance/task-engine.js +36 -0
- package/src/proof/bundle-format.js +265 -0
- package/src/proof/bundle.js +77 -0
- package/src/proof/envelope.js +548 -0
- package/src/proof/registry.js +18 -0
- package/src/protocol/constants.js +69 -0
- package/src/protocol/hashing.js +47 -0
- package/src/quality/profile-runner.js +291 -0
- package/src/repair/planner.js +62 -0
- package/src/shared/records.js +32 -0
- package/src/storage/local-json-storage-port.js +360 -0
- package/src/verification/failure.js +31 -0
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PACTIUM_BUNDLE_ENCODING,
|
|
5
|
+
PACTIUM_PROOF_BUNDLE_TYPE,
|
|
6
|
+
PACTIUM_PROOF_TYPES,
|
|
7
|
+
PACTIUM_PROTOCOL,
|
|
8
|
+
PACTIUM_SCHEMA_VERSION
|
|
9
|
+
} from "../protocol/constants.js";
|
|
10
|
+
import { canonicalEncode, normalizeCanonicalValue } from "../canonical/value.js";
|
|
11
|
+
import { createAppendCondition, assertAppendCondition } from "./append-condition.js";
|
|
12
|
+
import { createLedgerTransparencyLog } from "../ledger/transparency-log.js";
|
|
13
|
+
import { advanceTrustedHead as advanceTrustedLedgerHead } from "../ledger/signed-head.js";
|
|
14
|
+
import { createVerifiableIndexEngine } from "../index-engine/snapshot-merkle-index.js";
|
|
15
|
+
import { createId, protocolHash, protocolHashHex } from "../protocol/hashing.js";
|
|
16
|
+
import { createProofRef, finalizeEnvelope, materializeExtension, verifyProofEnvelope } from "../proof/envelope.js";
|
|
17
|
+
import { createRepairPlanner } from "../repair/planner.js";
|
|
18
|
+
import { createStoragePort } from "../storage/local-json-storage-port.js";
|
|
19
|
+
import { createTrackingCursor, verifyTrackingCursor } from "./tracking-cursor.js";
|
|
20
|
+
import { asArray, asRecord, nowIso, safeText } from "../shared/records.js";
|
|
21
|
+
import { createVerificationFailure, PactiumLifecycleError } from "../verification/failure.js";
|
|
22
|
+
|
|
23
|
+
function createEmptyCoreState() {
|
|
24
|
+
return {
|
|
25
|
+
protocol: PACTIUM_PROTOCOL,
|
|
26
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
27
|
+
indexRoots: {
|
|
28
|
+
openIntent: "",
|
|
29
|
+
outcome: "",
|
|
30
|
+
intentIdempotency: "",
|
|
31
|
+
outcomeIdempotency: "",
|
|
32
|
+
causality: ""
|
|
33
|
+
},
|
|
34
|
+
workspace: {},
|
|
35
|
+
stateEntries: {},
|
|
36
|
+
checkpointEntries: {},
|
|
37
|
+
intents: {},
|
|
38
|
+
outcomes: {},
|
|
39
|
+
intentEnvelopes: {},
|
|
40
|
+
intentIdempotencyClaims: {},
|
|
41
|
+
outcomeEnvelopes: {},
|
|
42
|
+
envelopes: {},
|
|
43
|
+
proofBundles: {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function idempotencyKeyFor(input) {
|
|
48
|
+
return [
|
|
49
|
+
safeText(input.workspaceId, "default"),
|
|
50
|
+
safeText(input.operationId),
|
|
51
|
+
safeText(input.idempotencyKey),
|
|
52
|
+
protocolHashHex("operation.intent", input.input ?? input.payload ?? {})
|
|
53
|
+
].join("\u0000");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function intentIdempotencyClaimKeyFor(input) {
|
|
57
|
+
return [
|
|
58
|
+
safeText(input.workspaceId, "default"),
|
|
59
|
+
safeText(input.operationId),
|
|
60
|
+
safeText(input.idempotencyKey)
|
|
61
|
+
].join("\u0000");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function outcomeIdempotencyKeyFor(input) {
|
|
65
|
+
return [
|
|
66
|
+
safeText(input.intentId),
|
|
67
|
+
safeText(input.outcomeIdempotencyKey || input.idempotencyKey),
|
|
68
|
+
protocolHashHex("operation.outcome", input.result ?? input.output ?? input.status ?? "succeeded")
|
|
69
|
+
].join("\u0000");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function eventRefValue(ledgerAppend) {
|
|
73
|
+
return {
|
|
74
|
+
valueRef: ledgerAppend.entry.factCid,
|
|
75
|
+
valueHash: ledgerAppend.entry.factHash,
|
|
76
|
+
metadata: {
|
|
77
|
+
ledgerEventId: ledgerAppend.entry.eventId,
|
|
78
|
+
ledgerIndex: ledgerAppend.entry.index,
|
|
79
|
+
factType: ledgerAppend.entry.fact.factType
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function lifecycleValueRef(id, extra = {}) {
|
|
85
|
+
return {
|
|
86
|
+
valueRef: `ref:${id}`,
|
|
87
|
+
valueHash: protocolHash("block", { id, ...extra }),
|
|
88
|
+
metadata: extra
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function workspaceStateFor(state, workspaceId) {
|
|
93
|
+
const key = safeText(workspaceId, "default");
|
|
94
|
+
state.workspace[key] ||= {
|
|
95
|
+
nextOrdinal: 0,
|
|
96
|
+
orderRoot: "",
|
|
97
|
+
membershipRoot: "",
|
|
98
|
+
checkpointRoot: "",
|
|
99
|
+
stateRoot: ""
|
|
100
|
+
};
|
|
101
|
+
return state.workspace[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stateEntriesFor(state, workspaceId) {
|
|
105
|
+
const key = safeText(workspaceId, "default");
|
|
106
|
+
state.stateEntries[key] ||= {};
|
|
107
|
+
return state.stateEntries[key];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function checkpointEntriesFor(state, workspaceId) {
|
|
111
|
+
const key = safeText(workspaceId, "default");
|
|
112
|
+
state.checkpointEntries[key] ||= {};
|
|
113
|
+
return state.checkpointEntries[key];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function padOrdinal(value) {
|
|
117
|
+
return String(value).padStart(16, "0");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isMembershipProof(proof) {
|
|
121
|
+
return proof?.proofType === PACTIUM_PROOF_TYPES.indexMembership;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function knownCausalityRefsFor(state) {
|
|
125
|
+
return new Set([
|
|
126
|
+
...Object.keys(asRecord(state.intents)),
|
|
127
|
+
...Object.values(asRecord(state.intents)).map((record) => record?.ledgerEventId).filter(Boolean),
|
|
128
|
+
...Object.keys(asRecord(state.outcomes)),
|
|
129
|
+
...Object.values(asRecord(state.outcomes)).map((outcome) => outcome?.outcomeId).filter(Boolean)
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function applyIndexPut(indexEngine, root, key, value, domain) {
|
|
134
|
+
const result = await indexEngine.put(root, key, value, { domain });
|
|
135
|
+
return result.root;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function applyIndexDelete(indexEngine, root, key, domain) {
|
|
139
|
+
const result = await indexEngine.delete(root, key, { domain });
|
|
140
|
+
return result.root;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function currentIndexRoots(state) {
|
|
144
|
+
const retained = new Set(Object.values(asRecord(state.indexRoots)).map(String).filter(Boolean));
|
|
145
|
+
for (const workspace of Object.values(asRecord(state.workspace))) {
|
|
146
|
+
for (const field of ["orderRoot", "membershipRoot", "checkpointRoot", "stateRoot"]) {
|
|
147
|
+
if (workspace?.[field]) retained.add(String(workspace[field]));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...retained];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hashFromCid(cid) {
|
|
154
|
+
return String(cid || "").split(":").pop() || "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function updateWorkspaceProjection({ indexEngine, state, workspaceId, ledgerAppend }) {
|
|
158
|
+
const workspace = workspaceStateFor(state, workspaceId);
|
|
159
|
+
const ordinal = workspace.nextOrdinal;
|
|
160
|
+
workspace.nextOrdinal += 1;
|
|
161
|
+
const orderKey = padOrdinal(ordinal);
|
|
162
|
+
workspace.orderRoot = await applyIndexPut(
|
|
163
|
+
indexEngine,
|
|
164
|
+
workspace.orderRoot,
|
|
165
|
+
orderKey,
|
|
166
|
+
eventRefValue(ledgerAppend),
|
|
167
|
+
"workspace-order"
|
|
168
|
+
);
|
|
169
|
+
workspace.membershipRoot = await applyIndexPut(
|
|
170
|
+
indexEngine,
|
|
171
|
+
workspace.membershipRoot,
|
|
172
|
+
ledgerAppend.entry.eventId,
|
|
173
|
+
lifecycleValueRef(orderKey, { workspaceId, ordinal }),
|
|
174
|
+
"workspace-membership"
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
workspaceId,
|
|
178
|
+
ordinal,
|
|
179
|
+
orderKey,
|
|
180
|
+
orderRoot: workspace.orderRoot,
|
|
181
|
+
membershipRoot: workspace.membershipRoot,
|
|
182
|
+
orderProof: await indexEngine.prove(workspace.orderRoot, orderKey),
|
|
183
|
+
membershipProof: await indexEngine.prove(workspace.membershipRoot, ledgerAppend.entry.eventId)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function ensureWorkspaceStateRoot({ indexEngine, workspace, workspaceStateEntries }) {
|
|
188
|
+
if (workspace.stateRoot) return workspace.stateRoot;
|
|
189
|
+
const stateIndex = await indexEngine.createIndex(Object.values(asRecord(workspaceStateEntries)), { domain: "state" });
|
|
190
|
+
workspace.stateRoot = stateIndex.root;
|
|
191
|
+
return workspace.stateRoot;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createPactium({
|
|
195
|
+
dataDir = "",
|
|
196
|
+
userDataPath = "",
|
|
197
|
+
storage = null,
|
|
198
|
+
inMemory = false
|
|
199
|
+
} = {}) {
|
|
200
|
+
const resolvedStorage = storage || createStoragePort({ dataDir, userDataPath, inMemory });
|
|
201
|
+
const ledger = createLedgerTransparencyLog({ storage: resolvedStorage });
|
|
202
|
+
const indexEngine = createVerifiableIndexEngine({ storage: resolvedStorage, domain: "pactium" });
|
|
203
|
+
const repairPlanner = createRepairPlanner();
|
|
204
|
+
let state = null;
|
|
205
|
+
let mutationLane = Promise.resolve();
|
|
206
|
+
const useDurableWriteLock = !resolvedStorage.inMemory && typeof resolvedStorage.withWriteLock === "function";
|
|
207
|
+
|
|
208
|
+
async function reloadFromStorage() {
|
|
209
|
+
if (typeof resolvedStorage.clearCache === "function") resolvedStorage.clearCache();
|
|
210
|
+
if (typeof ledger.reload === "function") await ledger.reload();
|
|
211
|
+
state = null;
|
|
212
|
+
return ensureState();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function prepareRead() {
|
|
216
|
+
await mutationLane.catch(() => null);
|
|
217
|
+
if (useDurableWriteLock) return reloadFromStorage();
|
|
218
|
+
return ensureState();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function enqueueMutation(task) {
|
|
222
|
+
const run = mutationLane.catch(() => null).then(() => {
|
|
223
|
+
if (!useDurableWriteLock) return task();
|
|
224
|
+
return resolvedStorage.withWriteLock(async () => {
|
|
225
|
+
await reloadFromStorage();
|
|
226
|
+
return task();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
mutationLane = run;
|
|
230
|
+
return run;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function ensureState() {
|
|
234
|
+
await resolvedStorage.initialize();
|
|
235
|
+
if (state) return state;
|
|
236
|
+
state = await resolvedStorage.getProtocolObject("core", "runtime-state", null);
|
|
237
|
+
if (!state) state = createEmptyCoreState();
|
|
238
|
+
state.intentIdempotencyClaims ||= {};
|
|
239
|
+
return state;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function saveState() {
|
|
243
|
+
await resolvedStorage.putProtocolObject("core", "runtime-state", state);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function createEnvelope({
|
|
247
|
+
envelopeKind,
|
|
248
|
+
fact,
|
|
249
|
+
ledgerAppend,
|
|
250
|
+
proofs = {},
|
|
251
|
+
appendCondition = null,
|
|
252
|
+
extensions = [],
|
|
253
|
+
replayed = false,
|
|
254
|
+
relatedEnvelopeIds = []
|
|
255
|
+
}) {
|
|
256
|
+
const material = {
|
|
257
|
+
protocol: PACTIUM_PROTOCOL,
|
|
258
|
+
materialType: "pactium.proof-material",
|
|
259
|
+
envelopeKind,
|
|
260
|
+
ledger: {
|
|
261
|
+
head: ledgerAppend.head,
|
|
262
|
+
previousHead: ledgerAppend.previousHead,
|
|
263
|
+
inclusionProof: ledgerAppend.inclusionProof,
|
|
264
|
+
consistencyProof: ledgerAppend.consistencyProof
|
|
265
|
+
},
|
|
266
|
+
appendCondition,
|
|
267
|
+
proofs
|
|
268
|
+
};
|
|
269
|
+
const materialRef = await createProofRef(resolvedStorage, "ledger-and-index-proofs", material);
|
|
270
|
+
const materializedExtensions = [];
|
|
271
|
+
for (const extension of extensions) {
|
|
272
|
+
const materialized = await materializeExtension(resolvedStorage, extension);
|
|
273
|
+
if (materialized) materializedExtensions.push(materialized);
|
|
274
|
+
}
|
|
275
|
+
const envelopeBase = {
|
|
276
|
+
protocol: PACTIUM_PROTOCOL,
|
|
277
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
278
|
+
envelopeType: "pactium.proof-envelope",
|
|
279
|
+
envelopeKind,
|
|
280
|
+
factType: fact.factType,
|
|
281
|
+
factId: fact.intentId || fact.outcomeId || fact.repairId || ledgerAppend.entry.eventId,
|
|
282
|
+
factRef: {
|
|
283
|
+
ledgerEventId: ledgerAppend.entry.eventId,
|
|
284
|
+
ledgerIndex: ledgerAppend.entry.index,
|
|
285
|
+
factCid: ledgerAppend.entry.factCid,
|
|
286
|
+
factHash: ledgerAppend.entry.factHash
|
|
287
|
+
},
|
|
288
|
+
ledgerHead: ledgerAppend.head,
|
|
289
|
+
proofRefs: [materialRef],
|
|
290
|
+
extensions: materializedExtensions,
|
|
291
|
+
criticalExtensions: materializedExtensions.filter((extension) => extension.critical).map((extension) => extension.name),
|
|
292
|
+
relatedEnvelopeIds,
|
|
293
|
+
replayed,
|
|
294
|
+
createdAt: nowIso()
|
|
295
|
+
};
|
|
296
|
+
const envelope = finalizeEnvelope(envelopeBase);
|
|
297
|
+
await resolvedStorage.putBlock(envelope, {
|
|
298
|
+
kind: "proof-envelope",
|
|
299
|
+
refs: [
|
|
300
|
+
...envelope.proofRefs.map((ref) => ref.cid),
|
|
301
|
+
...envelope.extensions.map((extension) => extension.valueRef)
|
|
302
|
+
]
|
|
303
|
+
});
|
|
304
|
+
const current = await ensureState();
|
|
305
|
+
current.envelopes[envelope.envelopeId] = envelope;
|
|
306
|
+
await saveState();
|
|
307
|
+
return envelope;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function beginOperationIntent(input = {}) {
|
|
311
|
+
return enqueueMutation(() => beginOperationIntentCommitted(input));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function beginOperationIntentCommitted(input = {}) {
|
|
315
|
+
const current = await ensureState();
|
|
316
|
+
const operationId = safeText(input.operationId);
|
|
317
|
+
if (!operationId) throw new Error("operationId is required for Operation Intent.");
|
|
318
|
+
const workspaceId = safeText(input.workspaceId || input.scope, "default");
|
|
319
|
+
const idempotencyKey = safeText(input.idempotencyKey);
|
|
320
|
+
const idKey = idempotencyKey ? idempotencyKeyFor({ ...input, operationId, workspaceId }) : "";
|
|
321
|
+
const idClaimKey = idempotencyKey ? intentIdempotencyClaimKeyFor({ operationId, workspaceId, idempotencyKey }) : "";
|
|
322
|
+
const inputHash = protocolHash("operation.intent", input.input ?? input.payload ?? {});
|
|
323
|
+
if (idKey && current.intentEnvelopes[idKey]) {
|
|
324
|
+
const envelope = { ...current.envelopes[current.intentEnvelopes[idKey]], replayed: true };
|
|
325
|
+
return envelope;
|
|
326
|
+
}
|
|
327
|
+
if (idClaimKey && current.intentIdempotencyClaims[idClaimKey] && current.intentIdempotencyClaims[idClaimKey].inputHash !== inputHash) {
|
|
328
|
+
throw new PactiumLifecycleError("Operation Intent idempotency key was reused with different input.", createVerificationFailure({
|
|
329
|
+
layer: "operation-lifecycle",
|
|
330
|
+
code: "idempotency_conflict",
|
|
331
|
+
message: "Intent idempotency key was reused with different input.",
|
|
332
|
+
evidenceRef: idClaimKey,
|
|
333
|
+
repairable: false
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
const appendCondition = input.appendCondition
|
|
337
|
+
? createAppendCondition({ workspaceId, ...asRecord(input.appendCondition) })
|
|
338
|
+
: null;
|
|
339
|
+
if (appendCondition) {
|
|
340
|
+
const requiredIntentId = appendCondition.requiredOpenIntentState?.intentId || "";
|
|
341
|
+
const openIntentState = requiredIntentId
|
|
342
|
+
? {
|
|
343
|
+
intentId: requiredIntentId,
|
|
344
|
+
exists: current.intents[requiredIntentId]?.open === true
|
|
345
|
+
}
|
|
346
|
+
: { exists: false };
|
|
347
|
+
await assertAppendCondition(appendCondition, {
|
|
348
|
+
phase: "intent",
|
|
349
|
+
currentHead: await ledger.head(),
|
|
350
|
+
workspace: workspaceStateFor(current, workspaceId),
|
|
351
|
+
openIntentState,
|
|
352
|
+
knownCausalityRefs: knownCausalityRefsFor(current)
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const intent = {
|
|
356
|
+
protocol: PACTIUM_PROTOCOL,
|
|
357
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
358
|
+
factType: "operation.intent",
|
|
359
|
+
intentId: createId("operation_intent", {
|
|
360
|
+
operationId,
|
|
361
|
+
workspaceId,
|
|
362
|
+
idempotencyKey,
|
|
363
|
+
input: input.input ?? input.payload ?? {},
|
|
364
|
+
nonce: input.nonce || crypto.randomUUID()
|
|
365
|
+
}),
|
|
366
|
+
operationId,
|
|
367
|
+
workspaceId,
|
|
368
|
+
idempotencyKey,
|
|
369
|
+
inputHash,
|
|
370
|
+
subject: normalizeCanonicalValue(asRecord(input.subject)),
|
|
371
|
+
causalityRefs: asArray(input.causalityRefs).map(String),
|
|
372
|
+
appendConditionHash: appendCondition?.conditionHash || "",
|
|
373
|
+
createdAt: nowIso()
|
|
374
|
+
};
|
|
375
|
+
const ledgerAppend = await ledger.append(intent);
|
|
376
|
+
current.intents[intent.intentId] = {
|
|
377
|
+
intent,
|
|
378
|
+
ledgerEventId: ledgerAppend.entry.eventId,
|
|
379
|
+
open: true
|
|
380
|
+
};
|
|
381
|
+
current.indexRoots.openIntent = await applyIndexPut(
|
|
382
|
+
indexEngine,
|
|
383
|
+
current.indexRoots.openIntent,
|
|
384
|
+
intent.intentId,
|
|
385
|
+
eventRefValue(ledgerAppend),
|
|
386
|
+
"open-intent"
|
|
387
|
+
);
|
|
388
|
+
if (idKey) {
|
|
389
|
+
current.indexRoots.intentIdempotency = await applyIndexPut(
|
|
390
|
+
indexEngine,
|
|
391
|
+
current.indexRoots.intentIdempotency,
|
|
392
|
+
idKey,
|
|
393
|
+
lifecycleValueRef(intent.intentId, { intentId: intent.intentId }),
|
|
394
|
+
"intent-idempotency"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (const ref of intent.causalityRefs) {
|
|
398
|
+
current.indexRoots.causality = await applyIndexPut(
|
|
399
|
+
indexEngine,
|
|
400
|
+
current.indexRoots.causality,
|
|
401
|
+
`${ref}\u0000${intent.intentId}`,
|
|
402
|
+
lifecycleValueRef(intent.intentId, { from: ref, to: intent.intentId, relation: "causes" }),
|
|
403
|
+
"operation-causality"
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const projection = await updateWorkspaceProjection({ indexEngine, state: current, workspaceId, ledgerAppend });
|
|
407
|
+
const workspace = workspaceStateFor(current, workspaceId);
|
|
408
|
+
const checkpoints = checkpointEntriesFor(current, workspaceId);
|
|
409
|
+
const checkpointNodeId = createId("checkpoint_node", { intentId: intent.intentId, kind: "intent" });
|
|
410
|
+
const checkpointNode = {
|
|
411
|
+
checkpointNodeId,
|
|
412
|
+
checkpointKind: "intent",
|
|
413
|
+
parentId: "",
|
|
414
|
+
intentId: intent.intentId,
|
|
415
|
+
ledgerEventId: ledgerAppend.entry.eventId,
|
|
416
|
+
workspaceId
|
|
417
|
+
};
|
|
418
|
+
checkpoints[checkpointNodeId] = checkpointNode;
|
|
419
|
+
workspace.checkpointRoot = await applyIndexPut(
|
|
420
|
+
indexEngine,
|
|
421
|
+
workspace.checkpointRoot,
|
|
422
|
+
checkpointNodeId,
|
|
423
|
+
{
|
|
424
|
+
valueRef: `ref:${checkpointNodeId}`,
|
|
425
|
+
valueHash: protocolHash("checkpoint.node", checkpointNode),
|
|
426
|
+
metadata: checkpointNode
|
|
427
|
+
},
|
|
428
|
+
"checkpoint"
|
|
429
|
+
);
|
|
430
|
+
const envelope = await createEnvelope({
|
|
431
|
+
envelopeKind: "operation-intent",
|
|
432
|
+
fact: intent,
|
|
433
|
+
ledgerAppend,
|
|
434
|
+
proofs: {
|
|
435
|
+
openIntent: await indexEngine.prove(current.indexRoots.openIntent, intent.intentId),
|
|
436
|
+
intentIdempotency: idKey ? await indexEngine.prove(current.indexRoots.intentIdempotency, idKey) : null,
|
|
437
|
+
workspaceProjection: projection,
|
|
438
|
+
checkpoint: {
|
|
439
|
+
root: workspace.checkpointRoot,
|
|
440
|
+
proof: await indexEngine.prove(workspace.checkpointRoot, checkpointNodeId)
|
|
441
|
+
},
|
|
442
|
+
causality: {
|
|
443
|
+
root: current.indexRoots.causality,
|
|
444
|
+
proofs: await Promise.all(intent.causalityRefs.map((ref) =>
|
|
445
|
+
indexEngine.prove(current.indexRoots.causality, `${ref}\u0000${intent.intentId}`)
|
|
446
|
+
))
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
appendCondition,
|
|
450
|
+
extensions: asArray(input.extensions),
|
|
451
|
+
replayed: false
|
|
452
|
+
});
|
|
453
|
+
if (idKey) current.intentEnvelopes[idKey] = envelope.envelopeId;
|
|
454
|
+
if (idClaimKey) current.intentIdempotencyClaims[idClaimKey] = { inputHash, envelopeId: envelope.envelopeId };
|
|
455
|
+
current.intents[intent.intentId].intentEnvelopeId = envelope.envelopeId;
|
|
456
|
+
await saveState();
|
|
457
|
+
return envelope;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function appendOperationOutcome(input = {}) {
|
|
461
|
+
return enqueueMutation(() => appendOperationOutcomeCommitted(input));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function appendOperationOutcomeCommitted(input = {}) {
|
|
465
|
+
const current = await ensureState();
|
|
466
|
+
const intentId = safeText(input.intentId);
|
|
467
|
+
if (!intentId) throw new Error("intentId is required for Operation Outcome.");
|
|
468
|
+
const intentRecord = current.intents[intentId];
|
|
469
|
+
if (!intentRecord) {
|
|
470
|
+
throw new PactiumLifecycleError("Operation Intent does not exist.", createVerificationFailure({
|
|
471
|
+
layer: "operation-lifecycle",
|
|
472
|
+
code: "intent_missing",
|
|
473
|
+
message: "Operation Outcome cannot be appended without a recorded Operation Intent.",
|
|
474
|
+
repairable: false
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
const outcomeIdKey = safeText(input.outcomeIdempotencyKey || input.idempotencyKey)
|
|
478
|
+
? outcomeIdempotencyKeyFor(input)
|
|
479
|
+
: "";
|
|
480
|
+
if (outcomeIdKey && current.outcomeEnvelopes[outcomeIdKey]) {
|
|
481
|
+
return { ...current.envelopes[current.outcomeEnvelopes[outcomeIdKey]], replayed: true };
|
|
482
|
+
}
|
|
483
|
+
if (current.outcomes[intentId]) {
|
|
484
|
+
throw new PactiumLifecycleError("Operation Intent already has a Terminal Outcome.", createVerificationFailure({
|
|
485
|
+
layer: "operation-lifecycle",
|
|
486
|
+
code: "terminal_outcome_exists",
|
|
487
|
+
message: "Pactium records exactly one Terminal Outcome per Operation Intent.",
|
|
488
|
+
evidenceRef: current.outcomes[intentId].outcomeId,
|
|
489
|
+
repairable: false
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
const workspaceId = intentRecord.intent.workspaceId;
|
|
493
|
+
const appendCondition = input.appendCondition
|
|
494
|
+
? createAppendCondition({ workspaceId, ...asRecord(input.appendCondition) })
|
|
495
|
+
: null;
|
|
496
|
+
if (appendCondition) {
|
|
497
|
+
await assertAppendCondition(appendCondition, {
|
|
498
|
+
phase: "outcome",
|
|
499
|
+
currentHead: await ledger.head(),
|
|
500
|
+
workspace: workspaceStateFor(current, workspaceId),
|
|
501
|
+
outcomeState: {
|
|
502
|
+
intentId,
|
|
503
|
+
exists: Boolean(current.outcomes[intentId]),
|
|
504
|
+
outcomeId: current.outcomes[intentId]?.outcomeId || ""
|
|
505
|
+
},
|
|
506
|
+
knownCausalityRefs: knownCausalityRefsFor(current)
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const outcome = {
|
|
510
|
+
protocol: PACTIUM_PROTOCOL,
|
|
511
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
512
|
+
factType: "operation.outcome",
|
|
513
|
+
outcomeId: createId("operation_outcome", {
|
|
514
|
+
intentId,
|
|
515
|
+
outcomeIdempotencyKey: outcomeIdKey,
|
|
516
|
+
status: input.status || "succeeded",
|
|
517
|
+
result: input.result ?? input.output ?? {},
|
|
518
|
+
nonce: input.nonce || crypto.randomUUID()
|
|
519
|
+
}),
|
|
520
|
+
intentId,
|
|
521
|
+
operationId: intentRecord.intent.operationId,
|
|
522
|
+
workspaceId,
|
|
523
|
+
status: safeText(input.status, "succeeded"),
|
|
524
|
+
resultHash: protocolHash("operation.outcome", input.result ?? input.output ?? {}),
|
|
525
|
+
hostEvidenceRefs: asArray(input.hostEvidenceRefs).map(String),
|
|
526
|
+
causalityRefs: asArray(input.causalityRefs).map(String),
|
|
527
|
+
appendConditionHash: appendCondition?.conditionHash || "",
|
|
528
|
+
createdAt: nowIso()
|
|
529
|
+
};
|
|
530
|
+
const ledgerAppend = await ledger.append(outcome);
|
|
531
|
+
current.outcomes[intentId] = outcome;
|
|
532
|
+
current.intents[intentId].open = false;
|
|
533
|
+
current.indexRoots.openIntent = await applyIndexDelete(indexEngine, current.indexRoots.openIntent, intentId, "open-intent");
|
|
534
|
+
current.indexRoots.outcome = await applyIndexPut(
|
|
535
|
+
indexEngine,
|
|
536
|
+
current.indexRoots.outcome,
|
|
537
|
+
intentId,
|
|
538
|
+
eventRefValue(ledgerAppend),
|
|
539
|
+
"outcome"
|
|
540
|
+
);
|
|
541
|
+
if (outcomeIdKey) {
|
|
542
|
+
current.indexRoots.outcomeIdempotency = await applyIndexPut(
|
|
543
|
+
indexEngine,
|
|
544
|
+
current.indexRoots.outcomeIdempotency,
|
|
545
|
+
outcomeIdKey,
|
|
546
|
+
lifecycleValueRef(outcome.outcomeId, { outcomeId: outcome.outcomeId }),
|
|
547
|
+
"outcome-idempotency"
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
for (const ref of outcome.causalityRefs) {
|
|
551
|
+
current.indexRoots.causality = await applyIndexPut(
|
|
552
|
+
indexEngine,
|
|
553
|
+
current.indexRoots.causality,
|
|
554
|
+
`${ref}\u0000${outcome.outcomeId}`,
|
|
555
|
+
lifecycleValueRef(outcome.outcomeId, { from: ref, to: outcome.outcomeId, relation: "causes" }),
|
|
556
|
+
"operation-causality"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const projection = await updateWorkspaceProjection({ indexEngine, state: current, workspaceId, ledgerAppend });
|
|
560
|
+
const workspace = workspaceStateFor(current, workspaceId);
|
|
561
|
+
const workspaceStateEntries = stateEntriesFor(current, workspaceId);
|
|
562
|
+
const mutations = asArray(input.stateMutations || input.state?.mutations);
|
|
563
|
+
const keyedMutations = mutations.filter((mutation) => mutation?.key);
|
|
564
|
+
await ensureWorkspaceStateRoot({ indexEngine, workspace, workspaceStateEntries });
|
|
565
|
+
for (const mutation of mutations) {
|
|
566
|
+
const key = String(mutation.key || "");
|
|
567
|
+
if (!key) continue;
|
|
568
|
+
if (mutation.action === "delete") {
|
|
569
|
+
delete workspaceStateEntries[key];
|
|
570
|
+
workspace.stateRoot = await applyIndexDelete(indexEngine, workspace.stateRoot, key, "state");
|
|
571
|
+
} else {
|
|
572
|
+
const valueBlock = mutation.valueRef
|
|
573
|
+
? { cid: mutation.valueRef, payloadHash: mutation.valueHash || "" }
|
|
574
|
+
: await resolvedStorage.putBlock(mutation.value ?? mutation, { kind: "state-value" });
|
|
575
|
+
const stateEntry = {
|
|
576
|
+
key,
|
|
577
|
+
valueRef: valueBlock.cid,
|
|
578
|
+
valueHash: valueBlock.payloadHash || "",
|
|
579
|
+
metadata: normalizeCanonicalValue(asRecord(mutation.metadata))
|
|
580
|
+
};
|
|
581
|
+
workspaceStateEntries[key] = stateEntry;
|
|
582
|
+
workspace.stateRoot = await applyIndexPut(indexEngine, workspace.stateRoot, key, stateEntry, "state");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const stateRoot = workspace.stateRoot;
|
|
586
|
+
const stateCommit = {
|
|
587
|
+
protocol: PACTIUM_PROTOCOL,
|
|
588
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
589
|
+
factType: "state.commit",
|
|
590
|
+
stateCommitId: createId("state_commit", {
|
|
591
|
+
outcomeId: outcome.outcomeId,
|
|
592
|
+
stateRoot,
|
|
593
|
+
mutations: keyedMutations
|
|
594
|
+
}),
|
|
595
|
+
outcomeId: outcome.outcomeId,
|
|
596
|
+
intentId,
|
|
597
|
+
workspaceId,
|
|
598
|
+
stateRoot,
|
|
599
|
+
mutationCount: keyedMutations.length,
|
|
600
|
+
mutationKeys: keyedMutations.map((mutation) => String(mutation.key || "")),
|
|
601
|
+
mutationActions: keyedMutations.map((mutation) => String(mutation.action || "put")),
|
|
602
|
+
touchedKeyCount: keyedMutations.slice(0, 32).length,
|
|
603
|
+
createdAt: nowIso()
|
|
604
|
+
};
|
|
605
|
+
const checkpointEntries = checkpointEntriesFor(current, workspaceId);
|
|
606
|
+
const outcomeCheckpointNodeId = createId("checkpoint_node", { outcomeId: outcome.outcomeId, kind: "outcome" });
|
|
607
|
+
const outcomeCheckpointNode = {
|
|
608
|
+
checkpointNodeId: outcomeCheckpointNodeId,
|
|
609
|
+
checkpointKind: "outcome",
|
|
610
|
+
parentId: createId("checkpoint_node", { intentId, kind: "intent" }),
|
|
611
|
+
intentId,
|
|
612
|
+
outcomeId: outcome.outcomeId,
|
|
613
|
+
stateCommitId: stateCommit.stateCommitId,
|
|
614
|
+
workspaceId,
|
|
615
|
+
ledgerEventId: ledgerAppend.entry.eventId
|
|
616
|
+
};
|
|
617
|
+
checkpointEntries[outcomeCheckpointNodeId] = outcomeCheckpointNode;
|
|
618
|
+
workspace.checkpointRoot = await applyIndexPut(
|
|
619
|
+
indexEngine,
|
|
620
|
+
workspace.checkpointRoot,
|
|
621
|
+
outcomeCheckpointNodeId,
|
|
622
|
+
{
|
|
623
|
+
valueRef: `ref:${outcomeCheckpointNodeId}`,
|
|
624
|
+
valueHash: protocolHash("checkpoint.node", outcomeCheckpointNode),
|
|
625
|
+
metadata: outcomeCheckpointNode
|
|
626
|
+
},
|
|
627
|
+
"checkpoint"
|
|
628
|
+
);
|
|
629
|
+
const touchedKeyProofs = [];
|
|
630
|
+
for (const mutation of keyedMutations.slice(0, 32)) {
|
|
631
|
+
touchedKeyProofs.push(await indexEngine.prove(stateRoot, String(mutation.key)));
|
|
632
|
+
}
|
|
633
|
+
const envelope = await createEnvelope({
|
|
634
|
+
envelopeKind: "operation-outcome",
|
|
635
|
+
fact: outcome,
|
|
636
|
+
ledgerAppend,
|
|
637
|
+
proofs: {
|
|
638
|
+
outcome: await indexEngine.prove(current.indexRoots.outcome, intentId),
|
|
639
|
+
openIntentRemoved: await indexEngine.prove(current.indexRoots.openIntent, intentId),
|
|
640
|
+
outcomeIdempotency: outcomeIdKey ? await indexEngine.prove(current.indexRoots.outcomeIdempotency, outcomeIdKey) : null,
|
|
641
|
+
workspaceProjection: projection,
|
|
642
|
+
stateCommit,
|
|
643
|
+
state: {
|
|
644
|
+
root: stateRoot,
|
|
645
|
+
touchedKeyProofs
|
|
646
|
+
},
|
|
647
|
+
checkpoint: {
|
|
648
|
+
root: workspace.checkpointRoot,
|
|
649
|
+
proof: await indexEngine.prove(workspace.checkpointRoot, outcomeCheckpointNodeId)
|
|
650
|
+
},
|
|
651
|
+
causality: {
|
|
652
|
+
root: current.indexRoots.causality,
|
|
653
|
+
proofs: await Promise.all(outcome.causalityRefs.map((ref) =>
|
|
654
|
+
indexEngine.prove(current.indexRoots.causality, `${ref}\u0000${outcome.outcomeId}`)
|
|
655
|
+
))
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
appendCondition,
|
|
659
|
+
extensions: asArray(input.extensions),
|
|
660
|
+
replayed: false,
|
|
661
|
+
relatedEnvelopeIds: [intentRecord.intentEnvelopeId].filter(Boolean)
|
|
662
|
+
});
|
|
663
|
+
if (outcomeIdKey) current.outcomeEnvelopes[outcomeIdKey] = envelope.envelopeId;
|
|
664
|
+
await saveState();
|
|
665
|
+
return envelope;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function recordOperation(input = {}) {
|
|
669
|
+
const intentEnvelope = await beginOperationIntent(input.intentAppendCondition
|
|
670
|
+
? { ...input, appendCondition: input.intentAppendCondition }
|
|
671
|
+
: input);
|
|
672
|
+
if (intentEnvelope.replayed && input.returnIntentReplay) return intentEnvelope;
|
|
673
|
+
const intentId = intentEnvelope.factId;
|
|
674
|
+
return appendOperationOutcome({
|
|
675
|
+
...input,
|
|
676
|
+
intentId,
|
|
677
|
+
appendCondition: input.outcomeAppendCondition || input.outcome?.appendCondition || null,
|
|
678
|
+
extensions: asArray(input.outcomeExtensions || input.extensions)
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function lookupOpenIntent(intentId) {
|
|
683
|
+
const current = await prepareRead();
|
|
684
|
+
const proof = await indexEngine.prove(current.indexRoots.openIntent, String(intentId || ""));
|
|
685
|
+
return {
|
|
686
|
+
protocol: PACTIUM_PROTOCOL,
|
|
687
|
+
intentId,
|
|
688
|
+
exists: isMembershipProof(proof),
|
|
689
|
+
proof,
|
|
690
|
+
intent: current.intents[intentId]?.intent || null
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function lookupOutcome(intentId) {
|
|
695
|
+
const current = await prepareRead();
|
|
696
|
+
const proof = await indexEngine.prove(current.indexRoots.outcome, String(intentId || ""));
|
|
697
|
+
return {
|
|
698
|
+
protocol: PACTIUM_PROTOCOL,
|
|
699
|
+
intentId,
|
|
700
|
+
exists: isMembershipProof(proof),
|
|
701
|
+
proof,
|
|
702
|
+
outcome: current.outcomes[intentId] || null
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function getWorkspaceProjection(workspaceId = "default") {
|
|
707
|
+
const current = await prepareRead();
|
|
708
|
+
const workspace = workspaceStateFor(current, workspaceId);
|
|
709
|
+
return {
|
|
710
|
+
protocol: PACTIUM_PROTOCOL,
|
|
711
|
+
workspaceId: safeText(workspaceId, "default"),
|
|
712
|
+
nextOrdinal: workspace.nextOrdinal,
|
|
713
|
+
orderRoot: workspace.orderRoot,
|
|
714
|
+
membershipRoot: workspace.membershipRoot,
|
|
715
|
+
order: workspace.orderRoot ? await indexEngine.scan(workspace.orderRoot, { limit: 100000 }) : [],
|
|
716
|
+
membership: workspace.membershipRoot ? await indexEngine.scan(workspace.membershipRoot, { limit: 100000 }) : []
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function proveWorkspaceMembership({ workspaceId = "default", ledgerEventId = "" } = {}) {
|
|
721
|
+
const current = await prepareRead();
|
|
722
|
+
const workspace = workspaceStateFor(current, workspaceId);
|
|
723
|
+
const proof = await indexEngine.prove(workspace.membershipRoot, ledgerEventId);
|
|
724
|
+
return {
|
|
725
|
+
protocol: PACTIUM_PROTOCOL,
|
|
726
|
+
workspaceId: safeText(workspaceId, "default"),
|
|
727
|
+
ledgerEventId,
|
|
728
|
+
member: isMembershipProof(proof),
|
|
729
|
+
proof
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function getLedgerCursor({ fromCursor = null, position = 0, limit = 100 } = {}) {
|
|
734
|
+
await prepareRead();
|
|
735
|
+
const start = Math.max(0, Number(fromCursor?.position ?? position ?? 0));
|
|
736
|
+
const pageLimit = Math.max(1, Math.min(Number(limit || 100), 10000));
|
|
737
|
+
const page = await ledger.pageEntries({ start, limit: pageLimit });
|
|
738
|
+
const currentHead = page.head;
|
|
739
|
+
const entries = page.entries;
|
|
740
|
+
const nextPosition = page.nextPosition;
|
|
741
|
+
const cursor = createTrackingCursor({
|
|
742
|
+
scope: "ledger",
|
|
743
|
+
position: nextPosition,
|
|
744
|
+
gaps: [],
|
|
745
|
+
headRef: currentHead.headId || currentHead.root || currentHead.rootHash
|
|
746
|
+
});
|
|
747
|
+
return {
|
|
748
|
+
protocol: PACTIUM_PROTOCOL,
|
|
749
|
+
pageType: "pactium.ledger-cursor-page",
|
|
750
|
+
entries,
|
|
751
|
+
cursor,
|
|
752
|
+
nextCursor: cursor,
|
|
753
|
+
head: currentHead
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function getWorkspaceCursor({ workspaceId = "default", fromCursor = null, position = 0, limit = 100 } = {}) {
|
|
758
|
+
const current = await prepareRead();
|
|
759
|
+
const workspace = workspaceStateFor(current, workspaceId);
|
|
760
|
+
const currentHead = await ledger.head();
|
|
761
|
+
const start = Math.max(0, Number(fromCursor?.position ?? position ?? 0));
|
|
762
|
+
const pageLimit = Math.max(1, Math.min(Number(limit || 100), 10000));
|
|
763
|
+
const entries = workspace.orderRoot
|
|
764
|
+
? await indexEngine.scan(workspace.orderRoot, { min: padOrdinal(start), limit: pageLimit })
|
|
765
|
+
: [];
|
|
766
|
+
const nextPosition = entries.length > 0
|
|
767
|
+
? Number.parseInt(entries[entries.length - 1].key, 10) + 1
|
|
768
|
+
: start;
|
|
769
|
+
const cursor = createTrackingCursor({
|
|
770
|
+
scope: "workspace",
|
|
771
|
+
workspaceId,
|
|
772
|
+
position: nextPosition,
|
|
773
|
+
gaps: [],
|
|
774
|
+
headRef: currentHead.headId || currentHead.root || currentHead.rootHash,
|
|
775
|
+
orderRoot: workspace.orderRoot
|
|
776
|
+
});
|
|
777
|
+
return {
|
|
778
|
+
protocol: PACTIUM_PROTOCOL,
|
|
779
|
+
pageType: "pactium.workspace-cursor-page",
|
|
780
|
+
workspaceId: safeText(workspaceId, "default"),
|
|
781
|
+
entries,
|
|
782
|
+
cursor,
|
|
783
|
+
nextCursor: cursor,
|
|
784
|
+
head: currentHead,
|
|
785
|
+
orderRoot: workspace.orderRoot,
|
|
786
|
+
orderProofs: await Promise.all(entries.map((entry) => indexEngine.prove(workspace.orderRoot, entry.key)))
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function verifyCursor(cursor, context = {}) {
|
|
791
|
+
return verifyTrackingCursor(cursor, context);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function advanceTrustedHead(input = {}) {
|
|
795
|
+
return advanceTrustedLedgerHead(input);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function planRecovery(input = {}) {
|
|
799
|
+
return repairPlanner.planRecovery(input);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function verifyEnvelope(envelope, options = {}) {
|
|
803
|
+
return verifyProofEnvelope(envelope, {
|
|
804
|
+
storage: resolvedStorage,
|
|
805
|
+
supportedCriticalExtensions: options.supportedCriticalExtensions || [],
|
|
806
|
+
proofVerifiers: options.proofVerifiers || {},
|
|
807
|
+
requireAllProofs: options.requireAllProofs !== false,
|
|
808
|
+
verifierManifest: options.verifierManifest || null,
|
|
809
|
+
ledgerHeadSignatures: options.ledgerHeadSignatures || []
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function encodeVarint(value) {
|
|
814
|
+
const bytes = [];
|
|
815
|
+
let current = Number(value || 0);
|
|
816
|
+
do {
|
|
817
|
+
let byte = current & 0x7f;
|
|
818
|
+
current = Math.floor(current / 128);
|
|
819
|
+
if (current > 0) byte |= 0x80;
|
|
820
|
+
bytes.push(byte);
|
|
821
|
+
} while (current > 0);
|
|
822
|
+
return Buffer.from(bytes);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function indexedBundleRecords(blocks) {
|
|
826
|
+
let offset = 0;
|
|
827
|
+
const index = [];
|
|
828
|
+
const records = [];
|
|
829
|
+
for (const block of blocks) {
|
|
830
|
+
const header = {
|
|
831
|
+
protocol: block.protocol,
|
|
832
|
+
cid: block.cid,
|
|
833
|
+
codec: block.codec,
|
|
834
|
+
kind: block.kind,
|
|
835
|
+
refs: block.refs,
|
|
836
|
+
byteLength: block.byteLength,
|
|
837
|
+
payloadHash: block.payloadHash
|
|
838
|
+
};
|
|
839
|
+
const headerBytes = Buffer.from(canonicalEncode(header));
|
|
840
|
+
const payloadBytes = Buffer.from(String(block.payloadBase64 || ""), "base64");
|
|
841
|
+
const recordLength = headerBytes.length + payloadBytes.length;
|
|
842
|
+
const lengthBytes = encodeVarint(recordLength);
|
|
843
|
+
records.push(lengthBytes, headerBytes, payloadBytes);
|
|
844
|
+
index.push({
|
|
845
|
+
cid: block.cid,
|
|
846
|
+
offset,
|
|
847
|
+
recordLength,
|
|
848
|
+
headerLength: headerBytes.length,
|
|
849
|
+
byteLength: block.byteLength,
|
|
850
|
+
payloadHash: block.payloadHash,
|
|
851
|
+
codec: block.codec,
|
|
852
|
+
kind: block.kind,
|
|
853
|
+
refs: block.refs
|
|
854
|
+
});
|
|
855
|
+
offset += lengthBytes.length + recordLength;
|
|
856
|
+
}
|
|
857
|
+
return { index, binaryBase64: Buffer.concat(records).toString("base64"), byteLength: offset };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function exportProofBundle(envelopeOrId, options = {}) {
|
|
861
|
+
return enqueueMutation(() => exportProofBundleCommitted(envelopeOrId, options));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function exportProofBundleCommitted(envelopeOrId, options = {}) {
|
|
865
|
+
const current = await ensureState();
|
|
866
|
+
const envelope = typeof envelopeOrId === "string" ? current.envelopes[envelopeOrId] : envelopeOrId;
|
|
867
|
+
if (!envelope) throw new Error("Proof Envelope not found.");
|
|
868
|
+
const refs = [
|
|
869
|
+
...asArray(envelope.proofRefs).map((ref) => ref.cid),
|
|
870
|
+
...asArray(envelope.extensions).map((extension) => extension.valueRef)
|
|
871
|
+
];
|
|
872
|
+
const blocks = [];
|
|
873
|
+
const seen = new Set();
|
|
874
|
+
for (const ref of refs) {
|
|
875
|
+
const walked = await resolvedStorage.walk(ref);
|
|
876
|
+
for (const block of walked.blocks) {
|
|
877
|
+
if (!seen.has(block.cid)) {
|
|
878
|
+
seen.add(block.cid);
|
|
879
|
+
blocks.push({
|
|
880
|
+
protocol: block.protocol,
|
|
881
|
+
cid: block.cid,
|
|
882
|
+
codec: block.codec,
|
|
883
|
+
kind: block.kind,
|
|
884
|
+
refs: block.refs,
|
|
885
|
+
byteLength: block.byteLength,
|
|
886
|
+
payloadHash: block.payloadHash,
|
|
887
|
+
payloadBase64: block.payloadBase64
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (options.format && options.format !== "indexed") {
|
|
893
|
+
throw new Error(`Unsupported proof bundle format: ${options.format}`);
|
|
894
|
+
}
|
|
895
|
+
const manifest = {
|
|
896
|
+
protocol: PACTIUM_PROTOCOL,
|
|
897
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
898
|
+
bundleType: PACTIUM_PROOF_BUNDLE_TYPE,
|
|
899
|
+
envelopeId: envelope.envelopeId,
|
|
900
|
+
ledgerHead: envelope.ledgerHead,
|
|
901
|
+
blockCount: blocks.length,
|
|
902
|
+
requiredBlocks: blocks.map((block) => block.cid),
|
|
903
|
+
criticalExtensions: envelope.criticalExtensions,
|
|
904
|
+
createdAt: nowIso()
|
|
905
|
+
};
|
|
906
|
+
const indexed = indexedBundleRecords(blocks);
|
|
907
|
+
const bundle = {
|
|
908
|
+
protocol: PACTIUM_PROTOCOL,
|
|
909
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
910
|
+
bundleType: PACTIUM_PROOF_BUNDLE_TYPE,
|
|
911
|
+
manifest,
|
|
912
|
+
envelope,
|
|
913
|
+
index: indexed.index,
|
|
914
|
+
blocksEncoding: PACTIUM_BUNDLE_ENCODING,
|
|
915
|
+
binaryBase64: indexed.binaryBase64,
|
|
916
|
+
byteLength: indexed.byteLength,
|
|
917
|
+
bundleHash: protocolHash("proof.bundle", {
|
|
918
|
+
manifest,
|
|
919
|
+
envelope,
|
|
920
|
+
index: indexed.index.map((item) => ({
|
|
921
|
+
cid: item.cid,
|
|
922
|
+
offset: item.offset,
|
|
923
|
+
recordLength: item.recordLength,
|
|
924
|
+
headerLength: item.headerLength,
|
|
925
|
+
byteLength: item.byteLength,
|
|
926
|
+
payloadHash: item.payloadHash
|
|
927
|
+
}))
|
|
928
|
+
})
|
|
929
|
+
};
|
|
930
|
+
current.proofBundles[envelope.envelopeId] = bundle;
|
|
931
|
+
await saveState();
|
|
932
|
+
return bundle;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function createExtension(extension) {
|
|
936
|
+
return enqueueMutation(() => materializeExtension(resolvedStorage, extension));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async function storeEnvelope(envelope) {
|
|
940
|
+
return enqueueMutation(() => storeEnvelopeCommitted(envelope));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function storeEnvelopeCommitted(envelope) {
|
|
944
|
+
const current = await ensureState();
|
|
945
|
+
const finalized = finalizeEnvelope(envelope);
|
|
946
|
+
await resolvedStorage.putBlock(finalized, {
|
|
947
|
+
kind: "proof-envelope",
|
|
948
|
+
refs: [
|
|
949
|
+
...asArray(finalized.proofRefs).map((ref) => ref.cid),
|
|
950
|
+
...asArray(finalized.extensions).map((extension) => extension.valueRef)
|
|
951
|
+
]
|
|
952
|
+
});
|
|
953
|
+
current.envelopes[finalized.envelopeId] = finalized;
|
|
954
|
+
await saveState();
|
|
955
|
+
return finalized;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function protocolCatalog() {
|
|
959
|
+
return {
|
|
960
|
+
protocol: PACTIUM_PROTOCOL,
|
|
961
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
962
|
+
name: "Pactium",
|
|
963
|
+
packageName: "pactium",
|
|
964
|
+
rootExport: "latest-proof-first-only",
|
|
965
|
+
capabilities: [
|
|
966
|
+
"canonical-value",
|
|
967
|
+
"protocol-hash",
|
|
968
|
+
"storage-port",
|
|
969
|
+
"ledger-transparency-log",
|
|
970
|
+
"verifiable-index-engine",
|
|
971
|
+
"operation-lifecycle",
|
|
972
|
+
"append-condition",
|
|
973
|
+
"tracking-cursor",
|
|
974
|
+
"trusted-head-advancement",
|
|
975
|
+
"workspace-projection",
|
|
976
|
+
"merkle-state",
|
|
977
|
+
"checkpoint-tree",
|
|
978
|
+
"proof-envelope",
|
|
979
|
+
"proof-bundle",
|
|
980
|
+
"maintenance-task-engine",
|
|
981
|
+
"repair-planner"
|
|
982
|
+
]
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function doctor() {
|
|
987
|
+
await prepareRead();
|
|
988
|
+
return {
|
|
989
|
+
protocol: PACTIUM_PROTOCOL,
|
|
990
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
991
|
+
ok: true,
|
|
992
|
+
dataDir: resolvedStorage.dataDir,
|
|
993
|
+
latestSchemaOnly: true,
|
|
994
|
+
historicalMigration: false,
|
|
995
|
+
catalog: await protocolCatalog()
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async function compactInMemoryCaches() {
|
|
1000
|
+
const current = await ensureState();
|
|
1001
|
+
if (!resolvedStorage.inMemory) {
|
|
1002
|
+
return {
|
|
1003
|
+
protocol: PACTIUM_PROTOCOL,
|
|
1004
|
+
inMemory: false,
|
|
1005
|
+
retainedRoots: 0,
|
|
1006
|
+
retainedNodeRoots: 0,
|
|
1007
|
+
prunedBlocks: 0,
|
|
1008
|
+
prunedProtocolObjects: 0
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const retainedRoots = currentIndexRoots(current);
|
|
1012
|
+
const cache = typeof indexEngine.pruneCache === "function"
|
|
1013
|
+
? await indexEngine.pruneCache({ roots: retainedRoots })
|
|
1014
|
+
: { retainedRoots, retainedNodeRoots: retainedRoots };
|
|
1015
|
+
const retainedNodeRoots = new Set(asArray(cache.retainedNodeRoots || retainedRoots).map(String));
|
|
1016
|
+
const retainedRootHashes = new Set([
|
|
1017
|
+
...asArray(cache.retainedRoots || retainedRoots),
|
|
1018
|
+
...retainedNodeRoots
|
|
1019
|
+
].map(hashFromCid).filter(Boolean));
|
|
1020
|
+
const prunedBlocks = typeof resolvedStorage.pruneBlocks === "function"
|
|
1021
|
+
? resolvedStorage.pruneBlocks((block) => {
|
|
1022
|
+
const kind = String(block.kind || "");
|
|
1023
|
+
if (kind.startsWith("index-node:")) return !retainedNodeRoots.has(block.cid);
|
|
1024
|
+
return kind !== "state-value";
|
|
1025
|
+
})
|
|
1026
|
+
: 0;
|
|
1027
|
+
const prunedProtocolObjects = typeof resolvedStorage.pruneProtocolObjects === "function"
|
|
1028
|
+
? resolvedStorage.pruneProtocolObjects((object) =>
|
|
1029
|
+
object.scope === "index" && !retainedRootHashes.has(String(object.key || "").split("-").pop())
|
|
1030
|
+
)
|
|
1031
|
+
: 0;
|
|
1032
|
+
return {
|
|
1033
|
+
protocol: PACTIUM_PROTOCOL,
|
|
1034
|
+
inMemory: true,
|
|
1035
|
+
retainedRoots: retainedRoots.length,
|
|
1036
|
+
retainedNodeRoots: retainedNodeRoots.size,
|
|
1037
|
+
prunedNodes: Number(cache.prunedNodes || 0),
|
|
1038
|
+
prunedRoots: Number(cache.prunedRoots || 0),
|
|
1039
|
+
prunedSnapshots: Number(cache.prunedSnapshots || 0),
|
|
1040
|
+
prunedBlocks,
|
|
1041
|
+
prunedProtocolObjects
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return Object.freeze({
|
|
1046
|
+
protocol: PACTIUM_PROTOCOL,
|
|
1047
|
+
schema: PACTIUM_SCHEMA_VERSION,
|
|
1048
|
+
dataDir: resolvedStorage.dataDir,
|
|
1049
|
+
storage: resolvedStorage,
|
|
1050
|
+
ledger,
|
|
1051
|
+
indexEngine,
|
|
1052
|
+
beginOperationIntent,
|
|
1053
|
+
appendOperationOutcome,
|
|
1054
|
+
recordOperation,
|
|
1055
|
+
lookupOpenIntent,
|
|
1056
|
+
lookupOutcome,
|
|
1057
|
+
createAppendCondition,
|
|
1058
|
+
getLedgerCursor,
|
|
1059
|
+
getWorkspaceCursor,
|
|
1060
|
+
verifyCursor,
|
|
1061
|
+
advanceTrustedHead,
|
|
1062
|
+
planRecovery,
|
|
1063
|
+
getWorkspaceProjection,
|
|
1064
|
+
proveWorkspaceMembership,
|
|
1065
|
+
verifyEnvelope,
|
|
1066
|
+
exportProofBundle,
|
|
1067
|
+
createExtension,
|
|
1068
|
+
storeEnvelope,
|
|
1069
|
+
protocolCatalog,
|
|
1070
|
+
doctor,
|
|
1071
|
+
_compactInMemoryCaches: compactInMemoryCaches
|
|
1072
|
+
});
|
|
1073
|
+
}
|