@crewhaus/target-onchain-game 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 +56 -0
- package/src/index.ts +36 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/target-onchain-game",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Codegen for the §47 `onchain-game` perceive-act-perceive loop. Emits a daemon that reads game state, asks the model for a move, broadcasts the move, awaits confirmation, and repeats.",
|
|
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
|
@@ -79,6 +79,31 @@ describe("emitOnchainGame — happy path", () => {
|
|
|
79
79
|
expect(c).toContain('"allowedContracts":["tictactoe"]');
|
|
80
80
|
expect(c).toContain('"simulationRequired":true');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
// #151 activation — the single game contract is resolved into the policy's
|
|
84
|
+
// contractId -> address map so the wallet-engine binds tx.to.
|
|
85
|
+
test("populates transaction_policy.contractAddresses from the game contract", () => {
|
|
86
|
+
const bundle = emitOnchainGame(baseIr());
|
|
87
|
+
const c = bundle.files[0]?.content ?? "";
|
|
88
|
+
expect(c).toContain('"contractAddresses":{"tictactoe":"0xgame"}');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// #159 (CWE-798) — a kms:// / hsm:// key handle is a permitted literal.
|
|
92
|
+
test("renders a kms:// keyRef handle verbatim", () => {
|
|
93
|
+
const bundle = emitOnchainGame(
|
|
94
|
+
baseIr({
|
|
95
|
+
wallet: {
|
|
96
|
+
id: "player",
|
|
97
|
+
chainId: "base-sepolia",
|
|
98
|
+
custody: "kms",
|
|
99
|
+
signingPolicy: "automated",
|
|
100
|
+
keyRef: { kind: "literal", value: "kms://aws/player-key" },
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const c = bundle.files[0]?.content ?? "";
|
|
105
|
+
expect(c).toContain('"kms://aws/player-key"');
|
|
106
|
+
});
|
|
82
107
|
});
|
|
83
108
|
|
|
84
109
|
describe("emitOnchainGame — validation", () => {
|
|
@@ -120,4 +145,35 @@ describe("emitOnchainGame — validation", () => {
|
|
|
120
145
|
}),
|
|
121
146
|
).toThrow(/moveTimeoutMs/);
|
|
122
147
|
});
|
|
148
|
+
|
|
149
|
+
// #159 (CWE-798) — a literal (raw hex private key) wallet keyRef must not be
|
|
150
|
+
// baked into the emitted artifact.
|
|
151
|
+
test("rejects a literal (hex private key) wallet keyRef", () => {
|
|
152
|
+
expect(() =>
|
|
153
|
+
emitOnchainGame(
|
|
154
|
+
baseIr({
|
|
155
|
+
wallet: {
|
|
156
|
+
id: "player",
|
|
157
|
+
chainId: "base-sepolia",
|
|
158
|
+
custody: "local",
|
|
159
|
+
signingPolicy: "automated",
|
|
160
|
+
keyRef: {
|
|
161
|
+
kind: "literal",
|
|
162
|
+
value: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
).toThrow(TargetEmitError);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("emitOnchainGame — spec-name codegen injection (#147)", () => {
|
|
172
|
+
test("a crafted name cannot break out of the header comment", () => {
|
|
173
|
+
const evil = "safe */ globalThis.__PWNED_GAME__ = 1; /*\nmore";
|
|
174
|
+
const content = emitOnchainGame(baseIr({ name: evil })).files[0]?.content ?? "";
|
|
175
|
+
expect(content).toMatch(/^\/\/ Compiled from spec:/m);
|
|
176
|
+
expect(content).not.toMatch(/^globalThis\.__PWNED_GAME__/m);
|
|
177
|
+
expect(content).not.toContain("*/\nglobalThis");
|
|
178
|
+
});
|
|
123
179
|
});
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,24 @@ function renderSecretRef(ref: IrSecretRef): string {
|
|
|
49
49
|
)}); })()`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* #159 (CWE-798) — a wallet signing key must never be a bare literal in
|
|
54
|
+
* the emitted DO-NOT-EDIT artifact. The compiler's `lowerWalletKeyRef`
|
|
55
|
+
* already rejects literal keyRefs at lower time; this is the
|
|
56
|
+
* defense-in-depth guard at the emit boundary (e.g. for a hand-built IR
|
|
57
|
+
* that bypassed the lowerer). Env refs pass; only `kms://` / `hsm://`
|
|
58
|
+
* key handles are permitted as literals.
|
|
59
|
+
*/
|
|
60
|
+
const KEY_HANDLE_RE = /^(kms|hsm):\/\/.+/;
|
|
61
|
+
function renderWalletKeyRef(walletId: string, ref: IrSecretRef): string {
|
|
62
|
+
if (ref.kind === "literal" && !KEY_HANDLE_RE.test(ref.value)) {
|
|
63
|
+
throw new TargetEmitError(
|
|
64
|
+
`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.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return renderSecretRef(ref);
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
export function emitOnchainGame(ir: IrChainGameV0): Bundle {
|
|
53
71
|
if (ir.chain.id !== ir.wallet.chainId) {
|
|
54
72
|
throw new TargetEmitError(
|
|
@@ -73,15 +91,26 @@ export function emitOnchainGame(ir: IrChainGameV0): Bundle {
|
|
|
73
91
|
: `{ kind: ${JSON.stringify(ir.chain.finality.kind)} }`;
|
|
74
92
|
|
|
75
93
|
const policy = ir.transactionPolicy;
|
|
94
|
+
// #151 — resolve the declared game contract (id -> address) into the policy
|
|
95
|
+
// so the wallet-engine can bind `tx.to` to the address registered for the
|
|
96
|
+
// claimed contractId. A game is single-contract, so the map carries exactly
|
|
97
|
+
// the one entry the move-broadcast flow can target.
|
|
98
|
+
const contractAddresses: Record<string, string> = {
|
|
99
|
+
[ir.game.contract.id]: ir.game.contract.address,
|
|
100
|
+
};
|
|
76
101
|
const policyLiteral = JSON.stringify({
|
|
77
102
|
defaultWriteApproval: policy.defaultWriteApproval,
|
|
78
103
|
allowedContracts: policy.allowedContracts,
|
|
79
104
|
simulationRequired: policy.simulationRequired,
|
|
80
105
|
...(policy.maxValueUsd !== undefined ? { maxValueUsd: policy.maxValueUsd } : {}),
|
|
106
|
+
...(policy.maxValueWei !== undefined ? { maxValueWei: policy.maxValueWei } : {}),
|
|
107
|
+
contractAddresses,
|
|
81
108
|
});
|
|
82
109
|
|
|
83
110
|
const walletKeyRef =
|
|
84
|
-
ir.wallet.keyRef !== undefined
|
|
111
|
+
ir.wallet.keyRef !== undefined
|
|
112
|
+
? `, keyRef: ${renderWalletKeyRef(ir.wallet.id, ir.wallet.keyRef)}`
|
|
113
|
+
: "";
|
|
85
114
|
|
|
86
115
|
const objectiveField =
|
|
87
116
|
ir.game.objective !== undefined ? `\n objective: ${escapeJsonString(ir.game.objective)},` : "";
|
|
@@ -95,11 +124,12 @@ export function emitOnchainGame(ir: IrChainGameV0): Bundle {
|
|
|
95
124
|
const instructions = escapeJsonString(ir.agent.instructions);
|
|
96
125
|
const name = escapeJsonString(ir.name);
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
// Header is a `//` line comment using the JSON-escaped name: a raw ir.name in
|
|
128
|
+
// a block comment lets a crafted spec.name containing `*/` (or a newline) break
|
|
129
|
+
// out of the comment and inject top-level code — RCE on build/run (#147).
|
|
130
|
+
const content = `// Generated by @crewhaus/target-onchain-game (§47 onchain-game).
|
|
131
|
+
// Compiled from spec: ${name}
|
|
132
|
+
// DO NOT EDIT — re-run \`crewhaus compile\` to regenerate.
|
|
103
133
|
import { classifyBoundary } from "@crewhaus/boundary-classifier";
|
|
104
134
|
import { createEvmAdapter } from "@crewhaus/chain-adapter-evm";
|
|
105
135
|
|