@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,48 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
optionalStringOption,
|
|
4
|
+
parseForestrieCommonOptions,
|
|
5
|
+
requiredStringOption,
|
|
6
|
+
type ForestrieCommonOptions,
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
|
|
9
|
+
/** Default onboard-token label when `--label` is absent. */
|
|
10
|
+
export const DEFAULT_ONBOARD_TOKEN_LABEL = "forestrie-cli";
|
|
11
|
+
|
|
12
|
+
/** `forestrie admin onboard-token` — FOR-406 (plan-2607-27 W1). */
|
|
13
|
+
export type AdminOnboardTokenOptions = ForestrieCommonOptions & {
|
|
14
|
+
/** SCRAPI origin, no trailing slash (`FORESTRIE_BASE_URL`). */
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
/**
|
|
17
|
+
* Operator credential (`CANOPY_OPS_ADMIN_TOKEN`). The `admin` family is
|
|
18
|
+
* the operator-credential surface (ADR-0052); participant commands never
|
|
19
|
+
* read this secret.
|
|
20
|
+
*/
|
|
21
|
+
opsToken: string;
|
|
22
|
+
/** Token label recorded by the mint endpoint. */
|
|
23
|
+
label: string;
|
|
24
|
+
/**
|
|
25
|
+
* Chain binding (mandatory on every token since ADR-0059 D7): either read
|
|
26
|
+
* from a `forestrie deploy --out` artifact, or given explicitly.
|
|
27
|
+
*/
|
|
28
|
+
deployment: string | undefined;
|
|
29
|
+
chainId: string | undefined;
|
|
30
|
+
univocityAddr: string | undefined;
|
|
31
|
+
/** Write the minted token here instead of stdout. */
|
|
32
|
+
out: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function parseAdminOnboardTokenOptions(
|
|
36
|
+
args: LooseParsedArgs,
|
|
37
|
+
): AdminOnboardTokenOptions {
|
|
38
|
+
return {
|
|
39
|
+
...parseForestrieCommonOptions(args),
|
|
40
|
+
baseUrl: requiredStringOption(args, "base-url", "FORESTRIE_BASE_URL"),
|
|
41
|
+
opsToken: requiredStringOption(args, "ops-token", "CANOPY_OPS_ADMIN_TOKEN"),
|
|
42
|
+
label: optionalStringOption(args, "label") ?? DEFAULT_ONBOARD_TOKEN_LABEL,
|
|
43
|
+
deployment: optionalStringOption(args, "deployment"),
|
|
44
|
+
chainId: optionalStringOption(args, "chain-id"),
|
|
45
|
+
univocityAddr: optionalStringOption(args, "univocity"),
|
|
46
|
+
out: optionalStringOption(args, "out"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readEvaluatedStringOption,
|
|
3
|
+
resolveVerbosity,
|
|
4
|
+
type LooseParsedArgs,
|
|
5
|
+
} from "@forestrie/cli-kit";
|
|
6
|
+
|
|
7
|
+
/** Options shared by every `forestrie` command (after parsing). */
|
|
8
|
+
export type ForestrieCommonOptions = {
|
|
9
|
+
/** Emit machine-readable JSON on stdout instead of human output. */
|
|
10
|
+
json: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Config file path, if provided (`--config` / `FORESTRIE_CONFIG`).
|
|
13
|
+
* Stub: recorded at parse time; loading lands with the implementations.
|
|
14
|
+
*/
|
|
15
|
+
config?: string;
|
|
16
|
+
/** Resolved output verbosity. */
|
|
17
|
+
verbosity: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Resolve the tool-wide common options at parse time. */
|
|
21
|
+
export function parseForestrieCommonOptions(
|
|
22
|
+
args: LooseParsedArgs,
|
|
23
|
+
): ForestrieCommonOptions {
|
|
24
|
+
const config =
|
|
25
|
+
readEvaluatedStringOption(args, "config") ??
|
|
26
|
+
process.env["FORESTRIE_CONFIG"] ??
|
|
27
|
+
undefined;
|
|
28
|
+
const options: ForestrieCommonOptions = {
|
|
29
|
+
json: args["json"] === true,
|
|
30
|
+
verbosity: resolveVerbosity(args),
|
|
31
|
+
};
|
|
32
|
+
if (config !== undefined && config !== "") {
|
|
33
|
+
options.config = config;
|
|
34
|
+
}
|
|
35
|
+
return options;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read a string option (kebab or camelCase, `${env:…}` templates
|
|
40
|
+
* evaluated), falling back to `envVar` when the flag is absent.
|
|
41
|
+
*/
|
|
42
|
+
export function optionalStringOption(
|
|
43
|
+
args: LooseParsedArgs,
|
|
44
|
+
name: string,
|
|
45
|
+
envVar?: string,
|
|
46
|
+
): string | undefined {
|
|
47
|
+
const value =
|
|
48
|
+
readEvaluatedStringOption(args, name) ??
|
|
49
|
+
(envVar ? process.env[envVar] : undefined);
|
|
50
|
+
return value === "" ? undefined : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* As `optionalStringOption`, but a missing value is a usage error.
|
|
55
|
+
* Options with an env fallback are enforced here rather than with
|
|
56
|
+
* citty `required: true` (which cannot see the environment).
|
|
57
|
+
*/
|
|
58
|
+
export function requiredStringOption(
|
|
59
|
+
args: LooseParsedArgs,
|
|
60
|
+
name: string,
|
|
61
|
+
envVar?: string,
|
|
62
|
+
): string {
|
|
63
|
+
const value = optionalStringOption(args, name, envVar);
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
const hint = envVar ? ` (or set ${envVar})` : "";
|
|
66
|
+
throw new Error(`missing required option --${name}${hint}`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
optionalStringOption,
|
|
4
|
+
parseForestrieCommonOptions,
|
|
5
|
+
requiredStringOption,
|
|
6
|
+
type ForestrieCommonOptions,
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `forestrie complete-grant` — FOR-344.
|
|
11
|
+
*
|
|
12
|
+
* Self-create the `Authorization: Forestrie-Grant` header content from a
|
|
13
|
+
* checkpoint (and possibly an idtimestamp) — grants are derivable from
|
|
14
|
+
* log data, not operator-issued.
|
|
15
|
+
*/
|
|
16
|
+
export type CompleteGrantOptions = ForestrieCommonOptions & {
|
|
17
|
+
/** Registered (uncompleted) grant, base64 file. */
|
|
18
|
+
grant: string;
|
|
19
|
+
/** Checkpoint (.sth) for the owner log. */
|
|
20
|
+
checkpoint: string;
|
|
21
|
+
/** Massif .log blob for local leaf/path recovery. */
|
|
22
|
+
massif: string | undefined;
|
|
23
|
+
/** Leaf idtimestamp (hex or be8 file) when the grant lacks one. */
|
|
24
|
+
idtimestamp: string | undefined;
|
|
25
|
+
/** Completed grant base64 output path (default: stdout). */
|
|
26
|
+
outB64: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function parseCompleteGrantOptions(
|
|
30
|
+
args: LooseParsedArgs,
|
|
31
|
+
): CompleteGrantOptions {
|
|
32
|
+
return {
|
|
33
|
+
...parseForestrieCommonOptions(args),
|
|
34
|
+
grant: requiredStringOption(args, "grant"),
|
|
35
|
+
checkpoint: requiredStringOption(args, "checkpoint"),
|
|
36
|
+
massif: optionalStringOption(args, "massif"),
|
|
37
|
+
idtimestamp: optionalStringOption(args, "idtimestamp"),
|
|
38
|
+
outB64: optionalStringOption(args, "out-b64"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
parseForestrieCommonOptions,
|
|
4
|
+
requiredStringOption,
|
|
5
|
+
type ForestrieCommonOptions,
|
|
6
|
+
} from "./common.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `forestrie create-consistency-proof` — FOR-368 Phase 3 (plan-2607-29).
|
|
10
|
+
*
|
|
11
|
+
* Build a portable, UNSIGNED top-up artifact from public massif tiles: one
|
|
12
|
+
* inclusion path per `--from-size` peak, proven at `--to-size`, plus the
|
|
13
|
+
* base accumulator. Any party with the tiles can produce it (SCRAPI §2.4
|
|
14
|
+
* Resolve Receipt, made third-party); the holder then verifies an old
|
|
15
|
+
* receipt tile-free with `verify --known-accumulator ... --consistency-proof`.
|
|
16
|
+
* Soundness never depends on who built it — the artifact can only fail
|
|
17
|
+
* against the holder's trusted snapshot, never mint trust.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type CreateConsistencyProofOptions = ForestrieCommonOptions & {
|
|
21
|
+
/** Massif blob paths, comma-separated (a spanning proof needs each). */
|
|
22
|
+
massifs: string[];
|
|
23
|
+
fromSize: bigint;
|
|
24
|
+
toSize: bigint;
|
|
25
|
+
out: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function requiredSize(args: LooseParsedArgs, name: string): bigint {
|
|
29
|
+
const raw = requiredStringOption(args, name);
|
|
30
|
+
let value: bigint;
|
|
31
|
+
try {
|
|
32
|
+
value = BigInt(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`--${name} must be an MMR size (integer)`);
|
|
35
|
+
}
|
|
36
|
+
if (value <= 0n) {
|
|
37
|
+
throw new Error(`--${name} must be a positive MMR size`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseCreateConsistencyProofOptions(
|
|
43
|
+
args: LooseParsedArgs,
|
|
44
|
+
): CreateConsistencyProofOptions {
|
|
45
|
+
const massifs = requiredStringOption(args, "massif")
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((f) => f.trim())
|
|
48
|
+
.filter((f) => f !== "");
|
|
49
|
+
if (massifs.length === 0) {
|
|
50
|
+
throw new Error("--massif requires at least one massif blob path");
|
|
51
|
+
}
|
|
52
|
+
const options: CreateConsistencyProofOptions = {
|
|
53
|
+
...parseForestrieCommonOptions(args),
|
|
54
|
+
massifs,
|
|
55
|
+
fromSize: requiredSize(args, "from-size"),
|
|
56
|
+
toSize: requiredSize(args, "to-size"),
|
|
57
|
+
out: requiredStringOption(args, "out"),
|
|
58
|
+
};
|
|
59
|
+
if (options.toSize <= options.fromSize) {
|
|
60
|
+
throw new Error("--to-size must be greater than --from-size");
|
|
61
|
+
}
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
optionalStringOption,
|
|
4
|
+
parseForestrieCommonOptions,
|
|
5
|
+
requiredStringOption,
|
|
6
|
+
type ForestrieCommonOptions,
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_POLL_INTERVAL_SECONDS,
|
|
10
|
+
DEFAULT_TIMEOUT_SECONDS,
|
|
11
|
+
durationOptionMs,
|
|
12
|
+
} from "./register.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `forestrie create-log` — FOR-390 / ADR-0052.
|
|
16
|
+
*
|
|
17
|
+
* Create a log and set its owner (K(L)). The create grant is sequenced into
|
|
18
|
+
* the parent/auth log (`--owner-log`); `--new-log` is the log being created.
|
|
19
|
+
* Absorbs the create / self-referential / auth-log shapes removed from
|
|
20
|
+
* `register-grant` (which is now writer-only).
|
|
21
|
+
*/
|
|
22
|
+
export type CreateLogOptions = ForestrieCommonOptions & {
|
|
23
|
+
/** SCRAPI origin (`FORESTRIE_BASE_URL`). */
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
/** Parent/auth log the create grant is sequenced into. */
|
|
26
|
+
ownerLog: string;
|
|
27
|
+
/** The log being created. */
|
|
28
|
+
newLog: string;
|
|
29
|
+
/** Create a child auth log rather than a data log. */
|
|
30
|
+
authLog: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Mark the creation grant `GF_DERIVED | GF_CHILD_PAYMENT_REQUIRED`
|
|
33
|
+
* (adr-0062): child grants registered under the new log's authority require
|
|
34
|
+
* an x402 payment. Auth-shaped grants only.
|
|
35
|
+
*/
|
|
36
|
+
childPaymentRequired: boolean;
|
|
37
|
+
/** Root bootstrap self-referential grant (`--new-log == --owner-log`). */
|
|
38
|
+
selfReferential: boolean;
|
|
39
|
+
/** PEM of the new log's owner (grantData = ES256 x||y); required unless self-referential. */
|
|
40
|
+
signerPem: string | undefined;
|
|
41
|
+
/** Granting authority PEM (the parent log's K(L)) that signs the grant. */
|
|
42
|
+
signWith: string;
|
|
43
|
+
/**
|
|
44
|
+
* Pre-register the child log's public root with the delegation coordinator
|
|
45
|
+
* (parent-authorized, ADR-0053) WITHOUT sequencing the create leaf, so the
|
|
46
|
+
* owner can `delegate` (pre-sign) before the log exists. Emits the create
|
|
47
|
+
* grant to `--out-b64` for the later real `create-log`.
|
|
48
|
+
*/
|
|
49
|
+
prepare: boolean;
|
|
50
|
+
/** Parent grant credential authorizing this registration. */
|
|
51
|
+
parentGrantB64: string | undefined;
|
|
52
|
+
/** Completed grant base64 output path (default: stdout). */
|
|
53
|
+
outB64: string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Forest bootstrap/root log id — first `/register/` path segment (grant
|
|
56
|
+
* genesis lookup is keyed by the forest root). Defaults to `--owner-log`.
|
|
57
|
+
*/
|
|
58
|
+
bootstrapLog: string;
|
|
59
|
+
/** Overall receipt wait budget, milliseconds (`--timeout` seconds). */
|
|
60
|
+
timeoutMs: number;
|
|
61
|
+
/** Poll pacing, milliseconds (`--poll-interval` seconds). */
|
|
62
|
+
pollIntervalMs: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function parseCreateLogOptions(args: LooseParsedArgs): CreateLogOptions {
|
|
66
|
+
const ownerLog = requiredStringOption(args, "owner-log");
|
|
67
|
+
const options: CreateLogOptions = {
|
|
68
|
+
...parseForestrieCommonOptions(args),
|
|
69
|
+
baseUrl: requiredStringOption(args, "base-url", "FORESTRIE_BASE_URL"),
|
|
70
|
+
ownerLog,
|
|
71
|
+
newLog: requiredStringOption(args, "new-log"),
|
|
72
|
+
authLog: args["auth-log"] === true,
|
|
73
|
+
childPaymentRequired: args["child-payment-required"] === true,
|
|
74
|
+
selfReferential: args["self-referential"] === true,
|
|
75
|
+
signerPem: optionalStringOption(args, "signer-pem"),
|
|
76
|
+
signWith: requiredStringOption(args, "sign-with"),
|
|
77
|
+
prepare: args["prepare"] === true,
|
|
78
|
+
parentGrantB64: optionalStringOption(args, "parent-grant-b64"),
|
|
79
|
+
outB64: optionalStringOption(args, "out-b64"),
|
|
80
|
+
bootstrapLog: optionalStringOption(args, "bootstrap-log") ?? ownerLog,
|
|
81
|
+
timeoutMs: durationOptionMs(args, "timeout", DEFAULT_TIMEOUT_SECONDS),
|
|
82
|
+
pollIntervalMs: durationOptionMs(
|
|
83
|
+
args,
|
|
84
|
+
"poll-interval",
|
|
85
|
+
DEFAULT_POLL_INTERVAL_SECONDS,
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
if (options.selfReferential && options.parentGrantB64 !== undefined) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"--self-referential and --parent-grant-b64 are mutually exclusive (the bootstrap leaf has no parent)",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!options.selfReferential && options.parentGrantB64 === undefined) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"either --parent-grant-b64 or --self-referential is required",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
optionalStringOption,
|
|
4
|
+
parseForestrieCommonOptions,
|
|
5
|
+
requiredStringOption,
|
|
6
|
+
type ForestrieCommonOptions,
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `forestrie resolve-receipt` (alias `create-receipt`) — FOR-345 / FOR-418.
|
|
11
|
+
*
|
|
12
|
+
* ONE receipt producer; the source is chosen by the flags present, not a
|
|
13
|
+
* `--mode` (plan-2607-32 D3). Three sources, mutually exclusive:
|
|
14
|
+
*
|
|
15
|
+
* - **tiles** (`--massif`): rebuild the leaf→peak path from a massif blob and
|
|
16
|
+
* either attach it to a checkpoint's pre-signed peak receipt (`--checkpoint`,
|
|
17
|
+
* offline) or verify the computed peak against the on-chain accumulator
|
|
18
|
+
* (`--univocity` + `--log-id` + `--rpc-url`, report-only). Leaf addressing:
|
|
19
|
+
* exactly one of `--mmr-index` / `--entry-id`.
|
|
20
|
+
* - **`.sth` chain** (`--checkpoint-chain`): tile-free FRESHEN of a stale
|
|
21
|
+
* `--receipt` — fold the retained checkpoint chain, extend the receipt's old
|
|
22
|
+
* path to the current peak, re-emit under the chain's own tail checkpoint.
|
|
23
|
+
* - **calldata** (`--rpc-url`/`--univocity`/`--log-id` + `--checkpoint`):
|
|
24
|
+
* tile-free FRESHEN reading the climb material from the on-chain
|
|
25
|
+
* `publishCheckpoint` calldata; emission borrows the separately-supplied
|
|
26
|
+
* latest `--checkpoint` (calldata carries no per-peak receipts).
|
|
27
|
+
*
|
|
28
|
+
* Both freshen sources re-anchor to the current accumulator; the trust posture
|
|
29
|
+
* (freshness vs signer provenance) is chosen at verify — see the receipt trust model:
|
|
30
|
+
* https://github.com/forestrie/protocol/blob/main/spec/receipt-trust-model.md
|
|
31
|
+
* Freshen recomputes the leaf value exactly as `verify` does, from `--payload`
|
|
32
|
+
* (statement receipts) or the committed grant (`--committed-grant` /
|
|
33
|
+
* `--committed-grant-file`) + `--entry-id`.
|
|
34
|
+
*
|
|
35
|
+
* Multiple sources → error ("choose one source"); an under-specified source →
|
|
36
|
+
* error naming what's missing. No implicit pick — every ambiguous invocation
|
|
37
|
+
* fails closed with guidance.
|
|
38
|
+
*/
|
|
39
|
+
export type CreateReceiptOptions = ForestrieCommonOptions & {
|
|
40
|
+
anchor: "checkpoint" | "chain" | "freshen-sth" | "freshen-calldata";
|
|
41
|
+
/** Tiles source: massif .log blob holding the leaf and its proof nodes. */
|
|
42
|
+
massif: string | undefined;
|
|
43
|
+
/** MMR index of the leaf to prove (`--mmr-index` addressing, tiles source). */
|
|
44
|
+
mmrIndex: bigint | undefined;
|
|
45
|
+
/** Permanent SCRAPI entry id (leaf addressing / freshen idtimestamp). */
|
|
46
|
+
entryId: string | undefined;
|
|
47
|
+
/** Checkpoint (.sth) with pre-signed peak receipts (tiles + `--checkpoint`). */
|
|
48
|
+
checkpoint: string | undefined;
|
|
49
|
+
/** ImutableUnivocity contract address (tiles + chain-anchored mode). */
|
|
50
|
+
univocity: string | undefined;
|
|
51
|
+
/** Log id for the on-chain accumulator read (chain mode). */
|
|
52
|
+
logId: string | undefined;
|
|
53
|
+
/** JSON-RPC endpoint (`RPC_URL`, chain mode). */
|
|
54
|
+
rpcUrl: string | undefined;
|
|
55
|
+
/** Stale receipt to freshen (`.sth`-chain / calldata source). */
|
|
56
|
+
receipt: string | undefined;
|
|
57
|
+
/** Retained `.sth` checkpoint chain (dir or comma-separated files). */
|
|
58
|
+
checkpointChain: string | undefined;
|
|
59
|
+
/** Committed grant, base64 (freshen leaf-value source, grant receipts). */
|
|
60
|
+
committedGrant: string | undefined;
|
|
61
|
+
/** Committed grant CBOR file (freshen leaf-value source, grant receipts). */
|
|
62
|
+
committedGrantFile: string | undefined;
|
|
63
|
+
/** Registered statement payload (freshen leaf-value source, statement
|
|
64
|
+
* receipts) — the leaf ContentHash is `SHA-256(payload)`, as in `verify`. */
|
|
65
|
+
payload: string | undefined;
|
|
66
|
+
/** Receipt output path (default: stdout). */
|
|
67
|
+
out: string | undefined;
|
|
68
|
+
/** Rewrite the `--receipt` file in place with the freshened receipt (freshen
|
|
69
|
+
* only; mutually exclusive with `--out`). */
|
|
70
|
+
inPlace: boolean;
|
|
71
|
+
/** Trusted accumulator snapshot (`fetch-accumulator` output) to bind the
|
|
72
|
+
* freshened state against — anchors freshness (not signer provenance), fails
|
|
73
|
+
* closed on mismatch; freshen only. See the receipt trust model:
|
|
74
|
+
* https://github.com/forestrie/protocol/blob/main/spec/receipt-trust-model.md */
|
|
75
|
+
knownAccumulator: string | undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function parseMMRIndex(raw: string): bigint {
|
|
79
|
+
if (!/^\d+$/.test(raw)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`invalid --mmr-index '${raw}' (expected a non-negative integer)`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return BigInt(raw);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function parseCreateReceiptOptions(
|
|
88
|
+
args: LooseParsedArgs,
|
|
89
|
+
): CreateReceiptOptions {
|
|
90
|
+
const massif = optionalStringOption(args, "massif");
|
|
91
|
+
const receipt = optionalStringOption(args, "receipt");
|
|
92
|
+
const checkpointChain = optionalStringOption(args, "checkpoint-chain");
|
|
93
|
+
const checkpoint = optionalStringOption(args, "checkpoint");
|
|
94
|
+
const univocity = optionalStringOption(args, "univocity");
|
|
95
|
+
const logId = optionalStringOption(args, "log-id");
|
|
96
|
+
const rpcUrl = optionalStringOption(args, "rpc-url", "RPC_URL");
|
|
97
|
+
const committedGrant = optionalStringOption(args, "committed-grant");
|
|
98
|
+
const committedGrantFile = optionalStringOption(args, "committed-grant-file");
|
|
99
|
+
const payload = optionalStringOption(args, "payload");
|
|
100
|
+
const entryId = optionalStringOption(args, "entry-id");
|
|
101
|
+
const out = optionalStringOption(args, "out");
|
|
102
|
+
const inPlace = args["in-place"] === true;
|
|
103
|
+
const knownAccumulator = optionalStringOption(args, "known-accumulator");
|
|
104
|
+
const common = parseForestrieCommonOptions(args);
|
|
105
|
+
|
|
106
|
+
const base = {
|
|
107
|
+
massif,
|
|
108
|
+
checkpoint,
|
|
109
|
+
univocity,
|
|
110
|
+
logId,
|
|
111
|
+
rpcUrl,
|
|
112
|
+
receipt,
|
|
113
|
+
checkpointChain,
|
|
114
|
+
committedGrant,
|
|
115
|
+
committedGrantFile,
|
|
116
|
+
payload,
|
|
117
|
+
entryId,
|
|
118
|
+
out,
|
|
119
|
+
inPlace,
|
|
120
|
+
knownAccumulator,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (inPlace && out !== undefined) {
|
|
124
|
+
throw new Error("choose --out or --in-place, not both");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Source selection is exclusive: tiles (--massif) vs freshen (--receipt).
|
|
128
|
+
if (massif !== undefined && receipt !== undefined) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"choose one source: --massif (build from tiles) or --receipt (freshen a stale receipt) — not both",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (inPlace && receipt === undefined) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"--in-place only applies to freshen (--receipt) — there is no receipt file to rewrite",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (knownAccumulator !== undefined && receipt === undefined) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"--known-accumulator only applies to freshen (--receipt)",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- FRESHEN source (--receipt + a tile-free chain) ---
|
|
145
|
+
if (receipt !== undefined) {
|
|
146
|
+
// Leaf value: a statement payload (--payload, as in `verify`) or a committed
|
|
147
|
+
// grant (--committed-grant/-file, as in `verify-grant`) — exactly one.
|
|
148
|
+
const hasGrant =
|
|
149
|
+
committedGrant !== undefined || committedGrantFile !== undefined;
|
|
150
|
+
const hasPayload = payload !== undefined;
|
|
151
|
+
if (hasGrant && hasPayload) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"choose one leaf source: --payload (statement) or --committed-grant/--committed-grant-file (grant)",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!hasGrant && !hasPayload) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"freshen needs the leaf's content: --payload (statement) or --committed-grant/--committed-grant-file (grant)",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (hasPayload && entryId === undefined) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"freshen from --payload needs the SCRAPI entry id (idtimestamp): --entry-id",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (committedGrantFile !== undefined && entryId === undefined) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"freshen from a raw grant file needs the SCRAPI entry id (idtimestamp): --entry-id",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const hasSth = checkpointChain !== undefined;
|
|
172
|
+
const hasCalldata =
|
|
173
|
+
univocity !== undefined || rpcUrl !== undefined || logId !== undefined;
|
|
174
|
+
if (hasSth && hasCalldata) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"choose one freshen source: --checkpoint-chain (.sth) or --rpc-url/--univocity/--log-id (calldata)",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (hasSth) {
|
|
180
|
+
return { ...common, ...base, anchor: "freshen-sth", mmrIndex: undefined };
|
|
181
|
+
}
|
|
182
|
+
if (hasCalldata) {
|
|
183
|
+
if (univocity === undefined || logId === undefined || rpcUrl === undefined) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"calldata freshen requires --univocity, --log-id and --rpc-url",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (checkpoint === undefined) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"calldata freshen requires --checkpoint (the latest .sth) for emission — calldata carries no pre-signed peak receipts",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
...common,
|
|
195
|
+
...base,
|
|
196
|
+
anchor: "freshen-calldata",
|
|
197
|
+
mmrIndex: undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
throw new Error(
|
|
201
|
+
"freshen (--receipt) needs a tile-free source: --checkpoint-chain (.sth) or --rpc-url/--univocity/--log-id (calldata)",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- TILES source (--massif) ---
|
|
206
|
+
if (massif === undefined) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"a source is required: --massif (build from tiles) or --receipt + --checkpoint-chain (freshen)",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const rawMMRIndex = optionalStringOption(args, "mmr-index");
|
|
212
|
+
if ((rawMMRIndex === undefined) === (entryId === undefined)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"exactly one of --mmr-index or --entry-id is required to address the leaf",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const mmrIndex =
|
|
218
|
+
rawMMRIndex !== undefined ? parseMMRIndex(rawMMRIndex) : undefined;
|
|
219
|
+
|
|
220
|
+
let anchor: "checkpoint" | "chain";
|
|
221
|
+
if (checkpoint !== undefined && univocity === undefined) {
|
|
222
|
+
anchor = "checkpoint";
|
|
223
|
+
} else if (checkpoint === undefined && univocity !== undefined) {
|
|
224
|
+
if (logId === undefined || rpcUrl === undefined) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
"chain-anchored mode requires --univocity, --log-id and --rpc-url",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
anchor = "chain";
|
|
230
|
+
} else {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"exactly one of --checkpoint (offline) or --univocity (chain-anchored) is required",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
...common,
|
|
238
|
+
...base,
|
|
239
|
+
anchor,
|
|
240
|
+
massif: requiredStringOption(args, "massif"),
|
|
241
|
+
mmrIndex,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
parseForestrieCommonOptions,
|
|
4
|
+
type ForestrieCommonOptions,
|
|
5
|
+
} from "./common.js";
|
|
6
|
+
|
|
7
|
+
/** `forestrie decode-receipt` — FOR-346. */
|
|
8
|
+
export type DecodeReceiptOptions = ForestrieCommonOptions & {
|
|
9
|
+
/**
|
|
10
|
+
* COSE receipt file to decode (Sign1 + MMR inclusion).
|
|
11
|
+
* Omitted or `-`: read the receipt CBOR from stdin.
|
|
12
|
+
*/
|
|
13
|
+
receipt?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function parseDecodeReceiptOptions(
|
|
17
|
+
args: LooseParsedArgs,
|
|
18
|
+
): DecodeReceiptOptions {
|
|
19
|
+
const options: DecodeReceiptOptions = parseForestrieCommonOptions(args);
|
|
20
|
+
const receipt = typeof args["receipt"] === "string" ? args["receipt"] : "";
|
|
21
|
+
if (receipt !== "") {
|
|
22
|
+
options.receipt = receipt;
|
|
23
|
+
}
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LooseParsedArgs } from "@forestrie/cli-kit";
|
|
2
|
+
import {
|
|
3
|
+
optionalStringOption,
|
|
4
|
+
parseForestrieCommonOptions,
|
|
5
|
+
requiredStringOption,
|
|
6
|
+
type ForestrieCommonOptions,
|
|
7
|
+
} from "./common.js";
|
|
8
|
+
|
|
9
|
+
/** Default horizon MMR end — effectively unbounded (Number.MAX_SAFE_INTEGER). */
|
|
10
|
+
export const DEFAULT_HORIZON_MMR_END = Number.MAX_SAFE_INTEGER;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `forestrie delegate` — FOR-390 / ADR-0052.
|
|
14
|
+
*
|
|
15
|
+
* Authorize a custodian-vouched sealer to publish checkpoints for a log the
|
|
16
|
+
* caller owns (K(L)). Public coordinator endpoints only — no operator token,
|
|
17
|
+
* no RPC. The registrar voucher is verified against a caller-known
|
|
18
|
+
* registrar key before the delegation certificate is bound (fail closed).
|
|
19
|
+
*/
|
|
20
|
+
export type DelegateOptions = ForestrieCommonOptions & {
|
|
21
|
+
/** Delegation coordinator origin (`DELEGATION_COORDINATOR_URL`). */
|
|
22
|
+
coordinatorUrl: string;
|
|
23
|
+
/** Target log id (UUID). */
|
|
24
|
+
logId: string;
|
|
25
|
+
/** ES256 log-root PEM (K(L)) that authorizes the delegation. */
|
|
26
|
+
signWith: string;
|
|
27
|
+
/** The known key that vouches for the operator's sealer — the registrar's
|
|
28
|
+
* voucher-signing key, base64 `x||y` (`KNOWN_SEALER_KEY`). */
|
|
29
|
+
knownSealerKey: string;
|
|
30
|
+
/** Exclusive MMR end of the horizon lease (mmrStart is fixed 0). */
|
|
31
|
+
horizonMmrEnd: number;
|
|
32
|
+
/** Lease TTL seconds; defaults to the standing entry's suggestedTtlSeconds. */
|
|
33
|
+
ttlSeconds: number | undefined;
|
|
34
|
+
/** When set, write the submitted certificate base64 to this path. */
|
|
35
|
+
outB64: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Read `--<name>` as a finite number; a missing flag yields `fallback`. */
|
|
39
|
+
function numberOption(
|
|
40
|
+
args: LooseParsedArgs,
|
|
41
|
+
name: string,
|
|
42
|
+
fallback: number | undefined,
|
|
43
|
+
): number | undefined {
|
|
44
|
+
const raw = optionalStringOption(args, name);
|
|
45
|
+
if (raw === undefined) return fallback;
|
|
46
|
+
const value = Number(raw);
|
|
47
|
+
if (!Number.isFinite(value)) {
|
|
48
|
+
throw new Error(`--${name} must be a number`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseDelegateOptions(args: LooseParsedArgs): DelegateOptions {
|
|
54
|
+
const horizonMmrEnd = numberOption(
|
|
55
|
+
args,
|
|
56
|
+
"horizon-mmr-end",
|
|
57
|
+
DEFAULT_HORIZON_MMR_END,
|
|
58
|
+
) as number;
|
|
59
|
+
// The horizon binds a durable on-chain delegation scope [0, mmrEnd) — reject
|
|
60
|
+
// non-integer / out-of-range values before they flow into cert + on-chain
|
|
61
|
+
// encoding (ADR-0050 V4). MMR indices are non-negative safe integers.
|
|
62
|
+
if (!Number.isSafeInteger(horizonMmrEnd) || horizonMmrEnd <= 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"--horizon-mmr-end must be a positive integer no larger than 2^53-1",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const ttlSeconds = numberOption(args, "ttl-seconds", undefined);
|
|
68
|
+
if (
|
|
69
|
+
ttlSeconds !== undefined &&
|
|
70
|
+
(!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("--ttl-seconds must be a positive integer");
|
|
73
|
+
}
|
|
74
|
+
const options: DelegateOptions = {
|
|
75
|
+
...parseForestrieCommonOptions(args),
|
|
76
|
+
coordinatorUrl: requiredStringOption(
|
|
77
|
+
args,
|
|
78
|
+
"coordinator-url",
|
|
79
|
+
"DELEGATION_COORDINATOR_URL",
|
|
80
|
+
),
|
|
81
|
+
logId: requiredStringOption(args, "log-id"),
|
|
82
|
+
signWith: requiredStringOption(args, "sign-with"),
|
|
83
|
+
knownSealerKey: requiredStringOption(
|
|
84
|
+
args,
|
|
85
|
+
"known-sealer-key",
|
|
86
|
+
"KNOWN_SEALER_KEY",
|
|
87
|
+
),
|
|
88
|
+
horizonMmrEnd,
|
|
89
|
+
ttlSeconds,
|
|
90
|
+
outB64: optionalStringOption(args, "out-b64"),
|
|
91
|
+
};
|
|
92
|
+
return options;
|
|
93
|
+
}
|