@forestrie/forestrie-cli 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/TRUST-MODEL.md +8 -0
  4. package/dist/cli.js +33811 -0
  5. package/dist/decode-receipt.d.ts +19 -0
  6. package/dist/decode-receipt.d.ts.map +1 -0
  7. package/dist/decode-receipt.js +19 -0
  8. package/dist/decode-receipt.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +11 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/decode-receipt-cbor.d.ts +26 -0
  14. package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
  15. package/dist/lib/decode-receipt-cbor.js +182 -0
  16. package/dist/lib/decode-receipt-cbor.js.map +1 -0
  17. package/dist/lib/decode-receipt-decode.d.ts +103 -0
  18. package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
  19. package/dist/lib/decode-receipt-decode.js +237 -0
  20. package/dist/lib/decode-receipt-decode.js.map +1 -0
  21. package/dist/lib/decode-receipt-labels.d.ts +54 -0
  22. package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
  23. package/dist/lib/decode-receipt-labels.js +117 -0
  24. package/dist/lib/decode-receipt-labels.js.map +1 -0
  25. package/dist/lib/decode-receipt-render.d.ts +11 -0
  26. package/dist/lib/decode-receipt-render.d.ts.map +1 -0
  27. package/dist/lib/decode-receipt-render.js +242 -0
  28. package/dist/lib/decode-receipt-render.js.map +1 -0
  29. package/package.json +83 -0
  30. package/src/cli.ts +5 -0
  31. package/src/command.ts +51 -0
  32. package/src/commands/admin-onboard-token.ts +51 -0
  33. package/src/commands/admin.ts +20 -0
  34. package/src/commands/complete-grant.ts +41 -0
  35. package/src/commands/create-consistency-proof.ts +44 -0
  36. package/src/commands/create-log.ts +95 -0
  37. package/src/commands/decode-receipt.ts +20 -0
  38. package/src/commands/delegate.ts +55 -0
  39. package/src/commands/deploy.ts +72 -0
  40. package/src/commands/fetch-accumulator.ts +49 -0
  41. package/src/commands/onboard-genesis.ts +70 -0
  42. package/src/commands/onboard-request.ts +59 -0
  43. package/src/commands/register-grant.ts +74 -0
  44. package/src/commands/register.ts +54 -0
  45. package/src/commands/resolve-receipt.ts +101 -0
  46. package/src/commands/sign-statement.ts +53 -0
  47. package/src/commands/verify-grant.ts +89 -0
  48. package/src/commands/verify.ts +92 -0
  49. package/src/commoncli.ts +34 -0
  50. package/src/decode-receipt.ts +56 -0
  51. package/src/index.ts +10 -0
  52. package/src/lib/bytes.ts +7 -0
  53. package/src/lib/checkpoint-provider.ts +297 -0
  54. package/src/lib/complete-grant-derive.ts +237 -0
  55. package/src/lib/consistency-proof.ts +271 -0
  56. package/src/lib/create-log-build.ts +168 -0
  57. package/src/lib/create-log-prepare.ts +75 -0
  58. package/src/lib/create-receipt-chain-verify.ts +267 -0
  59. package/src/lib/create-receipt-derive.ts +208 -0
  60. package/src/lib/create-receipt-inputs.ts +79 -0
  61. package/src/lib/decode-checkpoint-calldata.ts +188 -0
  62. package/src/lib/decode-receipt-cbor.ts +202 -0
  63. package/src/lib/decode-receipt-decode.ts +333 -0
  64. package/src/lib/decode-receipt-labels.ts +132 -0
  65. package/src/lib/decode-receipt-render.ts +284 -0
  66. package/src/lib/delegate-flow.ts +289 -0
  67. package/src/lib/delegate-voucher.ts +122 -0
  68. package/src/lib/deploy-artifact.ts +79 -0
  69. package/src/lib/deploy-bootstrap.ts +100 -0
  70. package/src/lib/deploy-flow.ts +184 -0
  71. package/src/lib/deploy-genesis-log-id.ts +29 -0
  72. package/src/lib/deploy-rpc.ts +40 -0
  73. package/src/lib/deployment-record.ts +65 -0
  74. package/src/lib/fsio.ts +91 -0
  75. package/src/lib/grant-flags-local.ts +19 -0
  76. package/src/lib/onboard-attestation.ts +120 -0
  77. package/src/lib/onboard-genesis-body.ts +94 -0
  78. package/src/lib/openssl-error-queue.ts +56 -0
  79. package/src/lib/register-flow.ts +292 -0
  80. package/src/lib/register-grant-build.ts +173 -0
  81. package/src/lib/register-grant-complete.ts +47 -0
  82. package/src/lib/register-grant-flow.ts +258 -0
  83. package/src/lib/resolve-receipt-freshen.ts +221 -0
  84. package/src/lib/sign-statement-build.ts +117 -0
  85. package/src/lib/sign-statement-key.ts +145 -0
  86. package/src/lib/verify-anchored.ts +326 -0
  87. package/src/lib/verify-checkpoint-chain.ts +160 -0
  88. package/src/lib/verify-eventscan.ts +214 -0
  89. package/src/lib/verify-inputs.ts +143 -0
  90. package/src/lib/verify-known-accumulator.ts +161 -0
  91. package/src/lib/verify-report.ts +265 -0
  92. package/src/lib/x402-payment.ts +115 -0
  93. package/src/main/admin-onboard-token.ts +216 -0
  94. package/src/main/complete-grant.ts +265 -0
  95. package/src/main/create-consistency-proof.ts +83 -0
  96. package/src/main/create-log.ts +347 -0
  97. package/src/main/create-receipt.ts +763 -0
  98. package/src/main/decode-receipt.ts +72 -0
  99. package/src/main/delegate.ts +141 -0
  100. package/src/main/deploy.ts +273 -0
  101. package/src/main/fetch-accumulator.ts +181 -0
  102. package/src/main/not-implemented.ts +43 -0
  103. package/src/main/onboard-genesis.ts +192 -0
  104. package/src/main/onboard-request.ts +333 -0
  105. package/src/main/register-grant.ts +270 -0
  106. package/src/main/register.ts +243 -0
  107. package/src/main/sign-statement.ts +168 -0
  108. package/src/main/verify.ts +767 -0
  109. package/src/options/admin-onboard-token.ts +48 -0
  110. package/src/options/common.ts +69 -0
  111. package/src/options/complete-grant.ts +40 -0
  112. package/src/options/create-consistency-proof.ts +63 -0
  113. package/src/options/create-log.ts +99 -0
  114. package/src/options/create-receipt.ts +243 -0
  115. package/src/options/decode-receipt.ts +25 -0
  116. package/src/options/delegate.ts +93 -0
  117. package/src/options/deploy.ts +105 -0
  118. package/src/options/fetch-accumulator.ts +62 -0
  119. package/src/options/onboard-genesis.ts +74 -0
  120. package/src/options/onboard-request.ts +57 -0
  121. package/src/options/register-grant.ts +72 -0
  122. package/src/options/register.ts +71 -0
  123. package/src/options/sign-statement.ts +64 -0
  124. package/src/options/verify.ts +250 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * ImutableUnivocity creation-bytecode acquisition for `forestrie deploy`
3
+ * (FOR-340).
4
+ *
5
+ * Default path: fetch the deploy-manifest + sha256 sidecar from the
6
+ * univocity GitHub release (deploy-core `fetchUnivocityReleaseManifest`,
7
+ * `latest` resolved via the GitHub API), verify the manifest bytes
8
+ * against the sidecar, then verify every embedded bytecode digest.
9
+ *
10
+ * `--release-manifest <path>` swaps the fetch for a local file (offline /
11
+ * test escape hatch). The sidecar step is skipped — the file is operator
12
+ * supplied — but the embedded per-contract sha256 digests are still
13
+ * verified by `verifyAndParseImutableManifest`.
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import {
17
+ fetchUnivocityReleaseManifest,
18
+ verifyAndParseImutableManifest,
19
+ verifyManifestBytesWithSidecar,
20
+ type DeployManifest,
21
+ } from "@forestrie/deploy-core";
22
+ import type { Hex } from "viem";
23
+ import { errorMessage } from "./sign-statement-key.js";
24
+
25
+ /** Verified deployable artifact. */
26
+ export type ImutableDeployArtifact = {
27
+ manifest: DeployManifest;
28
+ /** ImutableUnivocity creation bytecode (digest-verified). */
29
+ creationBytecode: Hex;
30
+ /** Manifest releaseId (release tag for fetched manifests). */
31
+ releaseId: string;
32
+ source: "release" | "file";
33
+ };
34
+
35
+ export type LoadImutableArtifactInput = {
36
+ /** Local deploy-manifest JSON path; overrides the release fetch. */
37
+ manifestPath?: string | undefined;
38
+ /** Release tag (`latest` or `vX.Y.Z`) when fetching. */
39
+ releaseTag: string;
40
+ };
41
+
42
+ /** Load + verify the ImutableUnivocity deploy artifact. */
43
+ export async function loadImutableArtifact(
44
+ input: LoadImutableArtifactInput,
45
+ ): Promise<ImutableDeployArtifact> {
46
+ if (input.manifestPath !== undefined) {
47
+ let raw: string;
48
+ try {
49
+ raw = readFileSync(input.manifestPath, "utf8");
50
+ } catch (err) {
51
+ throw new Error(
52
+ `cannot read deploy manifest ${input.manifestPath}: ${errorMessage(err)}`,
53
+ );
54
+ }
55
+ const { manifest, artifact } = await verifyAndParseImutableManifest(raw);
56
+ return {
57
+ manifest,
58
+ creationBytecode: artifact.bytecode,
59
+ releaseId: manifest.releaseId,
60
+ source: "file",
61
+ };
62
+ }
63
+
64
+ const fetched = await fetchUnivocityReleaseManifest(input.releaseTag);
65
+ await verifyManifestBytesWithSidecar(
66
+ new TextEncoder().encode(fetched.raw),
67
+ fetched.sidecar,
68
+ );
69
+ const { manifest, artifact } = await verifyAndParseImutableManifest(
70
+ fetched.raw,
71
+ { expectedReleaseId: fetched.releaseTag },
72
+ );
73
+ return {
74
+ manifest,
75
+ creationBytecode: artifact.bytecode,
76
+ releaseId: manifest.releaseId,
77
+ source: "release",
78
+ };
79
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * ES256 bootstrap key resolution for `forestrie deploy` (FOR-340).
3
+ *
4
+ * The bootstrap key is the log's trust root: its public P-256 point
5
+ * (`x||y`, 64 bytes) is bound into the ImutableUnivocity constructor.
6
+ * Only the PUBLIC coordinates go on-chain, so — unlike sign-statement's
7
+ * `loadEs256SigningKey` (src/lib/sign-statement-key.ts), which needs the
8
+ * private scalar — this resolver uses `@forestrie/deploy-core`'s
9
+ * `resolveBootstrapKey`, which also accepts public-only SPKI PEMs.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { writeOutputFile } from "./fsio.js";
13
+ import {
14
+ generateEs256BootstrapKey,
15
+ resolveBootstrapKey,
16
+ type BootstrapKey,
17
+ } from "@forestrie/deploy-core";
18
+ import { drainOpenSslErrorQueue } from "./openssl-error-queue.js";
19
+ import { errorMessage } from "./sign-statement-key.js";
20
+
21
+ /** Resolved ES256 bootstrap key ready for constructor encoding. */
22
+ export type ResolvedDeployBootstrap = {
23
+ /** deploy-core bootstrap key: `algId` (COSE -7) + 64-byte `x||y`. */
24
+ bootstrap: BootstrapKey;
25
+ /** Path the generated PKCS#8 PEM was written to (generate mode only). */
26
+ pemOut?: string;
27
+ };
28
+
29
+ /** Inputs already validated by `parseDeployOptions`. */
30
+ export type DeployBootstrapInput = {
31
+ /** Generate a fresh keypair (requires `pemOut`). */
32
+ generate: boolean;
33
+ /** Where to persist the generated PKCS#8 PEM. */
34
+ pemOut?: string | undefined;
35
+ /** Existing key PEM path (mutually exclusive with `generate`). */
36
+ pemPath?: string | undefined;
37
+ };
38
+
39
+ /** Test seam: PEM persistence (real file write by default). */
40
+ export type WritePem = (path: string, pem: string) => Promise<unknown>;
41
+
42
+ /**
43
+ * Resolve or generate the ES256 bootstrap key.
44
+ *
45
+ * Generate mode writes the PKCS#8 private key PEM to `pemOut` BEFORE the
46
+ * deploy transaction is built: if the write fails, nothing is deployed
47
+ * with an unrecoverable trust root.
48
+ */
49
+ export async function resolveDeployBootstrapKey(
50
+ input: DeployBootstrapInput,
51
+ writePem: WritePem = writeOutputFile,
52
+ ): Promise<ResolvedDeployBootstrap> {
53
+ if (input.generate) {
54
+ if (input.pemOut === undefined) {
55
+ throw new Error("--bootstrap-es256-generate requires --bootstrap-es256-pem-out");
56
+ }
57
+ const generated = await generateEs256BootstrapKey();
58
+ try {
59
+ await writePem(input.pemOut, generated.pem);
60
+ } catch (err) {
61
+ throw new Error(
62
+ `cannot write bootstrap PEM to ${input.pemOut}: ${errorMessage(err)}`,
63
+ );
64
+ }
65
+ const bootstrap = await resolveBootstrapKey({
66
+ alg: "es256",
67
+ x: generated.x,
68
+ y: generated.y,
69
+ });
70
+ return { bootstrap, pemOut: input.pemOut };
71
+ }
72
+
73
+ if (input.pemPath === undefined) {
74
+ throw new Error(
75
+ "es256 bootstrap needs --bootstrap-es256-generate or --bootstrap-es256-pem",
76
+ );
77
+ }
78
+ let pem: string;
79
+ try {
80
+ pem = readFileSync(input.pemPath, "utf8");
81
+ } catch (err) {
82
+ throw new Error(
83
+ `cannot read bootstrap PEM ${input.pemPath}: ${errorMessage(err)}`,
84
+ );
85
+ }
86
+ let bootstrap: BootstrapKey;
87
+ try {
88
+ bootstrap = await resolveBootstrapKey({ alg: "es256", pem });
89
+ } catch (err) {
90
+ // deploy-core parses via `crypto.subtle.importKey`; on bun a failed
91
+ // import over malformed DER leaves a stale entry on the OpenSSL error
92
+ // queue that would poison the next node:crypto PEM parse (FOR-343).
93
+ // Drain it here so the failure stays contained to this call.
94
+ drainOpenSslErrorQueue();
95
+ throw new Error(
96
+ `${input.pemPath}: not a usable ES256 (P-256) key PEM: ${errorMessage(err)}`,
97
+ );
98
+ }
99
+ return { bootstrap };
100
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Deploy transaction construction + submission flow for `forestrie
3
+ * deploy` (FOR-340).
4
+ *
5
+ * ES256 / EOA paved path: a DIRECT contract-creation transaction (no
6
+ * `to`) whose data is the ImutableUnivocity creation bytecode followed
7
+ * by `abi.encode(int64 bootstrapAlg, bytes bootstrapKey)` — built by
8
+ * deploy-core `buildImutableDeploymentData`. The deployed address comes
9
+ * from `receipt.contractAddress`. (The Safe/CreateCall CREATE2 path is
10
+ * the pre-provisioned KS256 aside and is deliberately NOT driven here.)
11
+ */
12
+ import {
13
+ buildImutableDeploymentData,
14
+ type BootstrapKey,
15
+ } from "@forestrie/deploy-core";
16
+ import { getAddress, type Address, type Hex } from "viem";
17
+ import { genesisLogIdFromImutableAddress } from "./deploy-genesis-log-id.js";
18
+
19
+ /**
20
+ * Failure stage taxonomy:
21
+ * - `network` — the RPC endpoint could not be reached / transport error
22
+ * - `deploy` — the chain rejected or reverted the deploy transaction
23
+ */
24
+ export type DeployFlowStage = "network" | "deploy";
25
+
26
+ /** Structured deploy failure; `stage` maps onto the `--json` error codes. */
27
+ export class DeployFlowError extends Error {
28
+ readonly stage: DeployFlowStage;
29
+ readonly detail?: string;
30
+ readonly txHash?: Hex;
31
+
32
+ constructor(
33
+ stage: DeployFlowStage,
34
+ message: string,
35
+ extra: { detail?: string; txHash?: Hex; cause?: unknown } = {},
36
+ ) {
37
+ super(message, extra.cause === undefined ? undefined : { cause: extra.cause });
38
+ this.name = "DeployFlowError";
39
+ this.stage = stage;
40
+ if (extra.detail !== undefined) this.detail = extra.detail;
41
+ if (extra.txHash !== undefined) this.txHash = extra.txHash;
42
+ }
43
+ }
44
+
45
+ /** Receipt subset the flow needs. */
46
+ export type DeployReceipt = {
47
+ status: "success" | "reverted";
48
+ contractAddress: string | null;
49
+ };
50
+
51
+ /**
52
+ * Minimal RPC surface for the flow — the seam mocked by unit tests.
53
+ * The real implementation (viem over `--rpc-url`) is in
54
+ * `deploy-rpc.ts`.
55
+ */
56
+ export type DeployRpc = {
57
+ /** Deployer (gas) account address derived from `--deployer-key`. */
58
+ readonly from: Address;
59
+ getChainId(): Promise<number>;
60
+ /** Send the contract-creation tx (`to` omitted); resolves to the tx hash. */
61
+ sendDeployTransaction(data: Hex): Promise<Hex>;
62
+ waitForReceipt(hash: Hex): Promise<DeployReceipt>;
63
+ };
64
+
65
+ export type DeployFlowInput = {
66
+ /** Digest-verified ImutableUnivocity creation bytecode. */
67
+ creationBytecode: Hex;
68
+ /** Resolved bootstrap key (constructor args). */
69
+ bootstrap: BootstrapKey;
70
+ };
71
+
72
+ export type DeployFlowResult = {
73
+ chainId: number;
74
+ /** Deployed ImutableUnivocity address (checksummed). */
75
+ imutableUnivocity: Address;
76
+ /** Root/bootstrap log UUID derived from the address. */
77
+ genesisLogId: string;
78
+ txHash: Hex;
79
+ from: Address;
80
+ /** Full creation-tx data (bytecode + encoded constructor args). */
81
+ deploymentData: Hex;
82
+ };
83
+
84
+ /** Creation-tx data: bytecode ++ abi.encode(int64 alg, bytes key). */
85
+ export function buildDeploymentData(
86
+ creationBytecode: Hex,
87
+ bootstrap: BootstrapKey,
88
+ ): Hex {
89
+ return buildImutableDeploymentData(
90
+ creationBytecode,
91
+ bootstrap.algId,
92
+ bootstrap.key,
93
+ );
94
+ }
95
+
96
+ /** True when `err` (or a cause below it) is an RPC transport failure. */
97
+ export function isTransportError(err: unknown): boolean {
98
+ for (let e: unknown = err; e instanceof Error; e = e.cause) {
99
+ if (
100
+ e.name === "HttpRequestError" ||
101
+ e.name === "TimeoutError" ||
102
+ e.name === "SocketClosedError" ||
103
+ e.name === "WebSocketRequestError" ||
104
+ (e instanceof TypeError && /fetch|connect/i.test(e.message))
105
+ ) {
106
+ return true;
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+
112
+ function stageFor(err: unknown, fallback: DeployFlowStage): DeployFlowStage {
113
+ return isTransportError(err) ? "network" : fallback;
114
+ }
115
+
116
+ function detailOf(err: unknown): string {
117
+ return err instanceof Error ? err.message : String(err);
118
+ }
119
+
120
+ /** Build, submit and confirm the deploy transaction. */
121
+ export async function runDeployFlow(
122
+ rpc: DeployRpc,
123
+ input: DeployFlowInput,
124
+ ): Promise<DeployFlowResult> {
125
+ let chainId: number;
126
+ try {
127
+ chainId = await rpc.getChainId();
128
+ } catch (err) {
129
+ throw new DeployFlowError("network", "cannot reach the RPC endpoint", {
130
+ detail: detailOf(err),
131
+ cause: err,
132
+ });
133
+ }
134
+
135
+ const deploymentData = buildDeploymentData(
136
+ input.creationBytecode,
137
+ input.bootstrap,
138
+ );
139
+
140
+ let txHash: Hex;
141
+ try {
142
+ txHash = await rpc.sendDeployTransaction(deploymentData);
143
+ } catch (err) {
144
+ throw new DeployFlowError(
145
+ stageFor(err, "deploy"),
146
+ "deploy transaction submission failed",
147
+ { detail: detailOf(err), cause: err },
148
+ );
149
+ }
150
+
151
+ let receipt: DeployReceipt;
152
+ try {
153
+ receipt = await rpc.waitForReceipt(txHash);
154
+ } catch (err) {
155
+ throw new DeployFlowError(
156
+ stageFor(err, "deploy"),
157
+ "deploy transaction was not confirmed",
158
+ { detail: detailOf(err), txHash, cause: err },
159
+ );
160
+ }
161
+
162
+ if (receipt.status !== "success") {
163
+ throw new DeployFlowError("deploy", "deploy transaction reverted", {
164
+ txHash,
165
+ });
166
+ }
167
+ if (receipt.contractAddress == null) {
168
+ throw new DeployFlowError(
169
+ "deploy",
170
+ "deploy receipt has no contractAddress (not a contract-creation tx?)",
171
+ { txHash },
172
+ );
173
+ }
174
+
175
+ const imutableUnivocity = getAddress(receipt.contractAddress);
176
+ return {
177
+ chainId,
178
+ imutableUnivocity,
179
+ genesisLogId: genesisLogIdFromImutableAddress(imutableUnivocity),
180
+ txHash,
181
+ from: rpc.from,
182
+ deploymentData,
183
+ };
184
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Genesis (root/bootstrap) logId derivation for `forestrie deploy`
3
+ * (FOR-340).
4
+ *
5
+ * The root log's UUID is the first 16 bytes of the deployed
6
+ * ImutableUnivocity contract address, formatted 8-4-4-4-12 (the last 4
7
+ * address bytes are dropped). This is a cross-repo contract with the
8
+ * canopy naming scheme; the same derivation lives in univocity-tools
9
+ * `@univocity-tools/deployer-common` `genesisLogIdFromImutableAddress`
10
+ * (unpublished — mirrored here rather than depended on; graduating it
11
+ * into `@forestrie/deploy-core` is a univocity-tools follow-up).
12
+ *
13
+ * Test vector (shared with deployer-common):
14
+ * `0x1528b86fF561f617602356efdbD05908a07AA788` →
15
+ * `1528b86f-f561-f617-6023-56efdbd05908`.
16
+ */
17
+ import { getAddress } from "viem";
18
+
19
+ /** Derive the root/bootstrap log UUID from an ImutableUnivocity address. */
20
+ export function genesisLogIdFromImutableAddress(address: string): string {
21
+ let normalized: string;
22
+ try {
23
+ normalized = getAddress(address);
24
+ } catch {
25
+ throw new Error(`expected 20-byte address, got ${address}`);
26
+ }
27
+ const h = normalized.slice(2).toLowerCase();
28
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
29
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Real `DeployRpc` implementation for `forestrie deploy` (FOR-340):
3
+ * viem public + wallet clients over the `--rpc-url` HTTP transport,
4
+ * signing locally with the funded `--deployer-key` (gas-only — the
5
+ * ES256 bootstrap key is the trust root, not this key).
6
+ *
7
+ * Mirrors univocity-tools deployer-common's `createRpcClients` /
8
+ * `executeProposal` EOA path: `sendTransaction` with `chain: null` and
9
+ * no `to` is a plain contract-creation transaction.
10
+ */
11
+ import { createPublicClient, createWalletClient, http, type Hex } from "viem";
12
+ import { privateKeyToAccount } from "viem/accounts";
13
+ import type { DeployRpc } from "./deploy-flow.js";
14
+
15
+ /** viem-backed `DeployRpc` over a JSON-RPC HTTP endpoint. */
16
+ export function createDeployRpc(rpcUrl: string, deployerKey: Hex): DeployRpc {
17
+ const account = privateKeyToAccount(deployerKey);
18
+ const transport = http(rpcUrl);
19
+ const publicClient = createPublicClient({ transport });
20
+ const walletClient = createWalletClient({ account, transport });
21
+
22
+ return {
23
+ from: account.address,
24
+ getChainId: () => publicClient.getChainId(),
25
+ sendDeployTransaction: (data) =>
26
+ walletClient.sendTransaction({
27
+ account,
28
+ chain: null,
29
+ data,
30
+ value: 0n,
31
+ }),
32
+ waitForReceipt: async (hash) => {
33
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
34
+ return {
35
+ status: receipt.status,
36
+ contractAddress: receipt.contractAddress ?? null,
37
+ };
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,65 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * A `forestrie deploy --out` artifact. Single shared reader (FOR-480):
5
+ * `onboard-genesis`, `admin onboard-token`, and `onboard-request` all resolve
6
+ * their chain binding from the same file the deploy wrote, so the binding is
7
+ * stated exactly once per ceremony.
8
+ */
9
+ export type DeploymentRecord = {
10
+ imutableUnivocity: string;
11
+ genesisLogId: string;
12
+ /**
13
+ * Bare decimal string (the file stores a JSON number; coerced here).
14
+ * Optional: hand-crafted two-field artifacts remain valid for
15
+ * onboard-genesis, which sources chainId from its own flag.
16
+ */
17
+ chainId?: string | undefined;
18
+ bootstrapAlg?: "es256" | "ks256" | undefined;
19
+ txHash?: string | undefined;
20
+ };
21
+
22
+ export function readDeploymentRecord(path: string): DeploymentRecord {
23
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
24
+ imutableUnivocity?: string;
25
+ genesisLogId?: string;
26
+ chainId?: number | string;
27
+ bootstrapAlg?: string;
28
+ txHash?: string;
29
+ };
30
+ const imutableUnivocity = parsed.imutableUnivocity?.trim();
31
+ const genesisLogId = parsed.genesisLogId?.trim();
32
+ if (!imutableUnivocity || !genesisLogId) {
33
+ throw new Error(
34
+ `${path}: expected imutableUnivocity and genesisLogId ` +
35
+ "(a forestrie deploy --out artifact)",
36
+ );
37
+ }
38
+ const chainIdRaw = parsed.chainId;
39
+ const chainId =
40
+ typeof chainIdRaw === "number"
41
+ ? String(chainIdRaw)
42
+ : (chainIdRaw?.trim() ?? "");
43
+ if (chainId && !/^[0-9]+$/.test(chainId)) {
44
+ throw new Error(`${path}: expected a numeric chainId`);
45
+ }
46
+ const bootstrapAlg =
47
+ parsed.bootstrapAlg === "es256" || parsed.bootstrapAlg === "ks256"
48
+ ? parsed.bootstrapAlg
49
+ : undefined;
50
+ const record: DeploymentRecord = { imutableUnivocity, genesisLogId };
51
+ if (chainId) record.chainId = chainId;
52
+ if (bootstrapAlg !== undefined) record.bootstrapAlg = bootstrapAlg;
53
+ const txHash = parsed.txHash?.trim();
54
+ if (txHash) record.txHash = txHash;
55
+ return record;
56
+ }
57
+
58
+ /** 40-hex lowercase address body (no 0x) from a deployment address. */
59
+ export function univocityAddrHex(record: Pick<DeploymentRecord, "imutableUnivocity">): string {
60
+ const hex = record.imutableUnivocity.trim().toLowerCase().replace(/^0x/, "");
61
+ if (!/^[0-9a-f]{40}$/.test(hex)) {
62
+ throw new Error("imutableUnivocity is not a 20-byte hex address");
63
+ }
64
+ return hex;
65
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Runtime-neutral file I/O for the CLI.
3
+ *
4
+ * The CLI ships to npm as a Node program (`npx @forestrie/forestrie-cli`)
5
+ * as well as a Bun-compiled static binary, so nothing under `src/` may
6
+ * reach for `Bun.*`. These helpers are the `node:fs` equivalents of the
7
+ * `Bun.file` / `Bun.write` / `Bun.stdin` calls the commands used to make,
8
+ * with the one behaviour that was load-bearing preserved: `Bun.write`
9
+ * creates missing parent directories, `fs.writeFile` does not.
10
+ */
11
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+
14
+ /** True for a `node:fs` "no such file or directory" rejection. */
15
+ function isNotFound(error: unknown): boolean {
16
+ return (
17
+ typeof error === "object" &&
18
+ error !== null &&
19
+ (error as { code?: unknown }).code === "ENOENT"
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Write `data` to `filePath`, creating parent directories first
25
+ * (`Bun.write` parity — several commands write into `--out` paths whose
26
+ * directory the caller has not made).
27
+ */
28
+ export async function writeOutputFile(
29
+ filePath: string,
30
+ data: string | Uint8Array,
31
+ ): Promise<void> {
32
+ const dir = path.dirname(path.resolve(filePath));
33
+ await mkdir(dir, { recursive: true });
34
+ await writeFile(filePath, data);
35
+ }
36
+
37
+ /**
38
+ * Read `filePath` as UTF-8, raising `notFoundMessage` (not a raw ENOENT)
39
+ * when it does not exist — the message flags surface to the user.
40
+ */
41
+ export async function readTextFile(
42
+ filePath: string,
43
+ notFoundMessage: string,
44
+ ): Promise<string> {
45
+ try {
46
+ return await readFile(filePath, "utf8");
47
+ } catch (error) {
48
+ if (isNotFound(error)) {
49
+ throw new Error(notFoundMessage);
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /** As `readTextFile`, but returns the raw bytes. */
56
+ export async function readBytesFile(
57
+ filePath: string,
58
+ notFoundMessage: string,
59
+ ): Promise<Uint8Array> {
60
+ try {
61
+ const buffer = await readFile(filePath);
62
+ return new Uint8Array(
63
+ buffer.buffer,
64
+ buffer.byteOffset,
65
+ buffer.byteLength,
66
+ );
67
+ } catch (error) {
68
+ if (isNotFound(error)) {
69
+ throw new Error(notFoundMessage);
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ /** Drain stdin to bytes (`Bun.stdin.arrayBuffer()` equivalent). */
76
+ export async function readStdinBytes(): Promise<Uint8Array> {
77
+ const chunks: Uint8Array[] = [];
78
+ for await (const chunk of process.stdin) {
79
+ chunks.push(
80
+ typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk),
81
+ );
82
+ }
83
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
84
+ const out = new Uint8Array(total);
85
+ let offset = 0;
86
+ for (const chunk of chunks) {
87
+ out.set(chunk, offset);
88
+ offset += chunk.byteLength;
89
+ }
90
+ return out;
91
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Local bridge until `@forestrie/grant-builder` publishes `dataLogExtendFlags()`.
3
+ *
4
+ * The published grant-builder ships `dataLogCreateExtendFlags()` (byte 3 =
5
+ * `GF_CREATE|GF_EXTEND` = 0x03) but not the extend-only shape a statement
6
+ * *writer* grant needs. Constructing the 8-byte wire bitmap here is a plain
7
+ * Uint8Array literal, NOT copied package logic — see ADR-0052 / plan-2607-21.
8
+ * Drop this module once grant-builder exports `dataLogExtendFlags()`.
9
+ */
10
+
11
+ /**
12
+ * Extend-only data-log writer grant flags: byte 3 = `GF_EXTEND` (0x02, no
13
+ * `GF_CREATE`), byte 7 = `GF_DATA_LOG` (0x02). Mirrors grant-builder's byte
14
+ * layout (grant-flags.ts): byte 3 carries the capability bits, byte 7 the
15
+ * log-class bits.
16
+ */
17
+ export function dataLogExtendFlags(): Uint8Array {
18
+ return Uint8Array.from([0, 0, 0, 0x02, 0, 0, 0, 0x02]);
19
+ }