@crewhaus/target-onchain 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/target-onchain",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Codegen for the §47 `onchain` event-driven daemon. Emits a self-contained agent.ts that subscribes to chain triggers, classifies inbound events, dedupes by tx-hash + log-index, and runs one agent turn per event.",
6
6
  "main": "src/index.ts",
@@ -12,15 +12,15 @@
12
12
  "test": "bun test src"
13
13
  },
14
14
  "dependencies": {
15
- "@crewhaus/errors": "0.1.1",
16
- "@crewhaus/infra-utils": "0.1.1",
17
- "@crewhaus/ir": "0.1.1"
15
+ "@crewhaus/errors": "0.1.3",
16
+ "@crewhaus/infra-utils": "0.1.3",
17
+ "@crewhaus/ir": "0.1.3"
18
18
  },
19
19
  "license": "Apache-2.0",
20
20
  "author": {
21
21
  "name": "Max Meier",
22
- "email": "max@studiomax.io",
23
- "url": "https://studiomax.io"
22
+ "email": "max@crewhaus.ai",
23
+ "url": "https://crewhaus.ai"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",
@@ -32,12 +32,7 @@
32
32
  "url": "https://github.com/crewhaus/factory/issues"
33
33
  },
34
34
  "publishConfig": {
35
- "access": "restricted"
35
+ "access": "public"
36
36
  },
37
- "files": [
38
- "src",
39
- "README.md",
40
- "LICENSE",
41
- "NOTICE"
42
- ]
37
+ "files": ["src", "README.md", "LICENSE", "NOTICE"]
43
38
  }
package/src/index.test.ts CHANGED
@@ -133,6 +133,105 @@ describe("emitOnchain — happy path", () => {
133
133
  expect(c).toContain('"allowedContracts":["treasury"]');
134
134
  expect(c).toContain('"maxValueUsd":5000');
135
135
  });
136
+
137
+ // SECURITY: maxValueWei is the ONLY native-value cap wallet-engine can
138
+ // enforce (maxValueUsd hard-throws with no oracle); it must reach the
139
+ // emitted TRANSACTION_POLICY literal so the ceiling is actually applied.
140
+ test("renders transaction_policy.maxValueWei into the emitted literal", () => {
141
+ const bundle = emitOnchain(
142
+ baseIr({
143
+ transactionPolicy: {
144
+ defaultWriteApproval: "policy",
145
+ allowedContracts: [],
146
+ simulationRequired: true,
147
+ maxValueWei: "1000000000000000000",
148
+ },
149
+ }),
150
+ );
151
+ const c = bundle.files[0]?.content ?? "";
152
+ expect(c).toContain('"maxValueWei":"1000000000000000000"');
153
+ });
154
+
155
+ // #151 activation — the policy must carry a resolved contractId -> address
156
+ // map so the wallet-engine can bind tx.to to the declared contract.
157
+ test("populates transaction_policy.contractAddresses from declared contracts[]", () => {
158
+ const bundle = emitOnchain(
159
+ baseIr({
160
+ contracts: [
161
+ { id: "treasury", chainId: "base-mainnet", address: "0xTREASURY", abiRef: "abi://safe" },
162
+ { id: "vault", chainId: "base-mainnet", address: "0xVAULT", abiRef: "abi://erc20" },
163
+ ],
164
+ }),
165
+ );
166
+ const c = bundle.files[0]?.content ?? "";
167
+ expect(c).toContain('"contractAddresses":{"treasury":"0xTREASURY","vault":"0xVAULT"}');
168
+ });
169
+
170
+ test("omits contractAddresses when no contracts are declared", () => {
171
+ const bundle = emitOnchain(baseIr({ contracts: [] }));
172
+ const c = bundle.files[0]?.content ?? "";
173
+ expect(c).not.toContain("contractAddresses");
174
+ });
175
+
176
+ // #159 (CWE-798) — a wallet keyRef that is an env ref or a kms:// / hsm://
177
+ // handle is fine; a bare literal (esp. a raw hex private key) must not be
178
+ // baked into the emitted artifact.
179
+ test("renders a wallet env-ref keyRef as a process.env lookup", () => {
180
+ const bundle = emitOnchain(
181
+ baseIr({
182
+ wallets: [
183
+ {
184
+ id: "treasurer",
185
+ chainId: "base-mainnet",
186
+ custody: "kms",
187
+ signingPolicy: "policy-gated",
188
+ keyRef: { kind: "env", name: "TREASURER_KEY" },
189
+ },
190
+ ],
191
+ }),
192
+ );
193
+ const c = bundle.files[0]?.content ?? "";
194
+ expect(c).toContain('process.env["TREASURER_KEY"]');
195
+ });
196
+
197
+ test("renders a kms:// keyRef handle verbatim", () => {
198
+ const bundle = emitOnchain(
199
+ baseIr({
200
+ wallets: [
201
+ {
202
+ id: "treasurer",
203
+ chainId: "base-mainnet",
204
+ custody: "kms",
205
+ signingPolicy: "policy-gated",
206
+ keyRef: { kind: "literal", value: "kms://aws/treasurer-key" },
207
+ },
208
+ ],
209
+ }),
210
+ );
211
+ const c = bundle.files[0]?.content ?? "";
212
+ expect(c).toContain('"kms://aws/treasurer-key"');
213
+ });
214
+
215
+ test("rejects a literal (hex private key) wallet keyRef", () => {
216
+ expect(() =>
217
+ emitOnchain(
218
+ baseIr({
219
+ wallets: [
220
+ {
221
+ id: "treasurer",
222
+ chainId: "base-mainnet",
223
+ custody: "local",
224
+ signingPolicy: "automated",
225
+ keyRef: {
226
+ kind: "literal",
227
+ value: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
228
+ },
229
+ },
230
+ ],
231
+ }),
232
+ ),
233
+ ).toThrow(TargetEmitError);
234
+ });
136
235
  });
137
236
 
138
237
  describe("emitOnchain — validation", () => {
@@ -154,3 +253,18 @@ describe("emitOnchain — validation", () => {
154
253
  ).toThrow(/not declared in chains/);
155
254
  });
156
255
  });
256
+
257
+ describe("emitOnchain — spec-name codegen injection (#147)", () => {
258
+ test("a crafted name cannot break out of the header comment", () => {
259
+ // Block-comment escape: a `*/` (plus a newline) in the name would, with a
260
+ // raw `/* … */` header, terminate the comment and inject top-level code.
261
+ const evil = "safe */ globalThis.__PWNED_ONCHAIN__ = 1; /*\nmore";
262
+ const content = emitOnchain(baseIr({ name: evil })).files[0]?.content ?? "";
263
+ // Header is now a `//` line comment using the JSON-escaped name.
264
+ expect(content).toMatch(/^\/\/ Compiled from spec:/m);
265
+ // The payload only ever appears inside the escaped SPEC_NAME string and the
266
+ // comment — never as a top-level statement at column 0.
267
+ expect(content).not.toMatch(/^globalThis\.__PWNED_ONCHAIN__/m);
268
+ expect(content).not.toContain("*/\nglobalThis");
269
+ });
270
+ });
package/src/index.ts CHANGED
@@ -50,6 +50,24 @@ function renderSecretRef(ref: IrSecretRef): string {
50
50
  )}); })()`;
51
51
  }
52
52
 
53
+ /**
54
+ * #159 (CWE-798) — a wallet signing key must never be a bare literal in
55
+ * the emitted DO-NOT-EDIT artifact. The compiler's `lowerWalletKeyRef`
56
+ * already rejects literal keyRefs at lower time; this is the
57
+ * defense-in-depth guard at the emit boundary (e.g. for a hand-built IR
58
+ * that bypassed the lowerer). Env refs pass; only `kms://` / `hsm://`
59
+ * key handles are permitted as literals.
60
+ */
61
+ const KEY_HANDLE_RE = /^(kms|hsm):\/\/.+/;
62
+ function renderWalletKeyRef(walletId: string, ref: IrSecretRef): string {
63
+ if (ref.kind === "literal" && !KEY_HANDLE_RE.test(ref.value)) {
64
+ throw new TargetEmitError(
65
+ `wallet "${walletId}" keyRef is a literal signing key; refusing to embed it in the generated bundle. Use an environment reference or a kms:// / hsm:// handle.`,
66
+ );
67
+ }
68
+ return renderSecretRef(ref);
69
+ }
70
+
53
71
  function renderChain(chain: IrChainV0["chains"][number]): string {
54
72
  const rpcs = chain.rpcUrls.map(renderSecretRef).join(", ");
55
73
  const finality =
@@ -68,7 +86,8 @@ function renderChain(chain: IrChainV0["chains"][number]): string {
68
86
  }
69
87
 
70
88
  function renderWallet(w: IrChainV0["wallets"][number]): string {
71
- const keyRefStr = w.keyRef !== undefined ? `,\n keyRef: ${renderSecretRef(w.keyRef)}` : "";
89
+ const keyRefStr =
90
+ w.keyRef !== undefined ? `,\n keyRef: ${renderWalletKeyRef(w.id, w.keyRef)}` : "";
72
91
  return [
73
92
  " {",
74
93
  ` id: ${JSON.stringify(w.id)},`,
@@ -120,11 +139,21 @@ export function emitOnchain(ir: IrChainV0): Bundle {
120
139
  }
121
140
 
122
141
  const policy = ir.transactionPolicy;
142
+ // #151 — resolve the declared contracts[] (id -> address) into the policy so
143
+ // the wallet-engine can bind `tx.to` to the address registered for a claimed
144
+ // contractId. Without this map a whitelisted id can still be pointed at an
145
+ // arbitrary address.
146
+ const contractAddresses: Record<string, string> = {};
147
+ for (const c of ir.contracts) {
148
+ contractAddresses[c.id] = c.address;
149
+ }
123
150
  const policyLiteral = JSON.stringify({
124
151
  defaultWriteApproval: policy.defaultWriteApproval,
125
152
  allowedContracts: policy.allowedContracts,
126
153
  simulationRequired: policy.simulationRequired,
127
154
  ...(policy.maxValueUsd !== undefined ? { maxValueUsd: policy.maxValueUsd } : {}),
155
+ ...(policy.maxValueWei !== undefined ? { maxValueWei: policy.maxValueWei } : {}),
156
+ ...(ir.contracts.length > 0 ? { contractAddresses } : {}),
128
157
  });
129
158
 
130
159
  const chains = ir.chains.map(renderChain).join(",\n");
@@ -135,11 +164,12 @@ export function emitOnchain(ir: IrChainV0): Bundle {
135
164
  const instructions = escapeJsonString(ir.agent.instructions);
136
165
  const name = escapeJsonString(ir.name);
137
166
 
138
- const content = `/**
139
- * Generated by @crewhaus/target-onchain (§47 onchain daemon).
140
- * Compiled from spec: ${ir.name}
141
- * DO NOT EDIT re-run \`crewhaus compile\` to regenerate.
142
- */
167
+ // Header is a `//` line comment using the JSON-escaped name: a raw ir.name in
168
+ // a block comment lets a crafted spec.name containing `*/` (or a newline) break
169
+ // out of the comment and inject top-level code — RCE on build/run (#147).
170
+ const content = `// Generated by @crewhaus/target-onchain (§47 onchain daemon).
171
+ // Compiled from spec: ${name}
172
+ // DO NOT EDIT — re-run \`crewhaus compile\` to regenerate.
143
173
  import { classifyBoundary } from "@crewhaus/boundary-classifier";
144
174
  import { createEvmAdapter } from "@crewhaus/chain-adapter-evm";
145
175