@crewhaus/target-onchain 0.1.0 → 0.1.2
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 +8 -13
- package/src/index.test.ts +114 -0
- package/src/index.ts +36 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/target-onchain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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.
|
|
16
|
-
"@crewhaus/infra-utils": "0.
|
|
17
|
-
"@crewhaus/ir": "0.
|
|
15
|
+
"@crewhaus/errors": "0.1.2",
|
|
16
|
+
"@crewhaus/infra-utils": "0.1.2",
|
|
17
|
+
"@crewhaus/ir": "0.1.2"
|
|
18
18
|
},
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"author": {
|
|
21
21
|
"name": "Max Meier",
|
|
22
|
-
"email": "max@
|
|
23
|
-
"url": "https://
|
|
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": "
|
|
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 =
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|