@ternent/ledger 0.1.2
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/SPEC.md +304 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +963 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +4 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/seal-cli.d.ts +27 -0
- package/dist/types.d.ts +262 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { canonicalStringify, getCommitSigningPayload, deriveEntryId, deriveCommitId, getCommitChain, validateLedger, getEntrySigningPayload } from "@ternent/concord-protocol";
|
|
2
|
+
import { initArmour, encryptForRecipients, decryptWithIdentity } from "@ternent/armour";
|
|
3
|
+
import { createSealProof, createSealHash, verifySealProofAgainstBytes } from "@ternent/seal-cli";
|
|
4
|
+
const LEDGER_FORMAT = "concord-ledger";
|
|
5
|
+
const LEDGER_VERSION = "1";
|
|
6
|
+
const LEDGER_SPEC = "@ternent/ledger@2";
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
const textDecoder = new TextDecoder();
|
|
9
|
+
function cloneValue(value) {
|
|
10
|
+
if (typeof structuredClone === "function") {
|
|
11
|
+
return structuredClone(value);
|
|
12
|
+
}
|
|
13
|
+
return JSON.parse(JSON.stringify(value));
|
|
14
|
+
}
|
|
15
|
+
function normalizeMeta(value) {
|
|
16
|
+
return value ?? null;
|
|
17
|
+
}
|
|
18
|
+
function normalizePayloadInput(value) {
|
|
19
|
+
return value === void 0 ? null : value;
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function assertRecordOrNull(value, label) {
|
|
25
|
+
if (value === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (!isRecord(value)) {
|
|
28
|
+
throw new Error(`${label} must be an object or null.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function base64UrlEncode(bytes) {
|
|
33
|
+
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
function base64UrlDecode(value) {
|
|
36
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
37
|
+
const pad = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
38
|
+
return new Uint8Array(Buffer.from(`${normalized}${pad}`, "base64"));
|
|
39
|
+
}
|
|
40
|
+
function toCiphertextBytes(payload) {
|
|
41
|
+
return payload.encoding === "armor" ? textEncoder.encode(payload.data) : base64UrlDecode(payload.data);
|
|
42
|
+
}
|
|
43
|
+
function createShadowEntryCore(entry) {
|
|
44
|
+
return {
|
|
45
|
+
kind: entry.kind,
|
|
46
|
+
timestamp: entry.authoredAt,
|
|
47
|
+
author: entry.author,
|
|
48
|
+
payload: {
|
|
49
|
+
meta: entry.meta,
|
|
50
|
+
payload: entry.payload
|
|
51
|
+
},
|
|
52
|
+
signature: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createShadowEntry(entry) {
|
|
56
|
+
return {
|
|
57
|
+
...createShadowEntryCore(entry),
|
|
58
|
+
signature: JSON.stringify(entry.seal)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function buildUnsignedEntrySubject(entry) {
|
|
62
|
+
return textEncoder.encode(getEntrySigningPayload(createShadowEntryCore(entry)));
|
|
63
|
+
}
|
|
64
|
+
function createShadowCommit(commit) {
|
|
65
|
+
return {
|
|
66
|
+
...createShadowCommitCore(commit),
|
|
67
|
+
signature: JSON.stringify(commit.seal)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function createShadowCommitCore(commit) {
|
|
71
|
+
return {
|
|
72
|
+
parent: commit.parentCommitId,
|
|
73
|
+
timestamp: commit.committedAt,
|
|
74
|
+
metadata: commit.metadata,
|
|
75
|
+
entries: commit.entryIds,
|
|
76
|
+
signature: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createShadowContainer(container) {
|
|
80
|
+
const commits = {};
|
|
81
|
+
const entries = {};
|
|
82
|
+
for (const [commitId, commit] of Object.entries(container.commits)) {
|
|
83
|
+
commits[commitId] = createShadowCommit(commit);
|
|
84
|
+
}
|
|
85
|
+
for (const [entryId, entry] of Object.entries(container.entries)) {
|
|
86
|
+
entries[entryId] = createShadowEntry(entry);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
format: "concord-ledger",
|
|
90
|
+
version: "1.0",
|
|
91
|
+
commits,
|
|
92
|
+
entries,
|
|
93
|
+
head: container.head
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function validatePayloadShape(payload) {
|
|
97
|
+
if (payload.type === "plain") {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const errors = [];
|
|
101
|
+
if (payload.scheme !== "age") {
|
|
102
|
+
errors.push("Encrypted payload scheme must be age.");
|
|
103
|
+
}
|
|
104
|
+
if (payload.mode !== "recipients") {
|
|
105
|
+
errors.push("Encrypted payload mode must be recipients.");
|
|
106
|
+
}
|
|
107
|
+
if (payload.encoding !== "armor" && payload.encoding !== "binary") {
|
|
108
|
+
errors.push("Encrypted payload encoding must be armor or binary.");
|
|
109
|
+
}
|
|
110
|
+
if (typeof payload.data !== "string" || payload.data.length === 0) {
|
|
111
|
+
errors.push("Encrypted payload data must be a non-empty string.");
|
|
112
|
+
}
|
|
113
|
+
if (typeof payload.payloadHash !== "string" || payload.payloadHash.length === 0) {
|
|
114
|
+
errors.push("Encrypted payload payloadHash must be a non-empty string.");
|
|
115
|
+
}
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
function validateEntryShape(entry) {
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (typeof entry.entryId !== "string" || entry.entryId.length === 0) {
|
|
121
|
+
errors.push("Entry.entryId must be a non-empty string.");
|
|
122
|
+
}
|
|
123
|
+
if (typeof entry.kind !== "string" || entry.kind.length === 0) {
|
|
124
|
+
errors.push("Entry.kind must be a non-empty string.");
|
|
125
|
+
}
|
|
126
|
+
if (typeof entry.authoredAt !== "string" || entry.authoredAt.length === 0) {
|
|
127
|
+
errors.push("Entry.authoredAt must be a non-empty string.");
|
|
128
|
+
}
|
|
129
|
+
if (typeof entry.author !== "string" || entry.author.length === 0) {
|
|
130
|
+
errors.push("Entry.author must be a non-empty string.");
|
|
131
|
+
}
|
|
132
|
+
if (entry.meta !== null && !isRecord(entry.meta)) {
|
|
133
|
+
errors.push("Entry.meta must be an object or null.");
|
|
134
|
+
}
|
|
135
|
+
errors.push(...validatePayloadShape(entry.payload));
|
|
136
|
+
if (!isRecord(entry.seal)) {
|
|
137
|
+
errors.push("Entry.seal must be an object.");
|
|
138
|
+
}
|
|
139
|
+
return errors;
|
|
140
|
+
}
|
|
141
|
+
function validateCommitShape(commit) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
if (typeof commit.commitId !== "string" || commit.commitId.length === 0) {
|
|
144
|
+
errors.push("Commit.commitId must be a non-empty string.");
|
|
145
|
+
}
|
|
146
|
+
if (commit.parentCommitId !== null && (typeof commit.parentCommitId !== "string" || commit.parentCommitId.length === 0)) {
|
|
147
|
+
errors.push("Commit.parentCommitId must be a non-empty string or null.");
|
|
148
|
+
}
|
|
149
|
+
if (typeof commit.committedAt !== "string" || commit.committedAt.length === 0) {
|
|
150
|
+
errors.push("Commit.committedAt must be a non-empty string.");
|
|
151
|
+
}
|
|
152
|
+
if (commit.metadata !== null && !isRecord(commit.metadata)) {
|
|
153
|
+
errors.push("Commit.metadata must be an object or null.");
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(commit.entryIds)) {
|
|
156
|
+
errors.push("Commit.entryIds must be an array.");
|
|
157
|
+
} else if (commit.entryIds.some((entryId) => typeof entryId !== "string")) {
|
|
158
|
+
errors.push("Commit.entryIds must contain only strings.");
|
|
159
|
+
}
|
|
160
|
+
if (!isRecord(commit.seal)) {
|
|
161
|
+
errors.push("Commit.seal must be an object.");
|
|
162
|
+
}
|
|
163
|
+
return errors;
|
|
164
|
+
}
|
|
165
|
+
function validatePublicContainerShape(container) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
if (container.format !== LEDGER_FORMAT) {
|
|
168
|
+
errors.push(`Ledger.format must be "${LEDGER_FORMAT}".`);
|
|
169
|
+
}
|
|
170
|
+
if (container.version !== LEDGER_VERSION) {
|
|
171
|
+
errors.push(`Ledger.version must be "${LEDGER_VERSION}".`);
|
|
172
|
+
}
|
|
173
|
+
if (!isRecord(container.commits)) {
|
|
174
|
+
errors.push("Ledger.commits must be an object.");
|
|
175
|
+
}
|
|
176
|
+
if (!isRecord(container.entries)) {
|
|
177
|
+
errors.push("Ledger.entries must be an object.");
|
|
178
|
+
}
|
|
179
|
+
if (typeof container.head !== "string" || container.head.length === 0) {
|
|
180
|
+
errors.push("Ledger.head must be a non-empty string.");
|
|
181
|
+
}
|
|
182
|
+
if (errors.length > 0) {
|
|
183
|
+
return errors;
|
|
184
|
+
}
|
|
185
|
+
for (const [commitId, commit] of Object.entries(container.commits)) {
|
|
186
|
+
if (commit.commitId !== commitId) {
|
|
187
|
+
errors.push(`Commit key mismatch for ${commitId}.`);
|
|
188
|
+
}
|
|
189
|
+
errors.push(...validateCommitShape(commit));
|
|
190
|
+
}
|
|
191
|
+
for (const [entryId, entry] of Object.entries(container.entries)) {
|
|
192
|
+
if (entry.entryId !== entryId) {
|
|
193
|
+
errors.push(`Entry key mismatch for ${entryId}.`);
|
|
194
|
+
}
|
|
195
|
+
errors.push(...validateEntryShape(entry));
|
|
196
|
+
}
|
|
197
|
+
if (!container.commits[container.head]) {
|
|
198
|
+
errors.push(`Ledger head ${container.head} does not exist in commits.`);
|
|
199
|
+
}
|
|
200
|
+
return errors;
|
|
201
|
+
}
|
|
202
|
+
function createDefaultProtocolContract() {
|
|
203
|
+
return {
|
|
204
|
+
canonicalizePayload(value) {
|
|
205
|
+
return canonicalStringify(normalizePayloadInput(value));
|
|
206
|
+
},
|
|
207
|
+
getEntrySubjectBytes(entry) {
|
|
208
|
+
return buildUnsignedEntrySubject(entry);
|
|
209
|
+
},
|
|
210
|
+
getCommitSubjectBytes(commit) {
|
|
211
|
+
return textEncoder.encode(getCommitSigningPayload(createShadowCommitCore(commit)));
|
|
212
|
+
},
|
|
213
|
+
async deriveEntryId(entry) {
|
|
214
|
+
return deriveEntryId(createShadowEntry(entry));
|
|
215
|
+
},
|
|
216
|
+
async deriveCommitId(commit) {
|
|
217
|
+
return deriveCommitId(createShadowCommitCore(commit));
|
|
218
|
+
},
|
|
219
|
+
getCommitChain(container) {
|
|
220
|
+
return getCommitChain(createShadowContainer(container));
|
|
221
|
+
},
|
|
222
|
+
validateContainer(container) {
|
|
223
|
+
const errors = validatePublicContainerShape(container);
|
|
224
|
+
if (errors.length > 0) {
|
|
225
|
+
return { ok: false, errors };
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const shadowValidation = validateLedger(createShadowContainer(container), {
|
|
229
|
+
strictSpec: false
|
|
230
|
+
});
|
|
231
|
+
return shadowValidation.ok ? { ok: true, errors: [] } : { ok: false, errors: shadowValidation.errors };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
errors: [error instanceof Error ? error.message : "Invalid container."]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function createDefaultSealContract() {
|
|
242
|
+
return {
|
|
243
|
+
async createEntryProof(input) {
|
|
244
|
+
return await createSealProof({
|
|
245
|
+
createdAt: input.entry.authoredAt,
|
|
246
|
+
signer: input.signer,
|
|
247
|
+
subject: {
|
|
248
|
+
kind: "artifact",
|
|
249
|
+
path: `ledger-entry:${input.entry.kind}:${input.entry.authoredAt}`,
|
|
250
|
+
hash: await createSealHash(input.subjectBytes)
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async verifyEntryProof(input) {
|
|
255
|
+
const result = await verifySealProofAgainstBytes(
|
|
256
|
+
input.proof,
|
|
257
|
+
input.subjectBytes
|
|
258
|
+
);
|
|
259
|
+
return result.valid;
|
|
260
|
+
},
|
|
261
|
+
async createCommitProof(input) {
|
|
262
|
+
return await createSealProof({
|
|
263
|
+
createdAt: input.commit.committedAt,
|
|
264
|
+
signer: input.signer,
|
|
265
|
+
subject: {
|
|
266
|
+
kind: "artifact",
|
|
267
|
+
path: `ledger-commit:${input.commitId}`,
|
|
268
|
+
hash: await createSealHash(input.subjectBytes)
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
async verifyCommitProof(input) {
|
|
273
|
+
const result = await verifySealProofAgainstBytes(
|
|
274
|
+
input.proof,
|
|
275
|
+
input.subjectBytes
|
|
276
|
+
);
|
|
277
|
+
return result.valid;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function createDefaultArmourContract() {
|
|
282
|
+
return {
|
|
283
|
+
async encrypt(input) {
|
|
284
|
+
await initArmour();
|
|
285
|
+
const ciphertext = await encryptForRecipients({
|
|
286
|
+
recipients: input.recipients,
|
|
287
|
+
data: input.data,
|
|
288
|
+
output: input.encoding
|
|
289
|
+
});
|
|
290
|
+
return {
|
|
291
|
+
data: input.encoding === "armor" ? textDecoder.decode(ciphertext) : base64UrlEncode(ciphertext),
|
|
292
|
+
payloadHash: await createSealHash(ciphertext)
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
async decrypt(input) {
|
|
296
|
+
await initArmour();
|
|
297
|
+
const plaintext = await decryptWithIdentity({
|
|
298
|
+
identity: input.decryptor.identity,
|
|
299
|
+
data: toCiphertextBytes(input.payload)
|
|
300
|
+
});
|
|
301
|
+
return JSON.parse(textDecoder.decode(plaintext));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function createEmptyProjection(value) {
|
|
306
|
+
return cloneValue(value);
|
|
307
|
+
}
|
|
308
|
+
function createInitialState(initialProjection) {
|
|
309
|
+
return {
|
|
310
|
+
container: null,
|
|
311
|
+
staged: [],
|
|
312
|
+
projection: createEmptyProjection(initialProjection),
|
|
313
|
+
verification: null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function sortStrings(values) {
|
|
317
|
+
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
|
318
|
+
}
|
|
319
|
+
function mergeReplayOptions(defaults, options, hasDecryptor) {
|
|
320
|
+
const verify = options?.verify ?? defaults?.verify ?? true;
|
|
321
|
+
const decrypt = options?.decrypt ?? defaults?.decrypt ?? true;
|
|
322
|
+
return {
|
|
323
|
+
fromEntryId: options?.fromEntryId ?? "",
|
|
324
|
+
toEntryId: options?.toEntryId ?? "",
|
|
325
|
+
verify,
|
|
326
|
+
decrypt: decrypt && hasDecryptor
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function assertAppendInput(input) {
|
|
330
|
+
if (typeof input.kind !== "string" || input.kind.length === 0) {
|
|
331
|
+
throw new Error("append input kind is required.");
|
|
332
|
+
}
|
|
333
|
+
if (input.meta !== void 0) {
|
|
334
|
+
assertRecordOrNull(input.meta, "append input meta");
|
|
335
|
+
}
|
|
336
|
+
if (input.protection?.type === "recipients") {
|
|
337
|
+
if (!Array.isArray(input.protection.recipients)) {
|
|
338
|
+
throw new Error("append protection recipients must be an array.");
|
|
339
|
+
}
|
|
340
|
+
if (input.protection.recipients.length === 0) {
|
|
341
|
+
throw new Error("append protection recipients must not be empty.");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function assertContainerInput(container) {
|
|
346
|
+
const errors = validatePublicContainerShape(container);
|
|
347
|
+
if (errors.length > 0) {
|
|
348
|
+
throw new Error(errors.join("; "));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function getCommittedEntriesInOrder(container, protocol) {
|
|
352
|
+
const ordered = [];
|
|
353
|
+
for (const commitId of protocol.getCommitChain(container)) {
|
|
354
|
+
const commit = container.commits[commitId];
|
|
355
|
+
if (!commit)
|
|
356
|
+
continue;
|
|
357
|
+
for (const entryId of commit.entryIds) {
|
|
358
|
+
const entry = container.entries[entryId];
|
|
359
|
+
if (entry) {
|
|
360
|
+
ordered.push(entry);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return ordered;
|
|
365
|
+
}
|
|
366
|
+
function getProjectionSlice(entries, fromEntryId, toEntryId) {
|
|
367
|
+
const startIndex = fromEntryId ? entries.findIndex((entry) => entry.entryId === fromEntryId) : 0;
|
|
368
|
+
const endIndex = toEntryId ? entries.findIndex((entry) => entry.entryId === toEntryId) : entries.length - 1;
|
|
369
|
+
if (startIndex < 0) {
|
|
370
|
+
throw new Error(`from entry not found: ${fromEntryId}`);
|
|
371
|
+
}
|
|
372
|
+
if (toEntryId && endIndex < 0) {
|
|
373
|
+
throw new Error(`to entry not found: ${toEntryId}`);
|
|
374
|
+
}
|
|
375
|
+
if (entries.length === 0) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
return entries.slice(startIndex, endIndex + 1);
|
|
379
|
+
}
|
|
380
|
+
async function toReplayEntry(entry, decrypt, identity, armour) {
|
|
381
|
+
if (entry.payload.type === "plain") {
|
|
382
|
+
return {
|
|
383
|
+
entryId: entry.entryId,
|
|
384
|
+
kind: entry.kind,
|
|
385
|
+
author: entry.author,
|
|
386
|
+
authoredAt: entry.authoredAt,
|
|
387
|
+
meta: entry.meta,
|
|
388
|
+
payload: {
|
|
389
|
+
type: "plain",
|
|
390
|
+
data: cloneValue(entry.payload.data)
|
|
391
|
+
},
|
|
392
|
+
verified: true
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (decrypt && identity.decryptor) {
|
|
396
|
+
return {
|
|
397
|
+
entryId: entry.entryId,
|
|
398
|
+
kind: entry.kind,
|
|
399
|
+
author: entry.author,
|
|
400
|
+
authoredAt: entry.authoredAt,
|
|
401
|
+
meta: entry.meta,
|
|
402
|
+
payload: {
|
|
403
|
+
type: "decrypted",
|
|
404
|
+
original: "encrypted",
|
|
405
|
+
data: await armour.decrypt({
|
|
406
|
+
payload: entry.payload,
|
|
407
|
+
decryptor: identity.decryptor
|
|
408
|
+
})
|
|
409
|
+
},
|
|
410
|
+
verified: true,
|
|
411
|
+
decrypted: true
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
entryId: entry.entryId,
|
|
416
|
+
kind: entry.kind,
|
|
417
|
+
author: entry.author,
|
|
418
|
+
authoredAt: entry.authoredAt,
|
|
419
|
+
meta: entry.meta,
|
|
420
|
+
payload: {
|
|
421
|
+
type: "encrypted",
|
|
422
|
+
scheme: entry.payload.scheme,
|
|
423
|
+
mode: entry.payload.mode,
|
|
424
|
+
encoding: entry.payload.encoding,
|
|
425
|
+
data: entry.payload.data
|
|
426
|
+
},
|
|
427
|
+
verified: true,
|
|
428
|
+
decrypted: false
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function createGenesisCommitDraft(timestamp, metadata) {
|
|
432
|
+
return {
|
|
433
|
+
parentCommitId: null,
|
|
434
|
+
committedAt: timestamp,
|
|
435
|
+
metadata: {
|
|
436
|
+
genesis: true,
|
|
437
|
+
spec: LEDGER_SPEC,
|
|
438
|
+
...metadata ?? {}
|
|
439
|
+
},
|
|
440
|
+
entryIds: []
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async function createLedger(config) {
|
|
444
|
+
const now = config.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
445
|
+
const protocol = config.protocol ?? createDefaultProtocolContract();
|
|
446
|
+
const seal = config.seal ?? createDefaultSealContract();
|
|
447
|
+
const armour = config.armour ?? createDefaultArmourContract();
|
|
448
|
+
const replayPolicy = config.replayPolicy;
|
|
449
|
+
const state = createInitialState(config.initialProjection);
|
|
450
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
451
|
+
function notify() {
|
|
452
|
+
for (const listener of listeners) {
|
|
453
|
+
listener(state);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function persist() {
|
|
457
|
+
if (!config.storage)
|
|
458
|
+
return;
|
|
459
|
+
const snapshot = {
|
|
460
|
+
container: state.container ? cloneValue(state.container) : null,
|
|
461
|
+
staged: cloneValue(state.staged)
|
|
462
|
+
};
|
|
463
|
+
await config.storage.save(snapshot);
|
|
464
|
+
}
|
|
465
|
+
async function buildCommitRecord(unsignedCommit) {
|
|
466
|
+
const subjectBytes = protocol.getCommitSubjectBytes(unsignedCommit);
|
|
467
|
+
const commitId = await protocol.deriveCommitId(unsignedCommit);
|
|
468
|
+
const sealProof = await seal.createCommitProof({
|
|
469
|
+
commit: unsignedCommit,
|
|
470
|
+
commitId,
|
|
471
|
+
subjectBytes,
|
|
472
|
+
signer: config.identity.signer
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
...unsignedCommit,
|
|
476
|
+
commitId,
|
|
477
|
+
seal: sealProof
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function verifySnapshot(container, staged, options) {
|
|
481
|
+
if (!container) {
|
|
482
|
+
return {
|
|
483
|
+
valid: true,
|
|
484
|
+
committedHistoryValid: true,
|
|
485
|
+
commitChainValid: true,
|
|
486
|
+
commitProofsValid: true,
|
|
487
|
+
entriesValid: true,
|
|
488
|
+
entryProofsValid: true,
|
|
489
|
+
payloadHashesValid: true,
|
|
490
|
+
proofsValid: true,
|
|
491
|
+
invalidCommitIds: [],
|
|
492
|
+
invalidEntryIds: []
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const invalidCommitIds = /* @__PURE__ */ new Set();
|
|
496
|
+
const invalidEntryIds = /* @__PURE__ */ new Set();
|
|
497
|
+
let commitChainValid = true;
|
|
498
|
+
let commitProofsValid = true;
|
|
499
|
+
let entriesValid = true;
|
|
500
|
+
let entryProofsValid = true;
|
|
501
|
+
let payloadHashesValid = true;
|
|
502
|
+
let proofsValid = true;
|
|
503
|
+
let committedEntriesValid = true;
|
|
504
|
+
let committedEntryProofsValid = true;
|
|
505
|
+
let committedPayloadHashesValid = true;
|
|
506
|
+
const containerValidation = protocol.validateContainer(container);
|
|
507
|
+
if (!containerValidation.ok) {
|
|
508
|
+
commitChainValid = false;
|
|
509
|
+
}
|
|
510
|
+
const reachableCommitIds = /* @__PURE__ */ new Set();
|
|
511
|
+
const reachableEntryIds = /* @__PURE__ */ new Set();
|
|
512
|
+
for (const [commitId, commit2] of Object.entries(container.commits)) {
|
|
513
|
+
try {
|
|
514
|
+
const derivedCommitId = await protocol.deriveCommitId({
|
|
515
|
+
parentCommitId: commit2.parentCommitId,
|
|
516
|
+
committedAt: commit2.committedAt,
|
|
517
|
+
metadata: commit2.metadata,
|
|
518
|
+
entryIds: commit2.entryIds
|
|
519
|
+
});
|
|
520
|
+
if (derivedCommitId !== commitId) {
|
|
521
|
+
invalidCommitIds.add(commitId);
|
|
522
|
+
commitChainValid = false;
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
invalidCommitIds.add(commitId);
|
|
526
|
+
commitChainValid = false;
|
|
527
|
+
}
|
|
528
|
+
if (commit2.parentCommitId !== null && !container.commits[commit2.parentCommitId]) {
|
|
529
|
+
invalidCommitIds.add(commitId);
|
|
530
|
+
commitChainValid = false;
|
|
531
|
+
}
|
|
532
|
+
if (options?.includeProofs !== false) {
|
|
533
|
+
try {
|
|
534
|
+
const proofValid = await seal.verifyCommitProof({
|
|
535
|
+
commit: commit2,
|
|
536
|
+
subjectBytes: protocol.getCommitSubjectBytes({
|
|
537
|
+
parentCommitId: commit2.parentCommitId,
|
|
538
|
+
committedAt: commit2.committedAt,
|
|
539
|
+
metadata: commit2.metadata,
|
|
540
|
+
entryIds: commit2.entryIds
|
|
541
|
+
}),
|
|
542
|
+
proof: commit2.seal
|
|
543
|
+
});
|
|
544
|
+
if (!proofValid) {
|
|
545
|
+
invalidCommitIds.add(commitId);
|
|
546
|
+
commitProofsValid = false;
|
|
547
|
+
proofsValid = false;
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
invalidCommitIds.add(commitId);
|
|
551
|
+
commitProofsValid = false;
|
|
552
|
+
proofsValid = false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (const entryId of commit2.entryIds) {
|
|
556
|
+
if (!container.entries[entryId]) {
|
|
557
|
+
invalidEntryIds.add(entryId);
|
|
558
|
+
entriesValid = false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const chain = protocol.getCommitChain(container);
|
|
564
|
+
for (const commitId of chain) {
|
|
565
|
+
reachableCommitIds.add(commitId);
|
|
566
|
+
for (const entryId of container.commits[commitId]?.entryIds ?? []) {
|
|
567
|
+
reachableEntryIds.add(entryId);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
invalidCommitIds.add(container.head);
|
|
572
|
+
commitChainValid = false;
|
|
573
|
+
committedEntriesValid = false;
|
|
574
|
+
committedEntryProofsValid = false;
|
|
575
|
+
committedPayloadHashesValid = false;
|
|
576
|
+
}
|
|
577
|
+
const entriesToVerify = [
|
|
578
|
+
...Object.entries(container.entries).map(([recordKey, entry]) => ({
|
|
579
|
+
recordKey,
|
|
580
|
+
entry
|
|
581
|
+
})),
|
|
582
|
+
...staged.map((entry) => ({ recordKey: null, entry }))
|
|
583
|
+
];
|
|
584
|
+
for (const { recordKey, entry } of entriesToVerify) {
|
|
585
|
+
const isReachableCommittedEntry = recordKey !== null && reachableEntryIds.has(recordKey) || reachableEntryIds.has(entry.entryId);
|
|
586
|
+
try {
|
|
587
|
+
const derivedEntryId = await protocol.deriveEntryId(entry);
|
|
588
|
+
if (derivedEntryId !== entry.entryId) {
|
|
589
|
+
invalidEntryIds.add(entry.entryId);
|
|
590
|
+
entriesValid = false;
|
|
591
|
+
if (isReachableCommittedEntry) {
|
|
592
|
+
committedEntriesValid = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
invalidEntryIds.add(entry.entryId);
|
|
597
|
+
entriesValid = false;
|
|
598
|
+
if (isReachableCommittedEntry) {
|
|
599
|
+
committedEntriesValid = false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const subjectBytes = buildUnsignedEntrySubject({
|
|
603
|
+
kind: entry.kind,
|
|
604
|
+
authoredAt: entry.authoredAt,
|
|
605
|
+
author: entry.author,
|
|
606
|
+
meta: entry.meta,
|
|
607
|
+
payload: entry.payload
|
|
608
|
+
});
|
|
609
|
+
if (options?.includeProofs !== false) {
|
|
610
|
+
try {
|
|
611
|
+
const proofValid = await seal.verifyEntryProof({
|
|
612
|
+
entry,
|
|
613
|
+
subjectBytes,
|
|
614
|
+
proof: entry.seal
|
|
615
|
+
});
|
|
616
|
+
if (!proofValid) {
|
|
617
|
+
invalidEntryIds.add(entry.entryId);
|
|
618
|
+
entriesValid = false;
|
|
619
|
+
entryProofsValid = false;
|
|
620
|
+
proofsValid = false;
|
|
621
|
+
if (isReachableCommittedEntry) {
|
|
622
|
+
committedEntriesValid = false;
|
|
623
|
+
committedEntryProofsValid = false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
invalidEntryIds.add(entry.entryId);
|
|
628
|
+
entriesValid = false;
|
|
629
|
+
entryProofsValid = false;
|
|
630
|
+
proofsValid = false;
|
|
631
|
+
if (isReachableCommittedEntry) {
|
|
632
|
+
committedEntriesValid = false;
|
|
633
|
+
committedEntryProofsValid = false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (entry.payload.type === "encrypted" && options?.includePayloadHashes !== false) {
|
|
638
|
+
try {
|
|
639
|
+
const payloadHash = await createSealHash(toCiphertextBytes(entry.payload));
|
|
640
|
+
if (payloadHash !== entry.payload.payloadHash) {
|
|
641
|
+
invalidEntryIds.add(entry.entryId);
|
|
642
|
+
entriesValid = false;
|
|
643
|
+
payloadHashesValid = false;
|
|
644
|
+
if (isReachableCommittedEntry) {
|
|
645
|
+
committedEntriesValid = false;
|
|
646
|
+
committedPayloadHashesValid = false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} catch {
|
|
650
|
+
invalidEntryIds.add(entry.entryId);
|
|
651
|
+
entriesValid = false;
|
|
652
|
+
payloadHashesValid = false;
|
|
653
|
+
if (isReachableCommittedEntry) {
|
|
654
|
+
committedEntriesValid = false;
|
|
655
|
+
committedPayloadHashesValid = false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const entryId of reachableEntryIds) {
|
|
661
|
+
if (!container.entries[entryId]) {
|
|
662
|
+
committedEntriesValid = false;
|
|
663
|
+
committedEntryProofsValid = false;
|
|
664
|
+
committedPayloadHashesValid = false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const committedHistoryValid = commitChainValid && commitProofsValid && committedEntriesValid && committedEntryProofsValid && committedPayloadHashesValid && containerValidation.ok;
|
|
668
|
+
return {
|
|
669
|
+
valid: committedHistoryValid,
|
|
670
|
+
committedHistoryValid,
|
|
671
|
+
commitChainValid,
|
|
672
|
+
commitProofsValid,
|
|
673
|
+
entriesValid,
|
|
674
|
+
entryProofsValid,
|
|
675
|
+
payloadHashesValid,
|
|
676
|
+
proofsValid,
|
|
677
|
+
invalidCommitIds: sortStrings(invalidCommitIds),
|
|
678
|
+
invalidEntryIds: sortStrings(invalidEntryIds)
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
async function verifyCurrent(options) {
|
|
682
|
+
return verifySnapshot(state.container, state.staged, options);
|
|
683
|
+
}
|
|
684
|
+
async function rebuildProjection(options) {
|
|
685
|
+
const merged = mergeReplayOptions(
|
|
686
|
+
replayPolicy,
|
|
687
|
+
options,
|
|
688
|
+
!!config.identity.decryptor
|
|
689
|
+
);
|
|
690
|
+
if (merged.verify) {
|
|
691
|
+
const verification = await verifyCurrent();
|
|
692
|
+
state.verification = {
|
|
693
|
+
valid: verification.valid,
|
|
694
|
+
checkedAt: now()
|
|
695
|
+
};
|
|
696
|
+
notify();
|
|
697
|
+
if (!verification.valid) {
|
|
698
|
+
throw new Error("Ledger verification failed.");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
let orderedCommitted = [];
|
|
702
|
+
if (state.container) {
|
|
703
|
+
try {
|
|
704
|
+
orderedCommitted = getCommittedEntriesInOrder(state.container, protocol);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (merged.verify) {
|
|
707
|
+
throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const orderedEntries = [...orderedCommitted, ...state.staged];
|
|
712
|
+
const slice = getProjectionSlice(
|
|
713
|
+
orderedEntries,
|
|
714
|
+
merged.fromEntryId,
|
|
715
|
+
merged.toEntryId
|
|
716
|
+
);
|
|
717
|
+
let projection = createEmptyProjection(config.initialProjection);
|
|
718
|
+
for (const entry of slice) {
|
|
719
|
+
projection = config.projector(
|
|
720
|
+
projection,
|
|
721
|
+
await toReplayEntry(entry, merged.decrypt, config.identity, armour)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
state.projection = projection;
|
|
725
|
+
notify();
|
|
726
|
+
return projection;
|
|
727
|
+
}
|
|
728
|
+
async function buildEntryRecord(input) {
|
|
729
|
+
assertAppendInput(input);
|
|
730
|
+
protocol.canonicalizePayload(input.payload);
|
|
731
|
+
const payloadValue = normalizePayloadInput(input.payload);
|
|
732
|
+
const meta = normalizeMeta(input.meta);
|
|
733
|
+
const authoredAt = now();
|
|
734
|
+
const author = await config.identity.authorResolver();
|
|
735
|
+
const payload = input.protection?.type === "recipients" ? {
|
|
736
|
+
type: "encrypted",
|
|
737
|
+
scheme: "age",
|
|
738
|
+
mode: "recipients",
|
|
739
|
+
encoding: input.protection.encoding ?? "armor",
|
|
740
|
+
...await armour.encrypt({
|
|
741
|
+
recipients: config.identity.recipientResolver ? await config.identity.recipientResolver(input.protection.recipients) : input.protection.recipients,
|
|
742
|
+
data: textEncoder.encode(protocol.canonicalizePayload(payloadValue)),
|
|
743
|
+
encoding: input.protection.encoding ?? "armor"
|
|
744
|
+
})
|
|
745
|
+
} : {
|
|
746
|
+
type: "plain",
|
|
747
|
+
data: cloneValue(payloadValue)
|
|
748
|
+
};
|
|
749
|
+
const unsignedEntry = {
|
|
750
|
+
kind: input.kind,
|
|
751
|
+
authoredAt,
|
|
752
|
+
author,
|
|
753
|
+
meta,
|
|
754
|
+
payload
|
|
755
|
+
};
|
|
756
|
+
const subjectBytes = buildUnsignedEntrySubject(unsignedEntry);
|
|
757
|
+
const sealProof = await seal.createEntryProof({
|
|
758
|
+
entry: unsignedEntry,
|
|
759
|
+
subjectBytes,
|
|
760
|
+
signer: config.identity.signer
|
|
761
|
+
});
|
|
762
|
+
const draft = {
|
|
763
|
+
entryId: "",
|
|
764
|
+
...unsignedEntry,
|
|
765
|
+
seal: sealProof
|
|
766
|
+
};
|
|
767
|
+
return {
|
|
768
|
+
...draft,
|
|
769
|
+
entryId: await protocol.deriveEntryId(draft)
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function assertContainerExists() {
|
|
773
|
+
if (!state.container) {
|
|
774
|
+
throw new Error("Ledger has not been created or loaded.");
|
|
775
|
+
}
|
|
776
|
+
return state.container;
|
|
777
|
+
}
|
|
778
|
+
async function stageEntries(inputs) {
|
|
779
|
+
const container = assertContainerExists();
|
|
780
|
+
const existingIds = /* @__PURE__ */ new Set([
|
|
781
|
+
...Object.keys(container.entries),
|
|
782
|
+
...state.staged.map((entry) => entry.entryId)
|
|
783
|
+
]);
|
|
784
|
+
const entries = [];
|
|
785
|
+
for (const input of inputs) {
|
|
786
|
+
const entry = await buildEntryRecord(input);
|
|
787
|
+
if (existingIds.has(entry.entryId)) {
|
|
788
|
+
throw new Error(`Entry ${entry.entryId} already exists.`);
|
|
789
|
+
}
|
|
790
|
+
existingIds.add(entry.entryId);
|
|
791
|
+
entries.push(entry);
|
|
792
|
+
}
|
|
793
|
+
const baseCount = state.staged.length;
|
|
794
|
+
state.staged = [...state.staged, ...entries];
|
|
795
|
+
const results = entries.map((entry, index) => ({
|
|
796
|
+
entry,
|
|
797
|
+
stagedCount: baseCount + index + 1
|
|
798
|
+
}));
|
|
799
|
+
if (config.autoCommit && entries.length > 0) {
|
|
800
|
+
await commit();
|
|
801
|
+
return results;
|
|
802
|
+
}
|
|
803
|
+
await rebuildProjection();
|
|
804
|
+
await persist();
|
|
805
|
+
return results;
|
|
806
|
+
}
|
|
807
|
+
async function create(params) {
|
|
808
|
+
const genesis = await buildCommitRecord(
|
|
809
|
+
createGenesisCommitDraft(now(), normalizeMeta(params?.metadata))
|
|
810
|
+
);
|
|
811
|
+
state.container = {
|
|
812
|
+
format: LEDGER_FORMAT,
|
|
813
|
+
version: LEDGER_VERSION,
|
|
814
|
+
commits: {
|
|
815
|
+
[genesis.commitId]: genesis
|
|
816
|
+
},
|
|
817
|
+
entries: {},
|
|
818
|
+
head: genesis.commitId
|
|
819
|
+
};
|
|
820
|
+
state.staged = [];
|
|
821
|
+
state.verification = null;
|
|
822
|
+
await rebuildProjection();
|
|
823
|
+
await persist();
|
|
824
|
+
}
|
|
825
|
+
async function load(container) {
|
|
826
|
+
assertContainerInput(container);
|
|
827
|
+
state.container = cloneValue(container);
|
|
828
|
+
state.staged = [];
|
|
829
|
+
state.verification = null;
|
|
830
|
+
await rebuildProjection();
|
|
831
|
+
await persist();
|
|
832
|
+
}
|
|
833
|
+
async function loadFromStorage() {
|
|
834
|
+
if (!config.storage) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
const snapshot = await config.storage.load();
|
|
838
|
+
if (!snapshot) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
if (snapshot.container) {
|
|
842
|
+
assertContainerInput(snapshot.container);
|
|
843
|
+
}
|
|
844
|
+
for (const stagedEntry of snapshot.staged) {
|
|
845
|
+
const errors = validateEntryShape(stagedEntry);
|
|
846
|
+
if (errors.length > 0) {
|
|
847
|
+
throw new Error(errors.join("; "));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
state.container = snapshot.container ? cloneValue(snapshot.container) : null;
|
|
851
|
+
state.staged = cloneValue(snapshot.staged);
|
|
852
|
+
state.verification = null;
|
|
853
|
+
await rebuildProjection();
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
async function append(input) {
|
|
857
|
+
const [result] = await stageEntries([input]);
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
async function appendMany(inputs) {
|
|
861
|
+
if (inputs.length === 0) {
|
|
862
|
+
return [];
|
|
863
|
+
}
|
|
864
|
+
return stageEntries(inputs);
|
|
865
|
+
}
|
|
866
|
+
async function commit(input) {
|
|
867
|
+
const container = assertContainerExists();
|
|
868
|
+
if (state.staged.length === 0) {
|
|
869
|
+
throw new Error("No staged entries to commit.");
|
|
870
|
+
}
|
|
871
|
+
const staged = cloneValue(state.staged);
|
|
872
|
+
const entries = { ...container.entries };
|
|
873
|
+
for (const entry of staged) {
|
|
874
|
+
entries[entry.entryId] = entry;
|
|
875
|
+
}
|
|
876
|
+
const unsignedCommit = {
|
|
877
|
+
parentCommitId: container.head,
|
|
878
|
+
committedAt: now(),
|
|
879
|
+
metadata: normalizeMeta(input?.metadata),
|
|
880
|
+
entryIds: staged.map((entry) => entry.entryId)
|
|
881
|
+
};
|
|
882
|
+
const commitRecord = await buildCommitRecord(unsignedCommit);
|
|
883
|
+
state.container = {
|
|
884
|
+
format: container.format,
|
|
885
|
+
version: container.version,
|
|
886
|
+
commits: {
|
|
887
|
+
...container.commits,
|
|
888
|
+
[commitRecord.commitId]: commitRecord
|
|
889
|
+
},
|
|
890
|
+
entries,
|
|
891
|
+
head: commitRecord.commitId
|
|
892
|
+
};
|
|
893
|
+
state.staged = [];
|
|
894
|
+
await rebuildProjection();
|
|
895
|
+
await persist();
|
|
896
|
+
return {
|
|
897
|
+
commit: cloneValue(commitRecord),
|
|
898
|
+
committedEntries: staged,
|
|
899
|
+
committedEntryIds: staged.map((entry) => entry.entryId)
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async function replay(options) {
|
|
903
|
+
return rebuildProjection(options);
|
|
904
|
+
}
|
|
905
|
+
async function recompute() {
|
|
906
|
+
return rebuildProjection();
|
|
907
|
+
}
|
|
908
|
+
async function verify(options) {
|
|
909
|
+
return verifyCurrent(options);
|
|
910
|
+
}
|
|
911
|
+
async function exportContainer() {
|
|
912
|
+
const container = assertContainerExists();
|
|
913
|
+
return cloneValue(container);
|
|
914
|
+
}
|
|
915
|
+
async function importContainer(container) {
|
|
916
|
+
await load(container);
|
|
917
|
+
}
|
|
918
|
+
function getState() {
|
|
919
|
+
return state;
|
|
920
|
+
}
|
|
921
|
+
function subscribe(listener) {
|
|
922
|
+
listeners.add(listener);
|
|
923
|
+
return () => {
|
|
924
|
+
listeners.delete(listener);
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
async function clearStaged() {
|
|
928
|
+
state.staged = [];
|
|
929
|
+
await rebuildProjection();
|
|
930
|
+
await persist();
|
|
931
|
+
}
|
|
932
|
+
async function destroy() {
|
|
933
|
+
state.container = null;
|
|
934
|
+
state.staged = [];
|
|
935
|
+
state.projection = createEmptyProjection(config.initialProjection);
|
|
936
|
+
state.verification = null;
|
|
937
|
+
notify();
|
|
938
|
+
if (config.storage?.clear) {
|
|
939
|
+
await config.storage.clear();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
create,
|
|
944
|
+
load,
|
|
945
|
+
loadFromStorage,
|
|
946
|
+
append,
|
|
947
|
+
appendMany,
|
|
948
|
+
commit,
|
|
949
|
+
replay,
|
|
950
|
+
recompute,
|
|
951
|
+
verify,
|
|
952
|
+
export: exportContainer,
|
|
953
|
+
import: importContainer,
|
|
954
|
+
getState,
|
|
955
|
+
subscribe,
|
|
956
|
+
clearStaged,
|
|
957
|
+
destroy
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
export {
|
|
961
|
+
createLedger
|
|
962
|
+
};
|
|
963
|
+
//# sourceMappingURL=index.js.map
|