@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/target-onchain-game",
3
- "version": "0.1.0",
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.0.0",
16
- "@crewhaus/infra-utils": "0.0.0",
17
- "@crewhaus/ir": "0.0.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@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
@@ -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 ? `, keyRef: ${renderSecretRef(ir.wallet.keyRef)}` : "";
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
- const content = `/**
99
- * Generated by @crewhaus/target-onchain-game (§47 onchain-game).
100
- * Compiled from spec: ${ir.name}
101
- * DO NOT EDIT re-run \`crewhaus compile\` to regenerate.
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