@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/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