@forestrie/forestrie-cli 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/TRUST-MODEL.md +8 -0
  4. package/dist/cli.js +33811 -0
  5. package/dist/decode-receipt.d.ts +19 -0
  6. package/dist/decode-receipt.d.ts.map +1 -0
  7. package/dist/decode-receipt.js +19 -0
  8. package/dist/decode-receipt.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +11 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/decode-receipt-cbor.d.ts +26 -0
  14. package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
  15. package/dist/lib/decode-receipt-cbor.js +182 -0
  16. package/dist/lib/decode-receipt-cbor.js.map +1 -0
  17. package/dist/lib/decode-receipt-decode.d.ts +103 -0
  18. package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
  19. package/dist/lib/decode-receipt-decode.js +237 -0
  20. package/dist/lib/decode-receipt-decode.js.map +1 -0
  21. package/dist/lib/decode-receipt-labels.d.ts +54 -0
  22. package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
  23. package/dist/lib/decode-receipt-labels.js +117 -0
  24. package/dist/lib/decode-receipt-labels.js.map +1 -0
  25. package/dist/lib/decode-receipt-render.d.ts +11 -0
  26. package/dist/lib/decode-receipt-render.d.ts.map +1 -0
  27. package/dist/lib/decode-receipt-render.js +242 -0
  28. package/dist/lib/decode-receipt-render.js.map +1 -0
  29. package/package.json +83 -0
  30. package/src/cli.ts +5 -0
  31. package/src/command.ts +51 -0
  32. package/src/commands/admin-onboard-token.ts +51 -0
  33. package/src/commands/admin.ts +20 -0
  34. package/src/commands/complete-grant.ts +41 -0
  35. package/src/commands/create-consistency-proof.ts +44 -0
  36. package/src/commands/create-log.ts +95 -0
  37. package/src/commands/decode-receipt.ts +20 -0
  38. package/src/commands/delegate.ts +55 -0
  39. package/src/commands/deploy.ts +72 -0
  40. package/src/commands/fetch-accumulator.ts +49 -0
  41. package/src/commands/onboard-genesis.ts +70 -0
  42. package/src/commands/onboard-request.ts +59 -0
  43. package/src/commands/register-grant.ts +74 -0
  44. package/src/commands/register.ts +54 -0
  45. package/src/commands/resolve-receipt.ts +101 -0
  46. package/src/commands/sign-statement.ts +53 -0
  47. package/src/commands/verify-grant.ts +89 -0
  48. package/src/commands/verify.ts +92 -0
  49. package/src/commoncli.ts +34 -0
  50. package/src/decode-receipt.ts +56 -0
  51. package/src/index.ts +10 -0
  52. package/src/lib/bytes.ts +7 -0
  53. package/src/lib/checkpoint-provider.ts +297 -0
  54. package/src/lib/complete-grant-derive.ts +237 -0
  55. package/src/lib/consistency-proof.ts +271 -0
  56. package/src/lib/create-log-build.ts +168 -0
  57. package/src/lib/create-log-prepare.ts +75 -0
  58. package/src/lib/create-receipt-chain-verify.ts +267 -0
  59. package/src/lib/create-receipt-derive.ts +208 -0
  60. package/src/lib/create-receipt-inputs.ts +79 -0
  61. package/src/lib/decode-checkpoint-calldata.ts +188 -0
  62. package/src/lib/decode-receipt-cbor.ts +202 -0
  63. package/src/lib/decode-receipt-decode.ts +333 -0
  64. package/src/lib/decode-receipt-labels.ts +132 -0
  65. package/src/lib/decode-receipt-render.ts +284 -0
  66. package/src/lib/delegate-flow.ts +289 -0
  67. package/src/lib/delegate-voucher.ts +122 -0
  68. package/src/lib/deploy-artifact.ts +79 -0
  69. package/src/lib/deploy-bootstrap.ts +100 -0
  70. package/src/lib/deploy-flow.ts +184 -0
  71. package/src/lib/deploy-genesis-log-id.ts +29 -0
  72. package/src/lib/deploy-rpc.ts +40 -0
  73. package/src/lib/deployment-record.ts +65 -0
  74. package/src/lib/fsio.ts +91 -0
  75. package/src/lib/grant-flags-local.ts +19 -0
  76. package/src/lib/onboard-attestation.ts +120 -0
  77. package/src/lib/onboard-genesis-body.ts +94 -0
  78. package/src/lib/openssl-error-queue.ts +56 -0
  79. package/src/lib/register-flow.ts +292 -0
  80. package/src/lib/register-grant-build.ts +173 -0
  81. package/src/lib/register-grant-complete.ts +47 -0
  82. package/src/lib/register-grant-flow.ts +258 -0
  83. package/src/lib/resolve-receipt-freshen.ts +221 -0
  84. package/src/lib/sign-statement-build.ts +117 -0
  85. package/src/lib/sign-statement-key.ts +145 -0
  86. package/src/lib/verify-anchored.ts +326 -0
  87. package/src/lib/verify-checkpoint-chain.ts +160 -0
  88. package/src/lib/verify-eventscan.ts +214 -0
  89. package/src/lib/verify-inputs.ts +143 -0
  90. package/src/lib/verify-known-accumulator.ts +161 -0
  91. package/src/lib/verify-report.ts +265 -0
  92. package/src/lib/x402-payment.ts +115 -0
  93. package/src/main/admin-onboard-token.ts +216 -0
  94. package/src/main/complete-grant.ts +265 -0
  95. package/src/main/create-consistency-proof.ts +83 -0
  96. package/src/main/create-log.ts +347 -0
  97. package/src/main/create-receipt.ts +763 -0
  98. package/src/main/decode-receipt.ts +72 -0
  99. package/src/main/delegate.ts +141 -0
  100. package/src/main/deploy.ts +273 -0
  101. package/src/main/fetch-accumulator.ts +181 -0
  102. package/src/main/not-implemented.ts +43 -0
  103. package/src/main/onboard-genesis.ts +192 -0
  104. package/src/main/onboard-request.ts +333 -0
  105. package/src/main/register-grant.ts +270 -0
  106. package/src/main/register.ts +243 -0
  107. package/src/main/sign-statement.ts +168 -0
  108. package/src/main/verify.ts +767 -0
  109. package/src/options/admin-onboard-token.ts +48 -0
  110. package/src/options/common.ts +69 -0
  111. package/src/options/complete-grant.ts +40 -0
  112. package/src/options/create-consistency-proof.ts +63 -0
  113. package/src/options/create-log.ts +99 -0
  114. package/src/options/create-receipt.ts +243 -0
  115. package/src/options/decode-receipt.ts +25 -0
  116. package/src/options/delegate.ts +93 -0
  117. package/src/options/deploy.ts +105 -0
  118. package/src/options/fetch-accumulator.ts +62 -0
  119. package/src/options/onboard-genesis.ts +74 -0
  120. package/src/options/onboard-request.ts +57 -0
  121. package/src/options/register-grant.ts +72 -0
  122. package/src/options/register.ts +71 -0
  123. package/src/options/sign-statement.ts +64 -0
  124. package/src/options/verify.ts +250 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Checkpoint-chain providers (FOR-418 Phase 2, plan-2607-32).
3
+ *
4
+ * A *provider* yields the log's ordered chain of consistency-proof links folded
5
+ * into the accumulator at each seal, so consumers (verify, `resolve-receipt`)
6
+ * work over `CheckpointLink[]` regardless of source. Two tile-free sources:
7
+ *
8
+ * - **chain calldata** (`calldataCheckpointChain`) — the `publishCheckpoint`
9
+ * transactions carry the whole `ConsistencyReceipt` (Phase 1).
10
+ * - **retained `.sth`** (`sthCheckpointChain`) — the store's signed checkpoints.
11
+ *
12
+ * TRUST (important — see the Phase 2 review, plan-2607-32 R1): a `CheckpointLink`
13
+ * accumulator here is **RPC-/store-asserted, NOT authenticated by this module**.
14
+ * The provider folds and does a cheap self-consistency cross-check (calldata
15
+ * fold vs the `CheckpointPublished` event accumulator), but it does **not**
16
+ * verify the COSE signature. So that the consumer *can*, each directly-signed
17
+ * link carries its `seal` (the signature + delegation for calldata; the
18
+ * checkpoint bytes for `.sth`); the consumer applies the genesis/known-key trust
19
+ * root against those seals (Phase 4). Both sources decode to the identical
20
+ * `CheckpointConsistencyProof` shape and fold identically — the parity that
21
+ * makes them interchangeable (see the tests).
22
+ *
23
+ * Each link also carries the raw `proof` it was folded from (its per-peak
24
+ * `paths`), so a consumer that needs the climb material rather than just the
25
+ * folded accumulator — `resolve-receipt`'s freshen (Phase 3c), which extends an
26
+ * old inclusion path to the latest peak — gets it in one pass with no re-decode
27
+ * and no second RPC sweep: `links.map((l) => l.proof)` is exactly freshen's
28
+ * ordered `consistencyProofs`. Consumers that only fold (verify) ignore it.
29
+ */
30
+ import {
31
+ computeCheckpointAccumulator,
32
+ checkpointConsistencyProof,
33
+ type CheckpointConsistencyProof,
34
+ } from "@forestrie/receipt-verify";
35
+ import { fetchPublishedCheckpoints } from "./verify-eventscan.js";
36
+ import {
37
+ fetchPublishCheckpointCalldata,
38
+ type CalldataDelegation,
39
+ } from "./decode-checkpoint-calldata.js";
40
+ import { bytesEqual } from "./bytes.js";
41
+
42
+ /**
43
+ * The signed checkpoint a link's accumulator is sealed by, retained so the
44
+ * consumer can verify it (this module does not). Present on the directly-signed
45
+ * link of each unit — every `.sth`; the FINAL link of each calldata tx's proof
46
+ * segment (intermediate links in a multi-proof tx are unsigned, transitively
47
+ * trusted via the fold to the next sealed link).
48
+ */
49
+ export type CheckpointSeal =
50
+ | {
51
+ kind: "calldata";
52
+ protectedHeader: Uint8Array;
53
+ signature: Uint8Array;
54
+ delegation: CalldataDelegation;
55
+ }
56
+ | { kind: "sth"; checkpointBytes: Uint8Array };
57
+
58
+ /** One folded link: the accumulator committed at `treeSize2`. */
59
+ export type CheckpointLink = {
60
+ treeSize1: bigint;
61
+ treeSize2: bigint;
62
+ /** Folded accumulator at `treeSize2` (descending-height / contract order). */
63
+ accumulator: Uint8Array[];
64
+ /** The raw consistency proof this link was folded from — its per-peak `paths`
65
+ * (the tile-free climb material). Retained so a path-extending consumer
66
+ * (freshen) gets it without a re-decode; folding consumers ignore it. */
67
+ proof: CheckpointConsistencyProof;
68
+ /** The signature material to verify this link's accumulator, when directly
69
+ * signed (see {@link CheckpointSeal}). The provider does NOT verify it. */
70
+ seal?: CheckpointSeal;
71
+ /** Where this link came from (a tx hash, an `.sth` name) — for narration. */
72
+ sourceRef?: string;
73
+ };
74
+
75
+ /**
76
+ * Fold an ordered consistency-proof chain into per-link accumulators. Each
77
+ * link's base must equal the previous link's sealed size — a mismatch is the
78
+ * legacy / non-contiguous signature and throws. For a suffix chain, pass BOTH
79
+ * `accumulatorFrom` and `accumulatorFromSize` (the size that seed is for); the
80
+ * first link's `treeSize1` is bound to it (R2). Without a seed the chain must
81
+ * start at base 0.
82
+ */
83
+ export async function foldProofChain(
84
+ proofs: readonly CheckpointConsistencyProof[],
85
+ opts: {
86
+ accumulatorFrom?: Uint8Array[];
87
+ accumulatorFromSize?: bigint;
88
+ seals?: readonly (CheckpointSeal | undefined)[];
89
+ sourceRefs?: readonly string[];
90
+ } = {},
91
+ ): Promise<CheckpointLink[]> {
92
+ let accumulator = opts.accumulatorFrom ?? [];
93
+ let expectedBase: bigint;
94
+ if (opts.accumulatorFrom !== undefined) {
95
+ if (opts.accumulatorFromSize === undefined) {
96
+ throw new Error(
97
+ "foldProofChain: accumulatorFrom requires accumulatorFromSize (the size the seed accumulator is for)",
98
+ );
99
+ }
100
+ expectedBase = opts.accumulatorFromSize;
101
+ } else {
102
+ expectedBase = 0n;
103
+ }
104
+ const links: CheckpointLink[] = [];
105
+ for (let i = 0; i < proofs.length; i++) {
106
+ const p = proofs[i]!;
107
+ if (p.treeSize1 !== expectedBase) {
108
+ throw new Error(
109
+ `checkpoint chain is not contiguous at link ${i}: base ${p.treeSize1} != expected ${expectedBase}`,
110
+ );
111
+ }
112
+ accumulator = await computeCheckpointAccumulator(p, accumulator);
113
+ const link: CheckpointLink = {
114
+ treeSize1: p.treeSize1,
115
+ treeSize2: p.treeSize2,
116
+ accumulator,
117
+ proof: p,
118
+ };
119
+ const seal = opts.seals?.[i];
120
+ if (seal !== undefined) link.seal = seal;
121
+ const ref = opts.sourceRefs?.[i];
122
+ if (ref !== undefined) link.sourceRef = ref;
123
+ links.push(link);
124
+ expectedBase = p.treeSize2;
125
+ }
126
+ return links;
127
+ }
128
+
129
+ /**
130
+ * Retained-`.sth` provider: decode each checkpoint's embedded consistency proof
131
+ * and fold. `checkpoints` are the raw `.sth` bytes in ascending massif order;
132
+ * `sourceRefs` optionally names them (e.g. filenames) for narration.
133
+ */
134
+ export async function sthCheckpointChain(
135
+ checkpoints: readonly Uint8Array[],
136
+ opts: {
137
+ accumulatorFrom?: Uint8Array[];
138
+ accumulatorFromSize?: bigint;
139
+ sourceRefs?: readonly string[];
140
+ } = {},
141
+ ): Promise<CheckpointLink[]> {
142
+ const proofs = checkpoints.map((bytes) => checkpointConsistencyProof(bytes));
143
+ const seals: CheckpointSeal[] = checkpoints.map((bytes) => ({
144
+ kind: "sth",
145
+ checkpointBytes: bytes,
146
+ }));
147
+ return foldProofChain(proofs, {
148
+ seals,
149
+ ...(opts.accumulatorFrom !== undefined
150
+ ? { accumulatorFrom: opts.accumulatorFrom }
151
+ : {}),
152
+ ...(opts.accumulatorFromSize !== undefined
153
+ ? { accumulatorFromSize: opts.accumulatorFromSize }
154
+ : {}),
155
+ ...(opts.sourceRefs !== undefined ? { sourceRefs: opts.sourceRefs } : {}),
156
+ });
157
+ }
158
+
159
+ /** Run `fn` over `items` with bounded concurrency, preserving order (R5). */
160
+ async function mapBounded<T, R>(
161
+ items: readonly T[],
162
+ limit: number,
163
+ fn: (item: T, index: number) => Promise<R>,
164
+ ): Promise<R[]> {
165
+ const out: R[] = new Array(items.length);
166
+ let next = 0;
167
+ async function worker(): Promise<void> {
168
+ for (;;) {
169
+ const i = next++;
170
+ if (i >= items.length) return;
171
+ out[i] = await fn(items[i]!, i);
172
+ }
173
+ }
174
+ const n = Math.min(Math.max(1, limit), items.length || 1);
175
+ await Promise.all(Array.from({ length: n }, () => worker()));
176
+ return out;
177
+ }
178
+
179
+ function accumulatorsEqual(a: Uint8Array[], b: Uint8Array[]): boolean {
180
+ if (a.length !== b.length) return false;
181
+ for (let i = 0; i < a.length; i++) if (!bytesEqual(a[i]!, b[i]!)) return false;
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * Chain-calldata provider: find the log's `CheckpointPublished` transactions
187
+ * (ascending), read each `publishCheckpoint`'s calldata, concatenate the
188
+ * embedded consistency-proof chains, and fold. Cross-checks each tx's folded
189
+ * accumulator against the `CheckpointPublished` event's accumulator (R3), and
190
+ * retains each tx's seal for the consumer to verify (R1); it does NOT verify
191
+ * signatures here (Phase 4). A bounded `--from-block` scan is only valid with a
192
+ * trusted seed at that block (R2): pass `accumulatorFrom` + `accumulatorFromSize`.
193
+ */
194
+ export async function calldataCheckpointChain(opts: {
195
+ univocity: string;
196
+ logId: string;
197
+ rpcUrl: string;
198
+ fromBlock?: bigint | undefined;
199
+ accumulatorFrom?: Uint8Array[] | undefined;
200
+ accumulatorFromSize?: bigint | undefined;
201
+ fetchImpl?: typeof fetch;
202
+ }): Promise<CheckpointLink[]> {
203
+ if (opts.fromBlock !== undefined && opts.accumulatorFrom === undefined) {
204
+ throw new Error(
205
+ "calldataCheckpointChain: --from-block must be paired with a trusted seed (accumulatorFrom + accumulatorFromSize) — a bounded scan starts mid-chain and cannot fold from base 0",
206
+ );
207
+ }
208
+ const published = await fetchPublishedCheckpoints({
209
+ univocity: opts.univocity,
210
+ logId: opts.logId,
211
+ rpcUrl: opts.rpcUrl,
212
+ fromBlock: opts.fromBlock,
213
+ ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
214
+ });
215
+ const decoded = await mapBounded(published, 8, (cp) =>
216
+ fetchPublishCheckpointCalldata({
217
+ rpcUrl: opts.rpcUrl,
218
+ txHash: cp.txHash,
219
+ ...(opts.fetchImpl !== undefined ? { fetchImpl: opts.fetchImpl } : {}),
220
+ }),
221
+ );
222
+
223
+ const proofs: CheckpointConsistencyProof[] = [];
224
+ const seals: (CheckpointSeal | undefined)[] = [];
225
+ const sourceRefs: string[] = [];
226
+ const finalLinkIndex: number[] = []; // link index of each tx's last proof
227
+ for (let t = 0; t < published.length; t++) {
228
+ const d = decoded[t]!;
229
+ const n = d.consistencyProofs.length;
230
+ d.consistencyProofs.forEach((p, k) => {
231
+ proofs.push(p);
232
+ sourceRefs.push(published[t]!.txHash);
233
+ seals.push(
234
+ k === n - 1
235
+ ? {
236
+ kind: "calldata",
237
+ protectedHeader: d.protectedHeader,
238
+ signature: d.signature,
239
+ delegation: d.delegation,
240
+ }
241
+ : undefined,
242
+ );
243
+ });
244
+ finalLinkIndex.push(proofs.length - 1);
245
+ }
246
+
247
+ const links = await foldProofChain(proofs, {
248
+ seals,
249
+ sourceRefs,
250
+ ...(opts.accumulatorFrom !== undefined
251
+ ? { accumulatorFrom: opts.accumulatorFrom }
252
+ : {}),
253
+ ...(opts.accumulatorFromSize !== undefined
254
+ ? { accumulatorFromSize: opts.accumulatorFromSize }
255
+ : {}),
256
+ });
257
+
258
+ // R3: the folded accumulator at each tx's seal MUST equal the accumulator the
259
+ // CheckpointPublished event reported (both from the RPC — this catches a fold
260
+ // bug or an RPC serving inconsistent event/calldata, not a fully malicious
261
+ // RPC; the signature seal is the real anchor, applied by the consumer).
262
+ for (let t = 0; t < published.length; t++) {
263
+ const link = links[finalLinkIndex[t]!]!;
264
+ const cp = published[t]!;
265
+ if (link.treeSize2 !== cp.size) {
266
+ throw new Error(
267
+ `calldata size ${link.treeSize2} disagrees with CheckpointPublished size ${cp.size} (tx ${cp.txHash})`,
268
+ );
269
+ }
270
+ if (!accumulatorsEqual(link.accumulator, cp.accumulator)) {
271
+ throw new Error(
272
+ `folded calldata accumulator disagrees with the CheckpointPublished event at size ${cp.size} (tx ${cp.txHash}) — inconsistent RPC data`,
273
+ );
274
+ }
275
+ }
276
+ return links;
277
+ }
278
+
279
+ /**
280
+ * Find the newest link whose folded accumulator contains `peak` (a receipt's
281
+ * recomputed peak). Newest-first: a match at any authenticated link proves the
282
+ * receipt — later links' proofs commit it forward. Null when no link covers it.
283
+ */
284
+ export function findPeakInChain(
285
+ links: readonly CheckpointLink[],
286
+ peak: Uint8Array,
287
+ ): { link: CheckpointLink; matchedPeak: number } | null {
288
+ for (let i = links.length - 1; i >= 0; i--) {
289
+ const link = links[i]!;
290
+ for (let j = 0; j < link.accumulator.length; j++) {
291
+ if (bytesEqual(peak, link.accumulator[j]!)) {
292
+ return { link, matchedPeak: j };
293
+ }
294
+ }
295
+ }
296
+ return null;
297
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * FOR-344 offline grant completion. `register-grant` gets the receipt +
3
+ * idtimestamp from the operator (register → 303 → poll → receipt);
4
+ * `complete-grant` instead DERIVES them locally from the checkpoint + massif,
5
+ * then runs the exact same merge (`completeGrantBase64`). Proving grants are
6
+ * *derivable from log data*, not operator-issued.
7
+ *
8
+ * Pure over bytes: decode the grant, locate its leaf in the massif by content
9
+ * hash (`findGrantLeafInMassif` — recovers mmrIndex + the sequenced
10
+ * idtimestamp), rebuild the leaf's inclusion receipt against the checkpoint's
11
+ * pre-signed peak (`deriveCheckpointReceipt`), and attach receipt (396) +
12
+ * idtimestamp (−65537) into the grant's unprotected headers with NO re-signing.
13
+ * No network, no key.
14
+ */
15
+ import {
16
+ decodeForestrieGrantCose,
17
+ findGrantLeafInMassif,
18
+ MissingIndexError,
19
+ } from "@forestrie/receipt-verify";
20
+ import { base64ToBytes } from "@forestrie/grant-builder";
21
+ import { completeGrantBase64 } from "./register-grant-complete.js";
22
+ import {
23
+ CreateReceiptFailure,
24
+ deriveCheckpointReceipt,
25
+ } from "./create-receipt-derive.js";
26
+
27
+ /** Where offline completion broke. */
28
+ export type CompleteGrantStage = "decode" | "locate" | "parse" | "derive";
29
+
30
+ /** Stable snake_case reasons — the `--json` error shape is a contract. */
31
+ export type CompleteGrantReason =
32
+ | "grant_decode_failed"
33
+ | "grant_leaf_not_found"
34
+ | "massif_missing_index"
35
+ | "idtimestamp_unresolved"
36
+ // Passed through from the receipt derivation (create-receipt taxonomy).
37
+ | "massif_parse_failed"
38
+ | "checkpoint_parse_failed"
39
+ | "checkpoint_missing_peak_receipts"
40
+ | "checkpoint_missing_sealed_size"
41
+ | "checkpoint_does_not_cover_leaf"
42
+ | "leaf_not_in_massif"
43
+ | "derive_failed";
44
+
45
+ export class CompleteGrantFailure extends Error {
46
+ readonly stage: CompleteGrantStage;
47
+ readonly reason: CompleteGrantReason;
48
+
49
+ constructor(
50
+ stage: CompleteGrantStage,
51
+ reason: CompleteGrantReason,
52
+ message: string,
53
+ ) {
54
+ super(message);
55
+ this.name = "CompleteGrantFailure";
56
+ this.stage = stage;
57
+ this.reason = reason;
58
+ }
59
+ }
60
+
61
+ /** Which input supplied the entry-id idtimestamp half. */
62
+ export type IdtimestampSource = "massif" | "override" | "grant";
63
+
64
+ export type DeriveCompletedGrantInput = {
65
+ /** Registered (uncompleted) grant, Forestrie-Grant header base64 (trimmed). */
66
+ grantBase64: string;
67
+ massifBytes: Uint8Array;
68
+ checkpointBytes: Uint8Array;
69
+ /** 8-byte idtimestamp override (`--idtimestamp`), used only as a fallback. */
70
+ idtimestampOverride?: Uint8Array | undefined;
71
+ };
72
+
73
+ export type CompletedGrant = {
74
+ /** Completed grant, Forestrie-Grant header base64. */
75
+ completedBase64: string;
76
+ /** Permanent entry id (`idtimestamp_be8 || mmrIndex_be8`), 32 hex chars. */
77
+ entryIdHex: string;
78
+ mmrIndex: bigint;
79
+ idtimestampBe8: Uint8Array;
80
+ idtimestampSource: IdtimestampSource;
81
+ receiptBytes: number;
82
+ proofLength: number;
83
+ peakIndex: number;
84
+ peakCount: number;
85
+ sealedSize: bigint;
86
+ certCopied: boolean;
87
+ };
88
+
89
+ function bytesToHex(bytes: Uint8Array): string {
90
+ let s = "";
91
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
92
+ return s;
93
+ }
94
+
95
+ function mmrIndexToBe8Hex(mmrIndex: bigint): string {
96
+ const out = new Uint8Array(8);
97
+ new DataView(out.buffer).setBigUint64(0, mmrIndex & 0xffffffffffffffffn, false);
98
+ return bytesToHex(out);
99
+ }
100
+
101
+ function isAllZero(bytes: Uint8Array): boolean {
102
+ for (const b of bytes) if (b !== 0) return false;
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * Resolve the idtimestamp that goes into the entry id. The massif leaf key is
108
+ * ground truth — it is exactly what the sequencer committed, so verification
109
+ * (`leafHash = H(idtimestamp || contentHash)`) only reproduces the anchored
110
+ * leaf when this value is used. `--idtimestamp` / an already-embedded grant
111
+ * idtimestamp are fallbacks for the (unusual) case where the massif index
112
+ * region did not record the key.
113
+ */
114
+ function resolveIdtimestamp(
115
+ fromMassif: Uint8Array,
116
+ override: Uint8Array | undefined,
117
+ fromGrant: Uint8Array,
118
+ ): { idtimestampBe8: Uint8Array; source: IdtimestampSource } {
119
+ if (!isAllZero(fromMassif)) {
120
+ return { idtimestampBe8: fromMassif, source: "massif" };
121
+ }
122
+ if (override !== undefined && !isAllZero(override)) {
123
+ return { idtimestampBe8: override, source: "override" };
124
+ }
125
+ if (!isAllZero(fromGrant)) {
126
+ return { idtimestampBe8: fromGrant, source: "grant" };
127
+ }
128
+ throw new CompleteGrantFailure(
129
+ "parse",
130
+ "idtimestamp_unresolved",
131
+ "could not resolve the leaf idtimestamp from the massif, --idtimestamp, " +
132
+ "or the grant — supply --idtimestamp",
133
+ );
134
+ }
135
+
136
+ /**
137
+ * Derive the completed grant from local artefacts. Throws
138
+ * {@link CompleteGrantFailure} with a stable stage + reason on any failure.
139
+ */
140
+ export async function deriveCompletedGrant(
141
+ input: DeriveCompletedGrantInput,
142
+ ): Promise<CompletedGrant> {
143
+ // 1. Decode the signed grant statement into its Grant payload (+ any
144
+ // already-embedded idtimestamp).
145
+ let grant: ReturnType<typeof decodeForestrieGrantCose>["grant"];
146
+ let grantIdtimestampBe8: Uint8Array;
147
+ try {
148
+ ({ grant, idtimestampBe8: grantIdtimestampBe8 } = decodeForestrieGrantCose(
149
+ base64ToBytes(input.grantBase64),
150
+ ));
151
+ } catch (err) {
152
+ throw new CompleteGrantFailure(
153
+ "decode",
154
+ "grant_decode_failed",
155
+ `not a Forestrie-Grant COSE Sign1: ${errorMessage(err)}`,
156
+ );
157
+ }
158
+
159
+ // 2. Locate the grant's leaf in the massif by its commitment hash — recovers
160
+ // both the mmrIndex and the sequenced idtimestamp, offline.
161
+ let located: Awaited<ReturnType<typeof findGrantLeafInMassif>>;
162
+ try {
163
+ located = await findGrantLeafInMassif(input.massifBytes, grant);
164
+ } catch (err) {
165
+ if (err instanceof MissingIndexError) {
166
+ throw new CompleteGrantFailure(
167
+ "locate",
168
+ "massif_missing_index",
169
+ `massif has no index region to search: ${err.message}`,
170
+ );
171
+ }
172
+ throw new CompleteGrantFailure("locate", "grant_leaf_not_found", errorMessage(err));
173
+ }
174
+ if (located === null) {
175
+ throw new CompleteGrantFailure(
176
+ "locate",
177
+ "grant_leaf_not_found",
178
+ "this grant's commitment hash is not a leaf in the supplied massif — " +
179
+ "wrong massif, or the grant is not sequenced yet",
180
+ );
181
+ }
182
+
183
+ const { idtimestampBe8, source } = resolveIdtimestamp(
184
+ located.idtimestampBe8,
185
+ input.idtimestampOverride,
186
+ grantIdtimestampBe8,
187
+ );
188
+
189
+ // 3. Rebuild the leaf's inclusion receipt against the checkpoint's pre-signed
190
+ // peak (identical derivation to `create-receipt`).
191
+ let derived: Awaited<ReturnType<typeof deriveCheckpointReceipt>>;
192
+ try {
193
+ derived = await deriveCheckpointReceipt({
194
+ massifBytes: input.massifBytes,
195
+ checkpointBytes: input.checkpointBytes,
196
+ mmrIndex: located.mmrIndex,
197
+ });
198
+ } catch (err) {
199
+ if (err instanceof CreateReceiptFailure) {
200
+ // Reuse the create-receipt reason taxonomy verbatim (it is a subset of
201
+ // CompleteGrantReason).
202
+ throw new CompleteGrantFailure(
203
+ err.stage === "parse" ? "parse" : "derive",
204
+ err.reason,
205
+ err.message,
206
+ );
207
+ }
208
+ throw new CompleteGrantFailure("derive", "derive_failed", errorMessage(err));
209
+ }
210
+
211
+ // 4. Merge receipt (396) + idtimestamp (−65537) into the grant's unprotected
212
+ // headers — no re-signing — and emit the completed grant base64.
213
+ const entryIdHex = bytesToHex(idtimestampBe8) + mmrIndexToBe8Hex(located.mmrIndex);
214
+ const completedBase64 = completeGrantBase64(
215
+ input.grantBase64,
216
+ derived.receiptCbor,
217
+ entryIdHex,
218
+ );
219
+
220
+ return {
221
+ completedBase64,
222
+ entryIdHex,
223
+ mmrIndex: located.mmrIndex,
224
+ idtimestampBe8,
225
+ idtimestampSource: source,
226
+ receiptBytes: derived.receiptCbor.length,
227
+ proofLength: derived.details.proofLength,
228
+ peakIndex: derived.details.peakIndex,
229
+ peakCount: derived.details.peakCount,
230
+ sealedSize: derived.details.sealedSize,
231
+ certCopied: derived.details.certCopied,
232
+ };
233
+ }
234
+
235
+ function errorMessage(err: unknown): string {
236
+ return err instanceof Error ? err.message : String(err);
237
+ }