@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,192 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { writeOutputFile } from "../lib/fsio.js";
|
|
3
|
+
import { readDeploymentRecord } from "../lib/deployment-record.js";
|
|
4
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
5
|
+
import {
|
|
6
|
+
buildEs256GenesisBody,
|
|
7
|
+
coordinatorSigningRouteUrl,
|
|
8
|
+
es256PublicKeyXy,
|
|
9
|
+
genesisPostUrl,
|
|
10
|
+
} from "../lib/onboard-genesis-body.js";
|
|
11
|
+
import type { OnboardGenesisOptions } from "../options/onboard-genesis.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FOR-406 (plan-2607-27 W2): operator genesis onboarding — turn a
|
|
15
|
+
* `forestrie deploy` into a usable forest. POSTs the five-label v2
|
|
16
|
+
* direct-sign genesis body under a PRE-MINTED onboard token (from
|
|
17
|
+
* `forestrie admin onboard-token`, or x402 settlement in future), wiring
|
|
18
|
+
* the coordinator signing-route webhook for sealing callbacks, then
|
|
19
|
+
* fetches the public genesis back to `--out` — the offline trust root
|
|
20
|
+
* every later `verify --genesis` consumes.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type OnboardGenesisReport = {
|
|
24
|
+
command: "onboard-genesis";
|
|
25
|
+
status: "onboarded";
|
|
26
|
+
logId: string;
|
|
27
|
+
univocity: string;
|
|
28
|
+
chainId: string;
|
|
29
|
+
webhookUrl: string;
|
|
30
|
+
httpStatus: number;
|
|
31
|
+
/** Present when `--out` was given. */
|
|
32
|
+
out?: string;
|
|
33
|
+
genesisBytes?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type OnboardGenesisErrorReport = {
|
|
37
|
+
error:
|
|
38
|
+
| "input_failed"
|
|
39
|
+
| "post_failed"
|
|
40
|
+
| "network_failed"
|
|
41
|
+
| "genesis_fetch_failed";
|
|
42
|
+
command: "onboard-genesis";
|
|
43
|
+
message: string;
|
|
44
|
+
httpStatus?: number;
|
|
45
|
+
/** Server response body (text) for post failures — surfaced, not masked. */
|
|
46
|
+
detail?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Test seam. */
|
|
50
|
+
export type OnboardGenesisDeps = {
|
|
51
|
+
fetchImpl?: typeof fetch;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ResolvedTarget = { univocity: string; logId: string };
|
|
55
|
+
|
|
56
|
+
function resolveTarget(options: OnboardGenesisOptions): ResolvedTarget {
|
|
57
|
+
if (options.deployment !== undefined) {
|
|
58
|
+
const record = readDeploymentRecord(options.deployment);
|
|
59
|
+
return { univocity: record.imutableUnivocity, logId: record.genesisLogId };
|
|
60
|
+
}
|
|
61
|
+
return { univocity: options.univocity!, logId: options.logId! };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reportError(
|
|
65
|
+
out: Out,
|
|
66
|
+
options: OnboardGenesisOptions,
|
|
67
|
+
report: OnboardGenesisErrorReport,
|
|
68
|
+
): void {
|
|
69
|
+
if (options.json) {
|
|
70
|
+
out.out(JSON.stringify(report, null, 2));
|
|
71
|
+
} else {
|
|
72
|
+
out.warn("forestrie onboard-genesis: %s", report.message);
|
|
73
|
+
if (report.detail !== undefined) {
|
|
74
|
+
out.warn(" detail: %s", report.detail);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runOnboardGenesis(
|
|
81
|
+
out: Out,
|
|
82
|
+
options: OnboardGenesisOptions,
|
|
83
|
+
deps: OnboardGenesisDeps = {},
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
86
|
+
|
|
87
|
+
let target: ResolvedTarget;
|
|
88
|
+
let body: Uint8Array;
|
|
89
|
+
try {
|
|
90
|
+
target = resolveTarget(options);
|
|
91
|
+
body = buildEs256GenesisBody({
|
|
92
|
+
chainId: options.chainId,
|
|
93
|
+
univocityAddress: target.univocity,
|
|
94
|
+
bootstrapKeyXy: es256PublicKeyXy(
|
|
95
|
+
readFileSync(options.bootstrapPem, "utf8"),
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
reportError(out, options, {
|
|
100
|
+
error: "input_failed",
|
|
101
|
+
command: "onboard-genesis",
|
|
102
|
+
message: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const webhookUrl =
|
|
108
|
+
options.webhookUrl ??
|
|
109
|
+
coordinatorSigningRouteUrl(options.coordinatorUrl!, target.logId);
|
|
110
|
+
const url = genesisPostUrl(options.baseUrl, target.logId, webhookUrl);
|
|
111
|
+
|
|
112
|
+
let res: Response;
|
|
113
|
+
try {
|
|
114
|
+
res = await fetchImpl(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Bearer ${options.onboardToken}`,
|
|
118
|
+
"Content-Type": "application/cbor",
|
|
119
|
+
},
|
|
120
|
+
body: body.buffer.slice(
|
|
121
|
+
body.byteOffset,
|
|
122
|
+
body.byteOffset + body.byteLength,
|
|
123
|
+
) as ArrayBuffer,
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
reportError(out, options, {
|
|
127
|
+
error: "network_failed",
|
|
128
|
+
command: "onboard-genesis",
|
|
129
|
+
message: err instanceof Error ? err.message : String(err),
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
// Includes the repeat-onboarding case: surface the server's verdict
|
|
135
|
+
// (status + body) clearly rather than masking it.
|
|
136
|
+
reportError(out, options, {
|
|
137
|
+
error: "post_failed",
|
|
138
|
+
command: "onboard-genesis",
|
|
139
|
+
message: `genesis POST rejected: HTTP ${res.status}`,
|
|
140
|
+
httpStatus: res.status,
|
|
141
|
+
detail: (await res.text()).slice(0, 512),
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let genesisBytes: number | undefined;
|
|
147
|
+
if (options.out !== undefined) {
|
|
148
|
+
const genesisRes = await fetchImpl(
|
|
149
|
+
`${options.baseUrl.replace(/\/$/, "")}/api/forest/${target.logId}/genesis`,
|
|
150
|
+
);
|
|
151
|
+
if (!genesisRes.ok) {
|
|
152
|
+
reportError(out, options, {
|
|
153
|
+
error: "genesis_fetch_failed",
|
|
154
|
+
command: "onboard-genesis",
|
|
155
|
+
message: `onboarded, but genesis fetch-back failed: HTTP ${genesisRes.status}`,
|
|
156
|
+
httpStatus: genesisRes.status,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const genesis = new Uint8Array(await genesisRes.arrayBuffer());
|
|
161
|
+
await writeOutputFile(options.out, genesis);
|
|
162
|
+
genesisBytes = genesis.length;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.json) {
|
|
166
|
+
const report: OnboardGenesisReport = {
|
|
167
|
+
command: "onboard-genesis",
|
|
168
|
+
status: "onboarded",
|
|
169
|
+
logId: target.logId,
|
|
170
|
+
univocity: target.univocity,
|
|
171
|
+
chainId: options.chainId,
|
|
172
|
+
webhookUrl,
|
|
173
|
+
httpStatus: res.status,
|
|
174
|
+
};
|
|
175
|
+
if (options.out !== undefined) {
|
|
176
|
+
report.out = options.out;
|
|
177
|
+
report.genesisBytes = genesisBytes!;
|
|
178
|
+
}
|
|
179
|
+
out.out(JSON.stringify(report, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
out.out("onboarded forest %s (HTTP %d)", target.logId, res.status);
|
|
183
|
+
out.print(" univocity: %s (chain %s)", target.univocity, options.chainId);
|
|
184
|
+
out.print(" sealing webhook: %s", webhookUrl);
|
|
185
|
+
if (options.out !== undefined) {
|
|
186
|
+
out.out(
|
|
187
|
+
"wrote public genesis (%d bytes) to %s — the offline trust root for verify --genesis",
|
|
188
|
+
genesisBytes!,
|
|
189
|
+
options.out,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
2
|
+
import { writeOutputFile } from "../lib/fsio.js";
|
|
3
|
+
import {
|
|
4
|
+
decodeCborDeterministic,
|
|
5
|
+
encodeCborDeterministic,
|
|
6
|
+
} from "@forestrie/encoding";
|
|
7
|
+
import {
|
|
8
|
+
readDeploymentRecord,
|
|
9
|
+
univocityAddrHex,
|
|
10
|
+
} from "../lib/deployment-record.js";
|
|
11
|
+
import {
|
|
12
|
+
buildOnboardAttestationEs256,
|
|
13
|
+
buildOnboardAttestationKs256,
|
|
14
|
+
} from "../lib/onboard-attestation.js";
|
|
15
|
+
import { loadEs256SigningKey } from "../lib/sign-statement-key.js";
|
|
16
|
+
import { signX402Payment } from "../lib/x402-payment.js";
|
|
17
|
+
import type { OnboardRequestOptions } from "../options/onboard-request.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Self-service onboarding (plan-2607-43 slice 06): create → (pay) → redeem.
|
|
21
|
+
*
|
|
22
|
+
* 1. `POST /api/onboarding/requests` with the chain binding from the deploy
|
|
23
|
+
* artifact and a bootstrap-key attestation (ADR-0059 D8) — the proof that
|
|
24
|
+
* the requester is the deployed contract's legitimate registrant.
|
|
25
|
+
* 2. `POST /api/onboarding/requests/{id}/redeem` with the one-time redeem
|
|
26
|
+
* code. An auto/ops-approved request redeems directly; a pending one
|
|
27
|
+
* answers 402 with an x402 challenge, which `--payer-key` signs
|
|
28
|
+
* (EIP-3009; REAL funds move when the payment settles) and the redeem is
|
|
29
|
+
* retried with `X-PAYMENT`.
|
|
30
|
+
*
|
|
31
|
+
* The redeemed onboard token is the ONLY product on stdout (or `--out`) so
|
|
32
|
+
* the command composes: `ONBOARD_TOKEN=$(forestrie onboard-request …)`.
|
|
33
|
+
* Private keys are never logged or echoed, including in errors.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const CREATE_LABEL_KEY = 1;
|
|
37
|
+
const CREATE_CHAIN_ID_KEY = 2;
|
|
38
|
+
const CREATE_UNIVOCITY_ADDR_KEY = 3;
|
|
39
|
+
const CREATE_CONTACT_EMAIL_KEY = 4;
|
|
40
|
+
const CREATE_ATTESTATION_KEY = 7;
|
|
41
|
+
const REDEEM_CODE_KEY = 1;
|
|
42
|
+
|
|
43
|
+
const X_PAYMENT_REQUIRED = "X-PAYMENT-REQUIRED";
|
|
44
|
+
const X_PAYMENT = "X-PAYMENT";
|
|
45
|
+
|
|
46
|
+
export type OnboardRequestReport = {
|
|
47
|
+
command: "onboard-request";
|
|
48
|
+
status: "redeemed";
|
|
49
|
+
requestId: string;
|
|
50
|
+
label: string;
|
|
51
|
+
admitted: "approved" | "paid";
|
|
52
|
+
token: string;
|
|
53
|
+
ref?: string;
|
|
54
|
+
out?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type OnboardRequestErrorReport = {
|
|
58
|
+
error:
|
|
59
|
+
| "input_failed"
|
|
60
|
+
| "create_failed"
|
|
61
|
+
| "redeem_failed"
|
|
62
|
+
| "payment_failed"
|
|
63
|
+
| "network_failed"
|
|
64
|
+
| "response_malformed";
|
|
65
|
+
command: "onboard-request";
|
|
66
|
+
message: string;
|
|
67
|
+
httpStatus?: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Test seam. */
|
|
71
|
+
export type OnboardRequestDeps = {
|
|
72
|
+
fetchImpl?: typeof fetch;
|
|
73
|
+
nowSec?: () => number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function requestsUrl(baseUrl: string): string {
|
|
77
|
+
return `${baseUrl.replace(/\/$/, "")}/api/onboarding/requests`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mapField(decoded: unknown, field: string): unknown {
|
|
81
|
+
if (decoded instanceof Map) return decoded.get(field);
|
|
82
|
+
if (decoded && typeof decoded === "object") {
|
|
83
|
+
return (decoded as Record<string, unknown>)[field];
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function textField(decoded: unknown, field: string): string | undefined {
|
|
89
|
+
const v = mapField(decoded, field);
|
|
90
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function reportError(
|
|
94
|
+
out: Out,
|
|
95
|
+
options: OnboardRequestOptions,
|
|
96
|
+
report: OnboardRequestErrorReport,
|
|
97
|
+
): void {
|
|
98
|
+
if (options.json) {
|
|
99
|
+
out.out(JSON.stringify(report, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
out.warn("forestrie onboard-request: %s", report.message);
|
|
102
|
+
if (report.httpStatus !== undefined) {
|
|
103
|
+
out.warn(" httpStatus: %d", report.httpStatus);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cborBody(map: Map<number, unknown>): ArrayBuffer {
|
|
110
|
+
const bytes = encodeCborDeterministic(map);
|
|
111
|
+
return bytes.buffer.slice(
|
|
112
|
+
bytes.byteOffset,
|
|
113
|
+
bytes.byteOffset + bytes.byteLength,
|
|
114
|
+
) as ArrayBuffer;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function runOnboardRequest(
|
|
118
|
+
out: Out,
|
|
119
|
+
options: OnboardRequestOptions,
|
|
120
|
+
deps: OnboardRequestDeps = {},
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
123
|
+
const nowSec = deps.nowSec ?? (() => Math.floor(Date.now() / 1000));
|
|
124
|
+
|
|
125
|
+
// --- Resolve binding + build the attestation ---
|
|
126
|
+
let chainId: string;
|
|
127
|
+
let univocityAddr: string;
|
|
128
|
+
let attestation: Uint8Array;
|
|
129
|
+
try {
|
|
130
|
+
const record = readDeploymentRecord(options.deployment);
|
|
131
|
+
if (record.chainId === undefined) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${options.deployment}: deployment carries no chainId (re-run forestrie deploy)`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
chainId = record.chainId;
|
|
137
|
+
univocityAddr = univocityAddrHex(record);
|
|
138
|
+
const aud = new URL(options.baseUrl).origin;
|
|
139
|
+
const input = { chainId, univocityAddr, aud, nowSec: nowSec() };
|
|
140
|
+
if (options.bootstrapPem !== undefined) {
|
|
141
|
+
const key = await loadEs256SigningKey(options.bootstrapPem);
|
|
142
|
+
attestation = await buildOnboardAttestationEs256(key, input);
|
|
143
|
+
} else if (options.bootstrapKeyHex !== undefined) {
|
|
144
|
+
attestation = await buildOnboardAttestationKs256(
|
|
145
|
+
options.bootstrapKeyHex,
|
|
146
|
+
input,
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"attestation key required: pass --bootstrap-pem (ES256) or --bootstrap-key-hex (KS256)",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
reportError(out, options, {
|
|
155
|
+
error: "input_failed",
|
|
156
|
+
command: "onboard-request",
|
|
157
|
+
message: err instanceof Error ? err.message : String(err),
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Create the attested request ---
|
|
163
|
+
let requestId: string;
|
|
164
|
+
let redeemCode: string;
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetchImpl(requestsUrl(options.baseUrl), {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: {
|
|
169
|
+
"Content-Type": "application/cbor",
|
|
170
|
+
Accept: "application/cbor",
|
|
171
|
+
},
|
|
172
|
+
body: cborBody(
|
|
173
|
+
new Map<number, unknown>([
|
|
174
|
+
[CREATE_LABEL_KEY, options.label],
|
|
175
|
+
[CREATE_CHAIN_ID_KEY, chainId],
|
|
176
|
+
[CREATE_UNIVOCITY_ADDR_KEY, univocityAddr],
|
|
177
|
+
[CREATE_CONTACT_EMAIL_KEY, options.contactEmail],
|
|
178
|
+
[CREATE_ATTESTATION_KEY, attestation],
|
|
179
|
+
]),
|
|
180
|
+
),
|
|
181
|
+
});
|
|
182
|
+
if (res.status !== 201) {
|
|
183
|
+
reportError(out, options, {
|
|
184
|
+
error: "create_failed",
|
|
185
|
+
command: "onboard-request",
|
|
186
|
+
message: `request rejected: HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`,
|
|
187
|
+
httpStatus: res.status,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const decoded = decodeCborDeterministic(
|
|
192
|
+
new Uint8Array(await res.arrayBuffer()),
|
|
193
|
+
);
|
|
194
|
+
const id = textField(decoded, "requestId");
|
|
195
|
+
const code = textField(decoded, "redeemCode");
|
|
196
|
+
if (!id || !code) {
|
|
197
|
+
reportError(out, options, {
|
|
198
|
+
error: "response_malformed",
|
|
199
|
+
command: "onboard-request",
|
|
200
|
+
message: "create response missing requestId/redeemCode",
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
requestId = id;
|
|
205
|
+
redeemCode = code;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
reportError(out, options, {
|
|
208
|
+
error: "network_failed",
|
|
209
|
+
command: "onboard-request",
|
|
210
|
+
message: err instanceof Error ? err.message : String(err),
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Redeem (pay the 402 when challenged) ---
|
|
216
|
+
const redeemUrl = `${requestsUrl(options.baseUrl)}/${encodeURIComponent(requestId)}/redeem`;
|
|
217
|
+
const redeem = (extraHeaders: Record<string, string>) =>
|
|
218
|
+
fetchImpl(redeemUrl, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: {
|
|
221
|
+
"Content-Type": "application/cbor",
|
|
222
|
+
Accept: "application/cbor",
|
|
223
|
+
...extraHeaders,
|
|
224
|
+
},
|
|
225
|
+
body: cborBody(new Map<number, unknown>([[REDEEM_CODE_KEY, redeemCode]])),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
let admitted: "approved" | "paid" = "approved";
|
|
229
|
+
let res: Response;
|
|
230
|
+
try {
|
|
231
|
+
res = await redeem({});
|
|
232
|
+
if (res.status === 402) {
|
|
233
|
+
const challenge = res.headers.get(X_PAYMENT_REQUIRED);
|
|
234
|
+
if (!challenge) {
|
|
235
|
+
reportError(out, options, {
|
|
236
|
+
error: "payment_failed",
|
|
237
|
+
command: "onboard-request",
|
|
238
|
+
message: "402 without an X-PAYMENT-REQUIRED challenge header",
|
|
239
|
+
httpStatus: 402,
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (options.payerKey === undefined) {
|
|
244
|
+
reportError(out, options, {
|
|
245
|
+
error: "payment_failed",
|
|
246
|
+
command: "onboard-request",
|
|
247
|
+
message:
|
|
248
|
+
"deployment requires payment to redeem; pass --payer-key (env X402_PAYER_KEY)",
|
|
249
|
+
httpStatus: 402,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const xPayment = await signX402Payment(
|
|
254
|
+
challenge,
|
|
255
|
+
options.payerKey,
|
|
256
|
+
nowSec(),
|
|
257
|
+
);
|
|
258
|
+
admitted = "paid";
|
|
259
|
+
res = await redeem({ [X_PAYMENT]: xPayment });
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
reportError(out, options, {
|
|
263
|
+
error: "network_failed",
|
|
264
|
+
command: "onboard-request",
|
|
265
|
+
message: err instanceof Error ? err.message : String(err),
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (res.status !== 200) {
|
|
271
|
+
reportError(out, options, {
|
|
272
|
+
error: "redeem_failed",
|
|
273
|
+
command: "onboard-request",
|
|
274
|
+
message: `redeem rejected: HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`,
|
|
275
|
+
httpStatus: res.status,
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let token: string | undefined;
|
|
281
|
+
let ref: string | undefined;
|
|
282
|
+
try {
|
|
283
|
+
const decoded = decodeCborDeterministic(
|
|
284
|
+
new Uint8Array(await res.arrayBuffer()),
|
|
285
|
+
);
|
|
286
|
+
token = textField(decoded, "token");
|
|
287
|
+
ref = textField(decoded, "ref");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
reportError(out, options, {
|
|
290
|
+
error: "response_malformed",
|
|
291
|
+
command: "onboard-request",
|
|
292
|
+
message: `redeem response is not valid CBOR: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (token === undefined) {
|
|
297
|
+
reportError(out, options, {
|
|
298
|
+
error: "response_malformed",
|
|
299
|
+
command: "onboard-request",
|
|
300
|
+
message: "redeem response carried no token field",
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.out !== undefined) {
|
|
306
|
+
await writeOutputFile(options.out, token);
|
|
307
|
+
}
|
|
308
|
+
if (options.json) {
|
|
309
|
+
const report: OnboardRequestReport = {
|
|
310
|
+
command: "onboard-request",
|
|
311
|
+
status: "redeemed",
|
|
312
|
+
requestId,
|
|
313
|
+
label: options.label,
|
|
314
|
+
admitted,
|
|
315
|
+
token,
|
|
316
|
+
};
|
|
317
|
+
if (ref !== undefined) report.ref = ref;
|
|
318
|
+
if (options.out !== undefined) report.out = options.out;
|
|
319
|
+
out.out(JSON.stringify(report, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (options.out !== undefined) {
|
|
323
|
+
out.print(
|
|
324
|
+
"wrote onboard token (request %s, %s) to %s",
|
|
325
|
+
requestId,
|
|
326
|
+
admitted,
|
|
327
|
+
options.out,
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// The token is the pipeable product: nothing else on stdout.
|
|
332
|
+
out.out("%s", token);
|
|
333
|
+
}
|