@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,271 @@
|
|
|
1
|
+
import { bytesEqual } from "./bytes.js";
|
|
2
|
+
import {
|
|
3
|
+
decodeCborDeterministic,
|
|
4
|
+
encodeCborDeterministic,
|
|
5
|
+
} from "@forestrie/encoding";
|
|
6
|
+
import {
|
|
7
|
+
createSyncHasher,
|
|
8
|
+
indexConsistencyProof,
|
|
9
|
+
openMassifNodeStore,
|
|
10
|
+
peakMMRIndexes,
|
|
11
|
+
verifyConsistency,
|
|
12
|
+
type NodeGetter,
|
|
13
|
+
} from "@forestrie/merklelog";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Portable consistency-proof artifact (FOR-368 Phase 3, plan-2607-29):
|
|
17
|
+
* the "top-up" a holder of an old receipt obtains from ANY party with the
|
|
18
|
+
* public massif tiles — after which the holder verifies tile-free against
|
|
19
|
+
* a trusted newer state (`--known-accumulator`).
|
|
20
|
+
*
|
|
21
|
+
* Trust model: the artifact is UNSIGNED and untrusted. Soundness comes
|
|
22
|
+
* from recomputation — the receipt's peak must appear in the artifact's
|
|
23
|
+
* base accumulator, and folding the artifact's paths from that base
|
|
24
|
+
* (draft-bryce `consistent_roots`) must land every proven root inside the
|
|
25
|
+
* TRUSTED target accumulator. A fabricated artifact can only fail; a
|
|
26
|
+
* passing one proves the old state is a committed prefix of the trusted
|
|
27
|
+
* state (SCRAPI §2.4's Resolve Receipt provision, made third-party).
|
|
28
|
+
*
|
|
29
|
+
* Encoding is strict RFC 8949 §4.2 deterministic CBOR (hard policy).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** CBOR map labels for the artifact (strict RFC 8949 §4.2). */
|
|
33
|
+
const LABEL_VERSION = 1;
|
|
34
|
+
const LABEL_FROM_SIZE = 2;
|
|
35
|
+
const LABEL_TO_SIZE = 3;
|
|
36
|
+
const LABEL_ACCUMULATOR_FROM = 4;
|
|
37
|
+
const LABEL_PATHS = 5;
|
|
38
|
+
|
|
39
|
+
const ARTIFACT_VERSION = 1;
|
|
40
|
+
|
|
41
|
+
export type ConsistencyProofArtifact = {
|
|
42
|
+
version: number;
|
|
43
|
+
/** MMR size the proof extends FROM (the old receipt's era). */
|
|
44
|
+
fromSize: bigint;
|
|
45
|
+
/** MMR size the proof lands on — must equal the trusted state's size. */
|
|
46
|
+
toSize: bigint;
|
|
47
|
+
/** Full accumulator at `fromSize` (untrusted; proven by the fold). */
|
|
48
|
+
accumulatorFrom: Uint8Array[];
|
|
49
|
+
/** One inclusion path per `fromSize` peak, proven at `toSize`. */
|
|
50
|
+
paths: Uint8Array[][];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function encodeConsistencyProofArtifact(
|
|
54
|
+
artifact: ConsistencyProofArtifact,
|
|
55
|
+
): Uint8Array {
|
|
56
|
+
return encodeCborDeterministic(
|
|
57
|
+
new Map<number, unknown>([
|
|
58
|
+
[LABEL_VERSION, artifact.version],
|
|
59
|
+
[LABEL_FROM_SIZE, artifact.fromSize],
|
|
60
|
+
[LABEL_TO_SIZE, artifact.toSize],
|
|
61
|
+
[LABEL_ACCUMULATOR_FROM, artifact.accumulatorFrom],
|
|
62
|
+
[LABEL_PATHS, artifact.paths],
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function asBigint(v: unknown, what: string): bigint {
|
|
68
|
+
if (typeof v === "bigint") return v;
|
|
69
|
+
if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0)
|
|
70
|
+
return BigInt(v);
|
|
71
|
+
throw new Error(`consistency proof: ${what} must be an unsigned integer`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Strict decode + structural validation of a top-up artifact. */
|
|
75
|
+
export function decodeConsistencyProofArtifact(
|
|
76
|
+
bytes: Uint8Array,
|
|
77
|
+
): ConsistencyProofArtifact {
|
|
78
|
+
let decoded: unknown;
|
|
79
|
+
try {
|
|
80
|
+
decoded = decodeCborDeterministic(bytes);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
throw new Error(`consistency proof is not canonical CBOR: ${message}`);
|
|
84
|
+
}
|
|
85
|
+
if (!(decoded instanceof Map)) {
|
|
86
|
+
throw new Error("consistency proof must be a CBOR map");
|
|
87
|
+
}
|
|
88
|
+
const version = Number(asBigint(decoded.get(LABEL_VERSION), "version"));
|
|
89
|
+
if (version !== ARTIFACT_VERSION) {
|
|
90
|
+
throw new Error(`consistency proof version ${version} not supported`);
|
|
91
|
+
}
|
|
92
|
+
const fromSize = asBigint(decoded.get(LABEL_FROM_SIZE), "fromSize");
|
|
93
|
+
const toSize = asBigint(decoded.get(LABEL_TO_SIZE), "toSize");
|
|
94
|
+
if (fromSize === 0n || toSize <= fromSize) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`consistency proof sizes invalid: from ${fromSize} to ${toSize}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const accRaw = decoded.get(LABEL_ACCUMULATOR_FROM);
|
|
100
|
+
const pathsRaw = decoded.get(LABEL_PATHS);
|
|
101
|
+
if (
|
|
102
|
+
!Array.isArray(accRaw) ||
|
|
103
|
+
accRaw.some((p) => !(p instanceof Uint8Array) || p.length !== 32)
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"consistency proof: accumulatorFrom must be 32-byte peaks",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
!Array.isArray(pathsRaw) ||
|
|
111
|
+
pathsRaw.some(
|
|
112
|
+
(p) => !Array.isArray(p) || p.some((n) => !(n instanceof Uint8Array)),
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"consistency proof: paths must be arrays of byte strings",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const wanted = peakMMRIndexes(fromSize - 1n).length;
|
|
120
|
+
if (accRaw.length !== wanted || pathsRaw.length !== wanted) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`consistency proof: size ${fromSize} has ${wanted} peaks; got ${accRaw.length} accumulator values and ${pathsRaw.length} paths`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
version,
|
|
127
|
+
fromSize,
|
|
128
|
+
toSize,
|
|
129
|
+
accumulatorFrom: accRaw as Uint8Array[],
|
|
130
|
+
paths: pathsRaw as Uint8Array[][],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Node access across one or more massif blobs (a proof spanning massifs
|
|
136
|
+
* needs nodes from each). First blob covering the index wins; ancestor
|
|
137
|
+
* peak-stack reads resolve through whichever blob carries them.
|
|
138
|
+
*/
|
|
139
|
+
export function multiMassifNodeGetter(blobs: Uint8Array[]): NodeGetter {
|
|
140
|
+
const stores = blobs.map((b) => openMassifNodeStore(b));
|
|
141
|
+
return (i: bigint) => {
|
|
142
|
+
let lastErr: unknown;
|
|
143
|
+
for (const store of stores) {
|
|
144
|
+
try {
|
|
145
|
+
return store.get(i);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
lastErr = err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new Error(
|
|
151
|
+
`node ${i} is not covered by the supplied massifs: ${
|
|
152
|
+
lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
153
|
+
}`,
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build (and self-verify) a top-up artifact from massif node data. The
|
|
160
|
+
* builder needs tiles; consumers of the artifact never do.
|
|
161
|
+
*/
|
|
162
|
+
export async function buildConsistencyProofArtifact(opts: {
|
|
163
|
+
get: NodeGetter;
|
|
164
|
+
fromSize: bigint;
|
|
165
|
+
toSize: bigint;
|
|
166
|
+
}): Promise<ConsistencyProofArtifact> {
|
|
167
|
+
if (opts.fromSize === 0n || opts.toSize <= opts.fromSize) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`--from-size must be > 0 and < --to-size (got ${opts.fromSize} -> ${opts.toSize})`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const accumulatorFrom = peakMMRIndexes(opts.fromSize - 1n).map(opts.get);
|
|
173
|
+
const proof = indexConsistencyProof(
|
|
174
|
+
opts.get,
|
|
175
|
+
opts.fromSize - 1n,
|
|
176
|
+
opts.toSize - 1n,
|
|
177
|
+
);
|
|
178
|
+
// Self-check before emitting: the artifact must verify against the
|
|
179
|
+
// accumulator the same nodes derive at toSize — a coverage gap in the
|
|
180
|
+
// supplied massifs fails HERE, not at the holder.
|
|
181
|
+
const accumulatorTo = peakMMRIndexes(opts.toSize - 1n).map(opts.get);
|
|
182
|
+
const hasher = await createSyncHasher();
|
|
183
|
+
const check = await verifyConsistency(
|
|
184
|
+
hasher,
|
|
185
|
+
proof,
|
|
186
|
+
accumulatorFrom,
|
|
187
|
+
accumulatorTo,
|
|
188
|
+
);
|
|
189
|
+
if (!check.ok) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"self-verification failed: supplied massifs do not consistently cover the requested range",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
version: ARTIFACT_VERSION,
|
|
196
|
+
fromSize: opts.fromSize,
|
|
197
|
+
toSize: opts.toSize,
|
|
198
|
+
accumulatorFrom,
|
|
199
|
+
paths: proof.paths,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type ConsistencyProofAnchorCheck = {
|
|
204
|
+
anchored: boolean;
|
|
205
|
+
/** Index of the receipt peak within the artifact's base accumulator. */
|
|
206
|
+
matchedPeak: number | null;
|
|
207
|
+
reason?: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Tile-free extension of an old receipt to a trusted newer accumulator:
|
|
212
|
+
* peak ∈ artifact base, then fold the artifact into the trusted target.
|
|
213
|
+
*/
|
|
214
|
+
export async function checkReceiptAnchoredViaConsistencyProof(opts: {
|
|
215
|
+
artifact: ConsistencyProofArtifact;
|
|
216
|
+
recomputedPeak: Uint8Array;
|
|
217
|
+
leafMmrIndex: bigint;
|
|
218
|
+
/** Trusted target state (e.g. a known-accumulator snapshot). */
|
|
219
|
+
trustedSize: bigint;
|
|
220
|
+
trustedAccumulator: Uint8Array[];
|
|
221
|
+
}): Promise<ConsistencyProofAnchorCheck> {
|
|
222
|
+
const { artifact } = opts;
|
|
223
|
+
if (artifact.toSize !== opts.trustedSize) {
|
|
224
|
+
return {
|
|
225
|
+
anchored: false,
|
|
226
|
+
matchedPeak: null,
|
|
227
|
+
reason: "consistency_proof_target_mismatch",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (opts.leafMmrIndex >= artifact.fromSize) {
|
|
231
|
+
return {
|
|
232
|
+
anchored: false,
|
|
233
|
+
matchedPeak: null,
|
|
234
|
+
reason: "receipt_newer_than_consistency_proof_base",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
let matchedPeak: number | null = null;
|
|
238
|
+
for (let i = 0; i < artifact.accumulatorFrom.length; i++) {
|
|
239
|
+
if (bytesEqual(opts.recomputedPeak, artifact.accumulatorFrom[i]!)) {
|
|
240
|
+
matchedPeak = i;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (matchedPeak === null) {
|
|
245
|
+
return {
|
|
246
|
+
anchored: false,
|
|
247
|
+
matchedPeak: null,
|
|
248
|
+
reason: "peak_not_in_consistency_proof_base",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const hasher = await createSyncHasher();
|
|
252
|
+
const check = await verifyConsistency(
|
|
253
|
+
hasher,
|
|
254
|
+
{
|
|
255
|
+
mmrSizeA: artifact.fromSize,
|
|
256
|
+
mmrSizeB: artifact.toSize,
|
|
257
|
+
paths: artifact.paths,
|
|
258
|
+
},
|
|
259
|
+
artifact.accumulatorFrom,
|
|
260
|
+
opts.trustedAccumulator,
|
|
261
|
+
);
|
|
262
|
+
if (!check.ok) {
|
|
263
|
+
return {
|
|
264
|
+
anchored: false,
|
|
265
|
+
matchedPeak: null,
|
|
266
|
+
reason: "consistency_proof_invalid",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { anchored: true, matchedPeak };
|
|
270
|
+
}
|
|
271
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FOR-390 / ADR-0052 create-log grant assembly: build the grant that CREATES
|
|
3
|
+
* a log and sets its owner (K(L)). Lifted out of the old register-grant-build
|
|
4
|
+
* (plan-2607-21) — register-grant is now writer-only.
|
|
5
|
+
*
|
|
6
|
+
* Three shapes, all `GF_CREATE|GF_EXTEND` in byte 3 (grants.md §5):
|
|
7
|
+
*
|
|
8
|
+
* - `--self-referential` — root bootstrap leaf (`logId == ownerLogId`); the
|
|
9
|
+
* grant is its own authority, so grantData IS the envelope signer and
|
|
10
|
+
* `--signer-pem` is forbidden. Auth-log shaped (`authLogBootstrapShapedFlags`).
|
|
11
|
+
* - `--auth-log` — create a child auth log; grantData = `--signer-pem` (the new
|
|
12
|
+
* log's owner). Auth-log shaped (`authLogBootstrapShapedFlags`).
|
|
13
|
+
* - default — create a data log; grantData = `--signer-pem`. Data-log shaped
|
|
14
|
+
* (`dataLogCreateExtendFlags`).
|
|
15
|
+
*
|
|
16
|
+
* Auth-shaped grants may additionally carry
|
|
17
|
+
* `GF_DERIVED | GF_CHILD_PAYMENT_REQUIRED` (`--child-payment-required`,
|
|
18
|
+
* adr-0062/plan-2608-09): child grants registered under the new log's
|
|
19
|
+
* authority are then x402 payment-gated when a lane enables admission.
|
|
20
|
+
*
|
|
21
|
+
* grantData is the new log owner's ES256 `x||y`. Reuses the register-grant
|
|
22
|
+
* ES256 PEM parser and error class. Node-only (node:crypto PEM handling); no
|
|
23
|
+
* HTTP, no env.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
authLogBootstrapShapedFlags,
|
|
27
|
+
bytesToForestrieGrantBase64,
|
|
28
|
+
dataLogCreateExtendFlags,
|
|
29
|
+
signGrantPayloadWithEs256Pem,
|
|
30
|
+
withChildPaymentRequired,
|
|
31
|
+
} from "@forestrie/grant-builder";
|
|
32
|
+
import {
|
|
33
|
+
encodeGrantPayloadV0Canonical,
|
|
34
|
+
uuidToBytes,
|
|
35
|
+
type Grant,
|
|
36
|
+
} from "@forestrie/encoding";
|
|
37
|
+
import {
|
|
38
|
+
RegisterGrantBuildError,
|
|
39
|
+
es256PublicKeyXyFromPem,
|
|
40
|
+
} from "./register-grant-build.js";
|
|
41
|
+
|
|
42
|
+
export { RegisterGrantBuildError };
|
|
43
|
+
|
|
44
|
+
export type BuildCreateLogGrantParams = {
|
|
45
|
+
/** The log being created (`grant.logId`, UUID). */
|
|
46
|
+
newLog: string;
|
|
47
|
+
/** Parent/auth log the create grant is sequenced into (`grant.ownerLogId`, UUID). */
|
|
48
|
+
ownerLog: string;
|
|
49
|
+
/** PKCS#8/SEC1 ES256 PEM that signs the grant envelope (the granting authority, K(L)). */
|
|
50
|
+
signWithPem: string;
|
|
51
|
+
/**
|
|
52
|
+
* PEM of the new log's owner (`grantData` = ES256 x||y). Required unless
|
|
53
|
+
* `--self-referential` (which binds the `--sign-with` key itself).
|
|
54
|
+
*/
|
|
55
|
+
signerPem?: string | undefined;
|
|
56
|
+
/** Root bootstrap: first leaf of the root log, `logId == ownerLogId`. */
|
|
57
|
+
selfReferential: boolean;
|
|
58
|
+
/** Create a child auth log (auth-log flag class) rather than a data log. */
|
|
59
|
+
authLog: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Set `GF_DERIVED | GF_CHILD_PAYMENT_REQUIRED` (univocity bits 34/35) on the
|
|
62
|
+
* creation grant, declaring that child grants registered under this log's
|
|
63
|
+
* authority require an x402 payment (adr-0062, plan-2608-09). Auth-shaped
|
|
64
|
+
* grants only — a data log is never the parent authority of a child grant.
|
|
65
|
+
*/
|
|
66
|
+
childPaymentRequired?: boolean | undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type BuiltCreateLogGrant = {
|
|
70
|
+
/** Signed grant transparent statement, Forestrie-Grant header base64. */
|
|
71
|
+
grantBase64: string;
|
|
72
|
+
/** Canonical grant v0 payload CBOR (keys 1-6) that was signed. */
|
|
73
|
+
grantPayloadBytes: Uint8Array;
|
|
74
|
+
/** The new log owner this grant commits (ES256 x||y, 64 bytes). */
|
|
75
|
+
grantData: Uint8Array;
|
|
76
|
+
/** 8-byte GF_* flags bitmap that went on the wire. */
|
|
77
|
+
flags: Uint8Array;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build and sign the create-log grant transparent statement: canonical grant
|
|
82
|
+
* v0 CBOR (`encodeGrantPayloadV0Canonical`) with `GF_CREATE|GF_EXTEND` flags,
|
|
83
|
+
* signed via the Custodian ES256 PEM profile (`signGrantPayloadWithEs256Pem`),
|
|
84
|
+
* returned as `Authorization: Forestrie-Grant` base64.
|
|
85
|
+
*/
|
|
86
|
+
export function buildCreateLogGrant(
|
|
87
|
+
params: BuildCreateLogGrantParams,
|
|
88
|
+
): BuiltCreateLogGrant {
|
|
89
|
+
let logId: Uint8Array;
|
|
90
|
+
let ownerLogId: Uint8Array;
|
|
91
|
+
try {
|
|
92
|
+
logId = uuidToBytes(params.newLog);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new RegisterGrantBuildError(`--new-log is not a UUID: ${params.newLog}`);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
ownerLogId = uuidToBytes(params.ownerLog);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new RegisterGrantBuildError(
|
|
100
|
+
`--owner-log is not a UUID: ${params.ownerLog}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (params.selfReferential) {
|
|
105
|
+
// Root bootstrap shape: the grant is its own authority (first leaf of the
|
|
106
|
+
// root log), so the new log IS the owner and grantData IS the envelope
|
|
107
|
+
// signer (arbor's root-grant check compares them).
|
|
108
|
+
if (params.newLog.toLowerCase() !== params.ownerLog.toLowerCase()) {
|
|
109
|
+
throw new RegisterGrantBuildError(
|
|
110
|
+
"--self-referential requires --new-log and --owner-log to be the same log (logId == ownerLogId)",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (params.signerPem !== undefined) {
|
|
114
|
+
throw new RegisterGrantBuildError(
|
|
115
|
+
"--self-referential grants bind the --sign-with key itself; omit --signer-pem",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} else if (params.signerPem === undefined) {
|
|
119
|
+
throw new RegisterGrantBuildError(
|
|
120
|
+
"--signer-pem is required (the new log's owner) unless --self-referential",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The new log owner's public key is the committed grantData; the
|
|
125
|
+
// self-referential bootstrap binds the granting key itself.
|
|
126
|
+
const grantData = es256PublicKeyXyFromPem(
|
|
127
|
+
params.signerPem ?? params.signWithPem,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
let flags =
|
|
131
|
+
params.authLog || params.selfReferential
|
|
132
|
+
? authLogBootstrapShapedFlags()
|
|
133
|
+
: dataLogCreateExtendFlags();
|
|
134
|
+
if (params.childPaymentRequired === true) {
|
|
135
|
+
if (!params.authLog && !params.selfReferential) {
|
|
136
|
+
throw new RegisterGrantBuildError(
|
|
137
|
+
"--child-payment-required applies to auth logs (--auth-log or --self-referential); a data log is never the parent authority of a child grant",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
flags = withChildPaymentRequired(flags);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const grant: Grant = {
|
|
144
|
+
logId,
|
|
145
|
+
ownerLogId,
|
|
146
|
+
grant: flags,
|
|
147
|
+
maxHeight: 0,
|
|
148
|
+
minGrowth: 0,
|
|
149
|
+
grantData,
|
|
150
|
+
};
|
|
151
|
+
const grantPayloadBytes = encodeGrantPayloadV0Canonical(grant);
|
|
152
|
+
let sign1: Uint8Array;
|
|
153
|
+
try {
|
|
154
|
+
sign1 = signGrantPayloadWithEs256Pem(grantPayloadBytes, params.signWithPem);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new RegisterGrantBuildError(
|
|
157
|
+
`--sign-with key rejected: ${
|
|
158
|
+
err instanceof Error ? err.message : String(err)
|
|
159
|
+
}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
grantBase64: bytesToForestrieGrantBase64(sign1),
|
|
164
|
+
grantPayloadBytes,
|
|
165
|
+
grantData,
|
|
166
|
+
flags,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FOR-390 / ADR-0053: pre-register a child log's public root with the delegation
|
|
3
|
+
* coordinator under PARENT authority, WITHOUT sequencing the create leaf.
|
|
4
|
+
*
|
|
5
|
+
* The parent-signed create grant travels as the `Authorization: Forestrie-Grant`
|
|
6
|
+
* credential; canopy-api verifies it against the parent log's registered root key
|
|
7
|
+
* and forwards `{childLogId -> owner key}` to the coordinator. This lets an owner
|
|
8
|
+
* pre-sign an advance delegation (`forestrie delegate`) for a child logId BEFORE
|
|
9
|
+
* the log exists — no operator onboard token, authority flows down the log
|
|
10
|
+
* hierarchy (ADR-0053). Node/Bun HTTP only.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Prepare (child public-root pre-registration) failure. */
|
|
14
|
+
export class PrepareLogError extends Error {
|
|
15
|
+
readonly httpStatus?: number;
|
|
16
|
+
constructor(message: string, httpStatus?: number) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "PrepareLogError";
|
|
19
|
+
if (httpStatus !== undefined) this.httpStatus = httpStatus;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Coordinator forward outcome (best-effort parse of the prepare response). */
|
|
24
|
+
export type PrepareResult = {
|
|
25
|
+
/** "ok" when the child public root was registered with the coordinator. */
|
|
26
|
+
publicRoot: string;
|
|
27
|
+
/** "ok" | "skipped" | "error" when a signing-route webhook was set. */
|
|
28
|
+
webhook?: string | undefined;
|
|
29
|
+
detail?: string | undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type PrepareDeps = { fetchImpl?: typeof fetch | undefined };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* POST the parent-signed create grant to
|
|
36
|
+
* `POST /api/forest/{childLogId}/prepare` (Authorization: Forestrie-Grant), which
|
|
37
|
+
* registers the child's public root with the coordinator under parent authority.
|
|
38
|
+
* Returns the coordinator forward status; throws {@link PrepareLogError} on non-2xx.
|
|
39
|
+
*/
|
|
40
|
+
export async function prepareChildLog(
|
|
41
|
+
params: { baseUrl: string; childLogId: string; grantBase64: string },
|
|
42
|
+
deps: PrepareDeps = {},
|
|
43
|
+
): Promise<PrepareResult> {
|
|
44
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
45
|
+
const url = `${params.baseUrl.replace(/\/$/, "")}/api/forest/${params.childLogId}/prepare`;
|
|
46
|
+
const res = await fetchImpl(url, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { Authorization: `Forestrie-Grant ${params.grantBase64}` },
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const preview = (await res.text().catch(() => "")).slice(0, 300);
|
|
52
|
+
throw new PrepareLogError(
|
|
53
|
+
`prepare failed: HTTP ${res.status} ${preview}`,
|
|
54
|
+
res.status,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
// Lenient: the endpoint returns the coordinator forward status (JSON or CBOR).
|
|
58
|
+
// A 2xx alone means the public root was registered; extract fields when the
|
|
59
|
+
// body is JSON-parseable, else report a generic success.
|
|
60
|
+
let publicRoot = "ok";
|
|
61
|
+
let webhook: string | undefined;
|
|
62
|
+
let detail: string | undefined;
|
|
63
|
+
try {
|
|
64
|
+
const body = (await res.json()) as Record<string, unknown> & {
|
|
65
|
+
coordinator?: Record<string, unknown>;
|
|
66
|
+
};
|
|
67
|
+
const src = (body.coordinator ?? body) as Record<string, unknown>;
|
|
68
|
+
if (typeof src.publicRoot === "string") publicRoot = src.publicRoot;
|
|
69
|
+
if (typeof src.webhook === "string") webhook = src.webhook;
|
|
70
|
+
if (typeof src.detail === "string") detail = src.detail;
|
|
71
|
+
} catch {
|
|
72
|
+
// non-JSON (e.g. CBOR) — 2xx is success.
|
|
73
|
+
}
|
|
74
|
+
return { publicRoot, webhook, detail };
|
|
75
|
+
}
|