@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.
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/TRUST-MODEL.md +8 -0
- package/dist/cli.js +33811 -0
- package/dist/decode-receipt.d.ts +19 -0
- package/dist/decode-receipt.d.ts.map +1 -0
- package/dist/decode-receipt.js +19 -0
- package/dist/decode-receipt.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/decode-receipt-cbor.d.ts +26 -0
- package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
- package/dist/lib/decode-receipt-cbor.js +182 -0
- package/dist/lib/decode-receipt-cbor.js.map +1 -0
- package/dist/lib/decode-receipt-decode.d.ts +103 -0
- package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
- package/dist/lib/decode-receipt-decode.js +237 -0
- package/dist/lib/decode-receipt-decode.js.map +1 -0
- package/dist/lib/decode-receipt-labels.d.ts +54 -0
- package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
- package/dist/lib/decode-receipt-labels.js +117 -0
- package/dist/lib/decode-receipt-labels.js.map +1 -0
- package/dist/lib/decode-receipt-render.d.ts +11 -0
- package/dist/lib/decode-receipt-render.d.ts.map +1 -0
- package/dist/lib/decode-receipt-render.js +242 -0
- package/dist/lib/decode-receipt-render.js.map +1 -0
- package/package.json +83 -0
- package/src/cli.ts +5 -0
- package/src/command.ts +51 -0
- package/src/commands/admin-onboard-token.ts +51 -0
- package/src/commands/admin.ts +20 -0
- package/src/commands/complete-grant.ts +41 -0
- package/src/commands/create-consistency-proof.ts +44 -0
- package/src/commands/create-log.ts +95 -0
- package/src/commands/decode-receipt.ts +20 -0
- package/src/commands/delegate.ts +55 -0
- package/src/commands/deploy.ts +72 -0
- package/src/commands/fetch-accumulator.ts +49 -0
- package/src/commands/onboard-genesis.ts +70 -0
- package/src/commands/onboard-request.ts +59 -0
- package/src/commands/register-grant.ts +74 -0
- package/src/commands/register.ts +54 -0
- package/src/commands/resolve-receipt.ts +101 -0
- package/src/commands/sign-statement.ts +53 -0
- package/src/commands/verify-grant.ts +89 -0
- package/src/commands/verify.ts +92 -0
- package/src/commoncli.ts +34 -0
- package/src/decode-receipt.ts +56 -0
- package/src/index.ts +10 -0
- package/src/lib/bytes.ts +7 -0
- package/src/lib/checkpoint-provider.ts +297 -0
- package/src/lib/complete-grant-derive.ts +237 -0
- package/src/lib/consistency-proof.ts +271 -0
- package/src/lib/create-log-build.ts +168 -0
- package/src/lib/create-log-prepare.ts +75 -0
- package/src/lib/create-receipt-chain-verify.ts +267 -0
- package/src/lib/create-receipt-derive.ts +208 -0
- package/src/lib/create-receipt-inputs.ts +79 -0
- package/src/lib/decode-checkpoint-calldata.ts +188 -0
- package/src/lib/decode-receipt-cbor.ts +202 -0
- package/src/lib/decode-receipt-decode.ts +333 -0
- package/src/lib/decode-receipt-labels.ts +132 -0
- package/src/lib/decode-receipt-render.ts +284 -0
- package/src/lib/delegate-flow.ts +289 -0
- package/src/lib/delegate-voucher.ts +122 -0
- package/src/lib/deploy-artifact.ts +79 -0
- package/src/lib/deploy-bootstrap.ts +100 -0
- package/src/lib/deploy-flow.ts +184 -0
- package/src/lib/deploy-genesis-log-id.ts +29 -0
- package/src/lib/deploy-rpc.ts +40 -0
- package/src/lib/deployment-record.ts +65 -0
- package/src/lib/fsio.ts +91 -0
- package/src/lib/grant-flags-local.ts +19 -0
- package/src/lib/onboard-attestation.ts +120 -0
- package/src/lib/onboard-genesis-body.ts +94 -0
- package/src/lib/openssl-error-queue.ts +56 -0
- package/src/lib/register-flow.ts +292 -0
- package/src/lib/register-grant-build.ts +173 -0
- package/src/lib/register-grant-complete.ts +47 -0
- package/src/lib/register-grant-flow.ts +258 -0
- package/src/lib/resolve-receipt-freshen.ts +221 -0
- package/src/lib/sign-statement-build.ts +117 -0
- package/src/lib/sign-statement-key.ts +145 -0
- package/src/lib/verify-anchored.ts +326 -0
- package/src/lib/verify-checkpoint-chain.ts +160 -0
- package/src/lib/verify-eventscan.ts +214 -0
- package/src/lib/verify-inputs.ts +143 -0
- package/src/lib/verify-known-accumulator.ts +161 -0
- package/src/lib/verify-report.ts +265 -0
- package/src/lib/x402-payment.ts +115 -0
- package/src/main/admin-onboard-token.ts +216 -0
- package/src/main/complete-grant.ts +265 -0
- package/src/main/create-consistency-proof.ts +83 -0
- package/src/main/create-log.ts +347 -0
- package/src/main/create-receipt.ts +763 -0
- package/src/main/decode-receipt.ts +72 -0
- package/src/main/delegate.ts +141 -0
- package/src/main/deploy.ts +273 -0
- package/src/main/fetch-accumulator.ts +181 -0
- package/src/main/not-implemented.ts +43 -0
- package/src/main/onboard-genesis.ts +192 -0
- package/src/main/onboard-request.ts +333 -0
- package/src/main/register-grant.ts +270 -0
- package/src/main/register.ts +243 -0
- package/src/main/sign-statement.ts +168 -0
- package/src/main/verify.ts +767 -0
- package/src/options/admin-onboard-token.ts +48 -0
- package/src/options/common.ts +69 -0
- package/src/options/complete-grant.ts +40 -0
- package/src/options/create-consistency-proof.ts +63 -0
- package/src/options/create-log.ts +99 -0
- package/src/options/create-receipt.ts +243 -0
- package/src/options/decode-receipt.ts +25 -0
- package/src/options/delegate.ts +93 -0
- package/src/options/deploy.ts +105 -0
- package/src/options/fetch-accumulator.ts +62 -0
- package/src/options/onboard-genesis.ts +74 -0
- package/src/options/onboard-request.ts +57 -0
- package/src/options/register-grant.ts +72 -0
- package/src/options/register.ts +71 -0
- package/src/options/sign-statement.ts +64 -0
- package/src/options/verify.ts +250 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
3
|
+
import type { CompleteGrantOptions } from "../options/complete-grant.js";
|
|
4
|
+
import {
|
|
5
|
+
CompleteGrantFailure,
|
|
6
|
+
deriveCompletedGrant,
|
|
7
|
+
type CompletedGrant,
|
|
8
|
+
type CompleteGrantReason,
|
|
9
|
+
type CompleteGrantStage,
|
|
10
|
+
} from "../lib/complete-grant-derive.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FOR-344: self-create the `Authorization: Forestrie-Grant` header content
|
|
14
|
+
* from a checkpoint (+ local massif) — grants are derivable from log data,
|
|
15
|
+
* not operator-issued. The offline twin of `register-grant`'s completion:
|
|
16
|
+
* locate the grant's leaf in the massif, rebuild its inclusion receipt against
|
|
17
|
+
* the checkpoint's pre-signed peak, and attach receipt + idtimestamp with no
|
|
18
|
+
* re-signing.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Where an operational error broke the run. */
|
|
22
|
+
export type CompleteGrantErrorStage = "input" | CompleteGrantStage;
|
|
23
|
+
|
|
24
|
+
/** `--json` operational-error shape — stable stage + reason tokens. */
|
|
25
|
+
export type CompleteGrantErrorReport = {
|
|
26
|
+
error:
|
|
27
|
+
| "complete_grant_input_failed"
|
|
28
|
+
| "complete_grant_decode_failed"
|
|
29
|
+
| "complete_grant_locate_failed"
|
|
30
|
+
| "complete_grant_parse_failed"
|
|
31
|
+
| "complete_grant_derive_failed";
|
|
32
|
+
command: "complete-grant";
|
|
33
|
+
stage: CompleteGrantErrorStage;
|
|
34
|
+
reason?: CompleteGrantReason;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Structured `--json` success report — the shape is a contract. */
|
|
39
|
+
export type CompleteGrantReport = {
|
|
40
|
+
command: "complete-grant";
|
|
41
|
+
status: "completed";
|
|
42
|
+
entryId: string;
|
|
43
|
+
mmrIndex: string;
|
|
44
|
+
idtimestamp: string;
|
|
45
|
+
idtimestampSource: CompletedGrant["idtimestampSource"];
|
|
46
|
+
proof: {
|
|
47
|
+
length: number;
|
|
48
|
+
peakIndex: number;
|
|
49
|
+
peakCount: number;
|
|
50
|
+
};
|
|
51
|
+
sealedSize: string;
|
|
52
|
+
certCopied: boolean;
|
|
53
|
+
receiptBytes: number;
|
|
54
|
+
/** Present when `--out-b64` was given. */
|
|
55
|
+
outB64?: string;
|
|
56
|
+
/** Completed grant base64 — only when not written to `--out-b64`. */
|
|
57
|
+
grantB64?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ERROR_CODES: Record<
|
|
61
|
+
CompleteGrantErrorStage,
|
|
62
|
+
CompleteGrantErrorReport["error"]
|
|
63
|
+
> = {
|
|
64
|
+
input: "complete_grant_input_failed",
|
|
65
|
+
decode: "complete_grant_decode_failed",
|
|
66
|
+
locate: "complete_grant_locate_failed",
|
|
67
|
+
parse: "complete_grant_parse_failed",
|
|
68
|
+
derive: "complete_grant_derive_failed",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const IDTIMESTAMP_HEX_RE = /^[0-9a-f]{16}$/i;
|
|
72
|
+
|
|
73
|
+
/** An operational error that carries the stage but no lib reason. */
|
|
74
|
+
class InputError extends Error {}
|
|
75
|
+
|
|
76
|
+
function readBytes(path: string, flag: string): Uint8Array {
|
|
77
|
+
try {
|
|
78
|
+
return new Uint8Array(readFileSync(path));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new InputError(
|
|
81
|
+
`cannot read ${flag} '${path}': ${err instanceof Error ? err.message : String(err)}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hexToBe8(hex: string): Uint8Array {
|
|
87
|
+
const out = new Uint8Array(8);
|
|
88
|
+
for (let i = 0; i < 8; i++) {
|
|
89
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `--idtimestamp <hex|path>` to 8 big-endian bytes: a bare 16-char hex
|
|
96
|
+
* value, or a file holding either the 8 raw bytes or a hex string.
|
|
97
|
+
*/
|
|
98
|
+
function parseIdtimestampOption(value: string): Uint8Array {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (IDTIMESTAMP_HEX_RE.test(trimmed)) return hexToBe8(trimmed);
|
|
101
|
+
let raw: Uint8Array;
|
|
102
|
+
try {
|
|
103
|
+
raw = new Uint8Array(readFileSync(value));
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new InputError(
|
|
106
|
+
`--idtimestamp is neither 16 hex chars nor a readable file '${value}': ${
|
|
107
|
+
err instanceof Error ? err.message : String(err)
|
|
108
|
+
}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (raw.length === 8) return raw;
|
|
112
|
+
const text = new TextDecoder().decode(raw).trim();
|
|
113
|
+
if (IDTIMESTAMP_HEX_RE.test(text)) return hexToBe8(text);
|
|
114
|
+
throw new InputError(
|
|
115
|
+
`--idtimestamp file '${value}' must be 8 raw bytes or 16 hex chars`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Structured envelope under `--json`; one clean line on stderr otherwise. */
|
|
120
|
+
function reportRunError(
|
|
121
|
+
out: Out,
|
|
122
|
+
options: CompleteGrantOptions,
|
|
123
|
+
stage: CompleteGrantErrorStage,
|
|
124
|
+
err: unknown,
|
|
125
|
+
): void {
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
const reason = err instanceof CompleteGrantFailure ? err.reason : undefined;
|
|
128
|
+
if (options.json) {
|
|
129
|
+
const report: CompleteGrantErrorReport = {
|
|
130
|
+
error: ERROR_CODES[stage],
|
|
131
|
+
command: "complete-grant",
|
|
132
|
+
stage,
|
|
133
|
+
...(reason !== undefined ? { reason } : {}),
|
|
134
|
+
message,
|
|
135
|
+
};
|
|
136
|
+
out.out(JSON.stringify(report, null, 2));
|
|
137
|
+
} else {
|
|
138
|
+
out.warn("forestrie complete-grant: %s: %s", stage, message);
|
|
139
|
+
}
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runCompleteGrant(
|
|
144
|
+
out: Out,
|
|
145
|
+
options: CompleteGrantOptions,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
// 1. Inputs. The massif is required — it is what carries the leaf we recover
|
|
148
|
+
// the mmrIndex and idtimestamp from.
|
|
149
|
+
let grantBase64: string;
|
|
150
|
+
let massifBytes: Uint8Array;
|
|
151
|
+
let checkpointBytes: Uint8Array;
|
|
152
|
+
let idtimestampOverride: Uint8Array | undefined;
|
|
153
|
+
try {
|
|
154
|
+
if (options.massif === undefined) {
|
|
155
|
+
throw new InputError(
|
|
156
|
+
"--massif is required: the grant's leaf (mmrIndex + idtimestamp) is " +
|
|
157
|
+
"recovered from the local massif blob",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
grantBase64 = readFileSync(options.grant, "utf8").trim();
|
|
161
|
+
massifBytes = readBytes(options.massif, "--massif");
|
|
162
|
+
checkpointBytes = readBytes(options.checkpoint, "--checkpoint");
|
|
163
|
+
idtimestampOverride =
|
|
164
|
+
options.idtimestamp !== undefined
|
|
165
|
+
? parseIdtimestampOption(options.idtimestamp)
|
|
166
|
+
: undefined;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err instanceof InputError) {
|
|
169
|
+
reportRunError(out, options, "input", err);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
reportRunError(
|
|
173
|
+
out,
|
|
174
|
+
options,
|
|
175
|
+
"input",
|
|
176
|
+
new InputError(
|
|
177
|
+
`cannot read --grant '${options.grant}': ${
|
|
178
|
+
err instanceof Error ? err.message : String(err)
|
|
179
|
+
}`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Derive the completed grant.
|
|
186
|
+
let completed: CompletedGrant;
|
|
187
|
+
try {
|
|
188
|
+
completed = await deriveCompletedGrant({
|
|
189
|
+
grantBase64,
|
|
190
|
+
massifBytes,
|
|
191
|
+
checkpointBytes,
|
|
192
|
+
idtimestampOverride,
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const stage = err instanceof CompleteGrantFailure ? err.stage : "derive";
|
|
196
|
+
reportRunError(out, options, stage, err);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 3. Emit.
|
|
201
|
+
try {
|
|
202
|
+
if (options.outB64 !== undefined) {
|
|
203
|
+
writeFileSync(options.outB64, completed.completedBase64);
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
reportRunError(out, options, "input", new InputError(
|
|
207
|
+
`cannot write --out-b64 '${options.outB64}': ${
|
|
208
|
+
err instanceof Error ? err.message : String(err)
|
|
209
|
+
}`,
|
|
210
|
+
));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const idtimestampHex = completed.entryIdHex.slice(0, 16);
|
|
215
|
+
if (options.json) {
|
|
216
|
+
const report: CompleteGrantReport = {
|
|
217
|
+
command: "complete-grant",
|
|
218
|
+
status: "completed",
|
|
219
|
+
entryId: completed.entryIdHex,
|
|
220
|
+
mmrIndex: completed.mmrIndex.toString(10),
|
|
221
|
+
idtimestamp: idtimestampHex,
|
|
222
|
+
idtimestampSource: completed.idtimestampSource,
|
|
223
|
+
proof: {
|
|
224
|
+
length: completed.proofLength,
|
|
225
|
+
peakIndex: completed.peakIndex,
|
|
226
|
+
peakCount: completed.peakCount,
|
|
227
|
+
},
|
|
228
|
+
sealedSize: completed.sealedSize.toString(10),
|
|
229
|
+
certCopied: completed.certCopied,
|
|
230
|
+
receiptBytes: completed.receiptBytes,
|
|
231
|
+
...(options.outB64 !== undefined
|
|
232
|
+
? { outB64: options.outB64 }
|
|
233
|
+
: { grantB64: completed.completedBase64 }),
|
|
234
|
+
};
|
|
235
|
+
out.out(JSON.stringify(report, null, 2));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Human mode: without --out-b64 the completed grant base64 is the pipeable
|
|
240
|
+
// product on stdout; the summary narrates on stderr (mirrors register-grant).
|
|
241
|
+
const emit = options.outB64 === undefined ? out.print : out.out;
|
|
242
|
+
emit("complete-grant: leaf — mmrIndex %s (recovered from massif)", completed.mmrIndex.toString(10));
|
|
243
|
+
emit(
|
|
244
|
+
"complete-grant: entry id — %s (idtimestamp from %s)",
|
|
245
|
+
completed.entryIdHex,
|
|
246
|
+
completed.idtimestampSource,
|
|
247
|
+
);
|
|
248
|
+
emit(
|
|
249
|
+
"complete-grant: proof — %d node(s) to peak %d/%d",
|
|
250
|
+
completed.proofLength,
|
|
251
|
+
completed.peakIndex + 1,
|
|
252
|
+
completed.peakCount,
|
|
253
|
+
);
|
|
254
|
+
emit(
|
|
255
|
+
"complete-grant: checkpoint — sealed size %s, delegation cert copied: %s",
|
|
256
|
+
completed.sealedSize.toString(10),
|
|
257
|
+
completed.certCopied ? "yes" : "no",
|
|
258
|
+
);
|
|
259
|
+
emit("complete-grant: receipt — %d bytes attached (header 396)", completed.receiptBytes);
|
|
260
|
+
if (options.outB64 !== undefined) {
|
|
261
|
+
emit("complete-grant: wrote completed grant (base64) to %s", options.outB64);
|
|
262
|
+
} else {
|
|
263
|
+
out.out("%s", completed.completedBase64);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
3
|
+
import type { CreateConsistencyProofOptions } from "../options/create-consistency-proof.js";
|
|
4
|
+
import {
|
|
5
|
+
buildConsistencyProofArtifact,
|
|
6
|
+
encodeConsistencyProofArtifact,
|
|
7
|
+
multiMassifNodeGetter,
|
|
8
|
+
} from "../lib/consistency-proof.js";
|
|
9
|
+
|
|
10
|
+
/** `--json` success shape on stdout — stable for demo scripting. */
|
|
11
|
+
export type CreateConsistencyProofReport = {
|
|
12
|
+
command: "create-consistency-proof";
|
|
13
|
+
fromSize: string;
|
|
14
|
+
toSize: string;
|
|
15
|
+
basePeakCount: number;
|
|
16
|
+
pathCount: number;
|
|
17
|
+
out: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CreateConsistencyProofErrorReport = {
|
|
21
|
+
error: "create_consistency_proof_failed";
|
|
22
|
+
command: "create-consistency-proof";
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the portable top-up artifact from massif tiles (FOR-368 Phase 3):
|
|
28
|
+
* base accumulator at `--from-size` plus one inclusion path per base peak
|
|
29
|
+
* proven at `--to-size`, self-verified before writing. Consumers verify
|
|
30
|
+
* tile-free with `verify --known-accumulator ... --consistency-proof`.
|
|
31
|
+
*/
|
|
32
|
+
export async function runCreateConsistencyProof(
|
|
33
|
+
out: Out,
|
|
34
|
+
options: CreateConsistencyProofOptions,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const blobs = options.massifs.map(
|
|
38
|
+
(f) => new Uint8Array(readFileSync(f)),
|
|
39
|
+
);
|
|
40
|
+
const artifact = await buildConsistencyProofArtifact({
|
|
41
|
+
get: multiMassifNodeGetter(blobs),
|
|
42
|
+
fromSize: options.fromSize,
|
|
43
|
+
toSize: options.toSize,
|
|
44
|
+
});
|
|
45
|
+
writeFileSync(options.out, encodeConsistencyProofArtifact(artifact));
|
|
46
|
+
|
|
47
|
+
if (options.json) {
|
|
48
|
+
const report: CreateConsistencyProofReport = {
|
|
49
|
+
command: "create-consistency-proof",
|
|
50
|
+
fromSize: artifact.fromSize.toString(),
|
|
51
|
+
toSize: artifact.toSize.toString(),
|
|
52
|
+
basePeakCount: artifact.accumulatorFrom.length,
|
|
53
|
+
pathCount: artifact.paths.length,
|
|
54
|
+
out: options.out,
|
|
55
|
+
};
|
|
56
|
+
out.out(JSON.stringify(report, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
out.print(
|
|
59
|
+
"create-consistency-proof: proved %s -> %s (%d base peaks; self-verified)",
|
|
60
|
+
artifact.fromSize.toString(),
|
|
61
|
+
artifact.toSize.toString(),
|
|
62
|
+
artifact.accumulatorFrom.length,
|
|
63
|
+
);
|
|
64
|
+
out.out(
|
|
65
|
+
`wrote consistency proof: ${options.out} (verify with --known-accumulator at size ${artifact.toSize} plus --consistency-proof)`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = 0;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
const report: CreateConsistencyProofErrorReport = {
|
|
73
|
+
error: "create_consistency_proof_failed",
|
|
74
|
+
command: "create-consistency-proof",
|
|
75
|
+
message,
|
|
76
|
+
};
|
|
77
|
+
out.out(JSON.stringify(report, null, 2));
|
|
78
|
+
} else {
|
|
79
|
+
out.warn("forestrie create-consistency-proof: %s", message);
|
|
80
|
+
}
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
2
|
+
import { readTextFile, writeOutputFile } from "../lib/fsio.js";
|
|
3
|
+
import type { CreateLogOptions } from "../options/create-log.js";
|
|
4
|
+
import {
|
|
5
|
+
RegisterGrantBuildError,
|
|
6
|
+
buildCreateLogGrant,
|
|
7
|
+
type BuiltCreateLogGrant,
|
|
8
|
+
} from "../lib/create-log-build.js";
|
|
9
|
+
import { completeGrantBase64 } from "../lib/register-grant-complete.js";
|
|
10
|
+
import {
|
|
11
|
+
PrepareLogError,
|
|
12
|
+
prepareChildLog,
|
|
13
|
+
} from "../lib/create-log-prepare.js";
|
|
14
|
+
import {
|
|
15
|
+
RegisterFlowError,
|
|
16
|
+
runRegisterGrantFlow,
|
|
17
|
+
type RegisterGrantFlowDeps,
|
|
18
|
+
type RegisterGrantFlowResult,
|
|
19
|
+
} from "../lib/register-grant-flow.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* FOR-390 / ADR-0052: create a log and set its owner (K(L)). Build the signed
|
|
23
|
+
* create grant (`@forestrie/grant-builder` ES256 profile) with
|
|
24
|
+
* `GF_CREATE|GF_EXTEND` flags, POST it to `/register/{bootstrap}/grants` with
|
|
25
|
+
* the parent grant as CBOR body evidence, follow the 303 to the receipt, then
|
|
26
|
+
* emit the COMPLETED grant. The flow is identical to register-grant's; only the
|
|
27
|
+
* grant shape and the reporter labels differ (newLog / owner).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** `--json` success shape. */
|
|
31
|
+
export type CreateLogReport = {
|
|
32
|
+
command: "create-log";
|
|
33
|
+
status: "receipt";
|
|
34
|
+
/** Parent/auth log the create grant was sequenced into. */
|
|
35
|
+
ownerLog: string;
|
|
36
|
+
/** The log that was created. */
|
|
37
|
+
newLog: string;
|
|
38
|
+
/** The new log owner (ES256 x||y, hex) committed as grantData. */
|
|
39
|
+
ownerHex: string;
|
|
40
|
+
entryId: string;
|
|
41
|
+
statusUrl: string;
|
|
42
|
+
receiptUrl: string;
|
|
43
|
+
receiptBytes: number;
|
|
44
|
+
/** Present when `--out-b64` was given. */
|
|
45
|
+
outB64?: string;
|
|
46
|
+
/** Completed grant base64 — only when not written to `--out-b64`. */
|
|
47
|
+
grantB64?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** `--json` failure shape (problem = CBOR problem-details passthrough). */
|
|
51
|
+
export type CreateLogErrorReport = {
|
|
52
|
+
error:
|
|
53
|
+
| "key_read_failed"
|
|
54
|
+
| "grant_build_failed"
|
|
55
|
+
| "registration_failed"
|
|
56
|
+
| "status_failed"
|
|
57
|
+
| "receipt_failed"
|
|
58
|
+
| "network_failed"
|
|
59
|
+
| "timeout";
|
|
60
|
+
command: "create-log";
|
|
61
|
+
message: string;
|
|
62
|
+
httpStatus?: number;
|
|
63
|
+
detail?: string;
|
|
64
|
+
problem?: Record<string, unknown>;
|
|
65
|
+
statusUrl?: string;
|
|
66
|
+
receiptUrl?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Test seam: effects injected by the create-log tests (real by default). */
|
|
70
|
+
export type CreateLogRunDeps = RegisterGrantFlowDeps;
|
|
71
|
+
|
|
72
|
+
const FLOW_ERROR_CODES = {
|
|
73
|
+
register: "registration_failed",
|
|
74
|
+
status: "status_failed",
|
|
75
|
+
receipt: "receipt_failed",
|
|
76
|
+
network: "network_failed",
|
|
77
|
+
timeout: "timeout",
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
async function readPemFile(path: string, flag: string): Promise<string> {
|
|
81
|
+
return readTextFile(path, `${flag} key file not found: ${path}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function reportError(
|
|
85
|
+
out: Out,
|
|
86
|
+
options: CreateLogOptions,
|
|
87
|
+
report: CreateLogErrorReport,
|
|
88
|
+
): void {
|
|
89
|
+
if (options.json) {
|
|
90
|
+
out.out(JSON.stringify(report, null, 2));
|
|
91
|
+
} else {
|
|
92
|
+
out.warn("forestrie create-log: %s", report.message);
|
|
93
|
+
if (report.detail !== undefined && !report.message.includes(report.detail)) {
|
|
94
|
+
out.warn(" detail: %s", report.detail);
|
|
95
|
+
}
|
|
96
|
+
if (report.problem !== undefined) {
|
|
97
|
+
out.log("problem details: %s", JSON.stringify(report.problem));
|
|
98
|
+
}
|
|
99
|
+
if (report.statusUrl !== undefined) {
|
|
100
|
+
out.warn(" statusUrl: %s", report.statusUrl);
|
|
101
|
+
}
|
|
102
|
+
if (report.receiptUrl !== undefined) {
|
|
103
|
+
out.warn(" receiptUrl: %s", report.receiptUrl);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function flowErrorReport(err: RegisterFlowError): CreateLogErrorReport {
|
|
110
|
+
const report: CreateLogErrorReport = {
|
|
111
|
+
error: FLOW_ERROR_CODES[err.stage],
|
|
112
|
+
command: "create-log",
|
|
113
|
+
message: err.message,
|
|
114
|
+
};
|
|
115
|
+
if (err.httpStatus !== undefined) report.httpStatus = err.httpStatus;
|
|
116
|
+
if (err.detail !== undefined) report.detail = err.detail;
|
|
117
|
+
if (err.problem !== undefined) report.problem = err.problem;
|
|
118
|
+
if (err.statusUrl !== undefined) report.statusUrl = err.statusUrl;
|
|
119
|
+
if (err.receiptUrl !== undefined) report.receiptUrl = err.receiptUrl;
|
|
120
|
+
return report;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function reportCompletedGrant(
|
|
124
|
+
out: Out,
|
|
125
|
+
options: CreateLogOptions,
|
|
126
|
+
built: BuiltCreateLogGrant,
|
|
127
|
+
result: RegisterGrantFlowResult,
|
|
128
|
+
completedB64: string,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const ownerHex = Buffer.from(built.grantData).toString("hex");
|
|
131
|
+
if (options.outB64 !== undefined) {
|
|
132
|
+
await writeOutputFile(options.outB64, completedB64);
|
|
133
|
+
}
|
|
134
|
+
if (options.json) {
|
|
135
|
+
const report: CreateLogReport = {
|
|
136
|
+
command: "create-log",
|
|
137
|
+
status: "receipt",
|
|
138
|
+
ownerLog: options.ownerLog,
|
|
139
|
+
newLog: options.newLog,
|
|
140
|
+
ownerHex,
|
|
141
|
+
entryId: result.entryIdHex,
|
|
142
|
+
statusUrl: result.statusUrl,
|
|
143
|
+
receiptUrl: result.receiptUrl,
|
|
144
|
+
receiptBytes: result.receipt.length,
|
|
145
|
+
};
|
|
146
|
+
if (options.outB64 !== undefined) {
|
|
147
|
+
report.outB64 = options.outB64;
|
|
148
|
+
} else {
|
|
149
|
+
report.grantB64 = completedB64;
|
|
150
|
+
}
|
|
151
|
+
out.out(JSON.stringify(report, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Human mode: without --out-b64 the completed grant base64 is the pipeable
|
|
155
|
+
// product on stdout; the summary narrates on stderr. With --out-b64 the
|
|
156
|
+
// summary is the stdout product (mirrors register / register-grant).
|
|
157
|
+
const emit = options.outB64 === undefined ? out.print : out.out;
|
|
158
|
+
emit("ownerLog: %s (grant leaf)", options.ownerLog);
|
|
159
|
+
emit("newLog: %s (created)", options.newLog);
|
|
160
|
+
emit("owner: %s", ownerHex);
|
|
161
|
+
emit("entryId: %s", result.entryIdHex);
|
|
162
|
+
emit("statusUrl: %s", result.statusUrl);
|
|
163
|
+
emit("receiptUrl: %s", result.receiptUrl);
|
|
164
|
+
if (options.outB64 !== undefined) {
|
|
165
|
+
emit("wrote completed grant (base64) to %s", options.outB64);
|
|
166
|
+
} else {
|
|
167
|
+
out.out("%s", completedB64);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function runCreateLog(
|
|
172
|
+
out: Out,
|
|
173
|
+
options: CreateLogOptions,
|
|
174
|
+
deps: CreateLogRunDeps = {},
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
// 1. Key material.
|
|
177
|
+
let signWithPem: string;
|
|
178
|
+
let signerPem: string | undefined;
|
|
179
|
+
try {
|
|
180
|
+
signWithPem = await readPemFile(options.signWith, "--sign-with");
|
|
181
|
+
signerPem =
|
|
182
|
+
options.signerPem !== undefined
|
|
183
|
+
? await readPemFile(options.signerPem, "--signer-pem")
|
|
184
|
+
: undefined;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
reportError(out, options, {
|
|
187
|
+
error: "key_read_failed",
|
|
188
|
+
command: "create-log",
|
|
189
|
+
message: err instanceof Error ? err.message : String(err),
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. Build + sign the create grant transparent statement.
|
|
195
|
+
let built: BuiltCreateLogGrant;
|
|
196
|
+
try {
|
|
197
|
+
built = buildCreateLogGrant({
|
|
198
|
+
newLog: options.newLog,
|
|
199
|
+
ownerLog: options.ownerLog,
|
|
200
|
+
signWithPem,
|
|
201
|
+
signerPem,
|
|
202
|
+
selfReferential: options.selfReferential,
|
|
203
|
+
authLog: options.authLog,
|
|
204
|
+
childPaymentRequired: options.childPaymentRequired,
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (err instanceof RegisterGrantBuildError) {
|
|
208
|
+
reportError(out, options, {
|
|
209
|
+
error: "grant_build_failed",
|
|
210
|
+
command: "create-log",
|
|
211
|
+
message: err.message,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2b. --prepare: pre-register the child public root with the coordinator under
|
|
219
|
+
// parent authority (ADR-0053), WITHOUT sequencing. Emits the create grant so a
|
|
220
|
+
// later plain `create-log` sequences it. Enables `delegate` before the log
|
|
221
|
+
// exists.
|
|
222
|
+
if (options.prepare) {
|
|
223
|
+
if (options.selfReferential) {
|
|
224
|
+
reportError(out, options, {
|
|
225
|
+
error: "grant_build_failed",
|
|
226
|
+
command: "create-log",
|
|
227
|
+
message:
|
|
228
|
+
"--prepare is for child logs; the root/bootstrap log is onboarded via genesis, not prepare",
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const prepared = await prepareChildLog(
|
|
234
|
+
{
|
|
235
|
+
baseUrl: options.baseUrl,
|
|
236
|
+
childLogId: options.newLog,
|
|
237
|
+
grantBase64: built.grantBase64,
|
|
238
|
+
},
|
|
239
|
+
{ fetchImpl: deps.fetchImpl },
|
|
240
|
+
);
|
|
241
|
+
if (options.outB64 !== undefined) {
|
|
242
|
+
await writeOutputFile(options.outB64, built.grantBase64);
|
|
243
|
+
}
|
|
244
|
+
if (options.json) {
|
|
245
|
+
const report = {
|
|
246
|
+
command: "create-log" as const,
|
|
247
|
+
status: "prepared" as const,
|
|
248
|
+
ownerLog: options.ownerLog,
|
|
249
|
+
newLog: options.newLog,
|
|
250
|
+
publicRoot: prepared.publicRoot,
|
|
251
|
+
...(prepared.webhook !== undefined
|
|
252
|
+
? { webhook: prepared.webhook }
|
|
253
|
+
: {}),
|
|
254
|
+
...(options.outB64 !== undefined ? { outB64: options.outB64 } : {}),
|
|
255
|
+
grantB64: options.outB64 === undefined ? built.grantBase64 : undefined,
|
|
256
|
+
};
|
|
257
|
+
out.out(JSON.stringify(report, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const emit = options.outB64 === undefined ? out.print : out.out;
|
|
261
|
+
emit("prepared child log %s (owner %s)", options.newLog, options.ownerLog);
|
|
262
|
+
emit(
|
|
263
|
+
"coordinator: public-root %s%s",
|
|
264
|
+
prepared.publicRoot,
|
|
265
|
+
prepared.webhook !== undefined ? `, webhook ${prepared.webhook}` : "",
|
|
266
|
+
);
|
|
267
|
+
emit("owner: %s", Buffer.from(built.grantData).toString("hex"));
|
|
268
|
+
if (options.outB64 !== undefined) {
|
|
269
|
+
emit("wrote create grant (base64) to %s", options.outB64);
|
|
270
|
+
} else {
|
|
271
|
+
out.out("%s", built.grantBase64);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
275
|
+
reportError(out, options, {
|
|
276
|
+
error: "registration_failed",
|
|
277
|
+
command: "create-log",
|
|
278
|
+
message,
|
|
279
|
+
...(err instanceof PrepareLogError && err.httpStatus !== undefined
|
|
280
|
+
? { httpStatus: err.httpStatus }
|
|
281
|
+
: {}),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const flowDeps: RegisterGrantFlowDeps = {
|
|
288
|
+
...deps,
|
|
289
|
+
onProgress:
|
|
290
|
+
deps.onProgress ??
|
|
291
|
+
((progress) => {
|
|
292
|
+
switch (progress.phase) {
|
|
293
|
+
case "registered":
|
|
294
|
+
out.print("create grant registered; status: %s", progress.statusUrl);
|
|
295
|
+
break;
|
|
296
|
+
case "status-pending":
|
|
297
|
+
out.log(
|
|
298
|
+
"sequencing pending (attempt %d): %s",
|
|
299
|
+
progress.attempt,
|
|
300
|
+
progress.statusUrl,
|
|
301
|
+
);
|
|
302
|
+
break;
|
|
303
|
+
case "receipt-located":
|
|
304
|
+
out.print(
|
|
305
|
+
"sequenced as entry %s; receipt: %s",
|
|
306
|
+
progress.entryIdHex,
|
|
307
|
+
progress.receiptUrl,
|
|
308
|
+
);
|
|
309
|
+
break;
|
|
310
|
+
case "receipt-pending":
|
|
311
|
+
out.log(
|
|
312
|
+
"receipt objects still writing (attempt %d): %s",
|
|
313
|
+
progress.attempt,
|
|
314
|
+
progress.receiptUrl,
|
|
315
|
+
);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// 3. Register the create grant, follow to the receipt, complete the grant.
|
|
322
|
+
try {
|
|
323
|
+
const result = await runRegisterGrantFlow(
|
|
324
|
+
{
|
|
325
|
+
baseUrl: options.baseUrl,
|
|
326
|
+
bootstrapLogId: options.bootstrapLog,
|
|
327
|
+
grantBase64: built.grantBase64,
|
|
328
|
+
parentGrantBase64: options.parentGrantB64,
|
|
329
|
+
timeoutMs: options.timeoutMs,
|
|
330
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
331
|
+
},
|
|
332
|
+
flowDeps,
|
|
333
|
+
);
|
|
334
|
+
const completedB64 = completeGrantBase64(
|
|
335
|
+
built.grantBase64,
|
|
336
|
+
result.receipt,
|
|
337
|
+
result.entryIdHex,
|
|
338
|
+
);
|
|
339
|
+
await reportCompletedGrant(out, options, built, result, completedB64);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (err instanceof RegisterFlowError) {
|
|
342
|
+
reportError(out, options, flowErrorReport(err));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|