@flayerlabs/gamemode-cli 0.3.1 → 0.4.1

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.
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: run-a-game-server
3
+ description: Connects a game with its own authoritative multiplayer server to Flaunch Game Mode with @flayerlabs/gamemode-gate. Use when a game runs its own realtime server or region fleet (custom netcode, Colyseus, Socket.IO, raw WebSockets) and needs the gate, /config, join tickets, awards and the platform submission to line up. Do not use for rules-based games with no server — $build-game-mode covers those.
4
+ ---
5
+
6
+ # Run a game server behind a Game Mode gate
7
+
8
+ You have a game whose gameplay lives on your own server. The platform never hosts or relays your
9
+ realtime traffic: you run the server and the gate, and the platform gives your hosted game a strict
10
+ policy that only reaches what you declared. This skill exists because that declaration lives in
11
+ three places that must agree, and because the launch form refuses a gate that does not announce
12
+ itself. Work through the sections in order; each ends with a check you can run.
13
+
14
+ ## Know the three services and who hosts them
15
+
16
+ 1. Your game bundle: static files you zip and submit. The platform hosts these on Moongate and
17
+ serves them from `https://<deploy-id>.games.moongate.com`. The deploy id is derived from the
18
+ bundle's content, so every re-upload changes the hostname. Never hardcode it; never expect it
19
+ to be stable.
20
+ 2. Your gate: one process you deploy (`createGameServerGate()` from `@flayerlabs/gamemode-gate`).
21
+ It owns wallet sessions, the points ledger, spend authorisations and settlement. One process
22
+ serves one chain.
23
+ 3. Your game server(s): your realtime processes, up to four public HTTPS origins. Gameplay,
24
+ scoring decisions and anti-abuse are yours; points move only through the gate's award route.
25
+
26
+ ## Declare the same origins in all three places
27
+
28
+ The reviewed game-server origins must match exactly — same scheme, same host, no path — in:
29
+
30
+ 1. The platform submission form's game server addresses field (one per line). This becomes your
31
+ hosted game's `connect-src`: the browser can only reach origins listed here or your gate.
32
+ 2. `gameServerOrigins` in your `createGameServerGate()` options. This puts each origin into the
33
+ set of valid join-ticket audiences.
34
+ 3. The `joinTicket(origin)` call in your game client and the `audience` your server verifies.
35
+
36
+ A mismatch fails at a different layer each time: missing from (1) and the fetch dies on CSP with
37
+ "Refused to connect"; missing from (2) and the fetch succeeds but the ticket does not verify;
38
+ wrong in (3) and verification rejects every player. At most four origins, exact HTTPS only —
39
+ wildcards, paths and plain HTTP are refused. Rotating regions? Put them behind stable hostnames;
40
+ the list is not meant to churn.
41
+
42
+ ## Serve /config, or the launch form refuses your gate
43
+
44
+ The launch page reads `GET /config` from your gate before it lets anyone launch a coin through
45
+ your game. `startGate()` serves it automatically; `createGameServerGate()` serves it only when
46
+ you pass `announce`. Without it the form reports your server "isn't answering — it may be offline,
47
+ or built before the current Game Mode SDK". Pass it:
48
+
49
+ ```ts
50
+ const app = createGameServerGate({
51
+ // ...pool, sessions, claims, discovery, settlement, gameId, awardToken...
52
+ gameServerOrigins: ['https://us.game.example.com', 'https://eu.game.example.com'],
53
+ announce: () => ({
54
+ chainId: 84532,
55
+ contracts: {
56
+ positionManager: '0x4E7cB1e6800a7B297B38BddcecAF9Ca5b6616FDC',
57
+ spendGatedCalculator: '0x8cbbE6b4cFA5Ccf68399Dbf1429d91A21097ebA5',
58
+ },
59
+ signer: SIGNER_ADDRESS, // the address of your gate's signing key
60
+ settler: SIGNER_ADDRESS,
61
+ walletCapWei: '25000000000000000',
62
+ roundDurationMs: 90_000,
63
+ minLobbyLeadMs: 60_000,
64
+ gateEndsAtGraceS: 10,
65
+ flaunchVariant: 'legacy11',
66
+ requiresEoa: false,
67
+ accepting: true,
68
+ publicLaunchesOpen: true,
69
+ privateLaunches: false,
70
+ }),
71
+ })
72
+ ```
73
+
74
+ The addresses above are Base Sepolia (chain 84532), read back from the chain. `signer` is what a
75
+ launch writes into its pool as the trusted signer — announce an address you do not hold and every
76
+ coin launched through your game is unplayable. `walletCapWei` must cover a flawless round
77
+ (`maxPointsPerPlayer` times the wei value of a point) or boot refuses.
78
+
79
+ Check: `curl https://<your-gate>/config` returns JSON with your `chainId` and `signer`, and
80
+ `curl https://<your-gate>/health` returns `{"ok":true}`.
81
+
82
+ ## Point the gate's environment at the right origins
83
+
84
+ The two origin settings developers most often get backwards:
85
+
86
+ - Allowed origins (browser CORS and the WebSocket `Origin` check) name where your GAME is served
87
+ from — the Moongate origin, not the flaunch page. Because the Moongate hostname changes per
88
+ upload, use the single-label wildcard: `https://*.games.moongate.com`.
89
+ - The sign-in domain names the page the player is looking at — the flaunch site embedding your
90
+ game — not your game and not your gate.
91
+
92
+ Generate the signing key, session secret (32+ characters) and award token (32+ characters,
93
+ `openssl rand -hex 32`) fresh per environment. The award token lives on the gate and your game
94
+ server only; it never reaches a browser or a zip.
95
+
96
+ ## Wire the ticket handshake
97
+
98
+ Browser, after `joinEconomy()`:
99
+
100
+ ```ts
101
+ const gameServerOrigin = 'https://us.game.example.com' // one reviewed origin, chosen by you
102
+ const { ticket } = await gameMode.joinTicket(gameServerOrigin)
103
+ socket.send(JSON.stringify({ type: 'join', ticket })) // first message; never in the URL
104
+ ```
105
+
106
+ Your server, before accepting any gameplay frame:
107
+
108
+ ```ts
109
+ const playerJoin = verifyPlayerJoinTicket(message.ticket, {
110
+ awardToken,
111
+ gameId,
112
+ roundId,
113
+ audience: gameServerOrigin, // the exact origin the browser connected to
114
+ })
115
+ if (!playerJoin || usedTicketIds.has(playerJoin.jti)) return socket.close(4401, 'invalid join ticket')
116
+ usedTicketIds.add(playerJoin.jti)
117
+ ```
118
+
119
+ Also check the WebSocket `Origin` header against the Moongate wildcard, keep one live connection
120
+ per wallet, and treat every later frame as hostile input. Award points only through
121
+ `POST /internal/rounds/:id/awards` with the award token — see the gate README for the routes.
122
+
123
+ ## Strip development fallbacks from the production bundle
124
+
125
+ A production zip that pings `localhost` ports, probes a region list on boot, or falls back to a
126
+ dev wallet endpoint will emit CSP errors on every load and can hang the game on a spinner when the
127
+ fallback wins a race. Gate development-only network paths behind a build flag so the submitted
128
+ bundle contains none of them.
129
+
130
+ ## Submit, then verify against the real policy
131
+
132
+ Submit the zip with the gate option on (your gate origin) and the game server addresses filled in.
133
+ The form live-probes your gate's `/health` at submission, so deploy the gate first. Localhost
134
+ cannot rehearse the deployed CSP: after hosting, open the deployed game and confirm every declared
135
+ origin connects and an undeclared one is blocked. Then launch a fresh test coin through the room
136
+ link — a coin launched before a registration change stays bound to what it was launched with.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Run a Game Server"
3
+ short_description: "Wire an authoritative multiplayer server into Game Mode"
4
+ default_prompt: "Use $run-a-game-server to connect this game's own server to its Game Mode gate, /config, join tickets and platform submission."
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5D;AA+BD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW7D"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5D;AA0CD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW7D"}
package/dist/scaffold.js CHANGED
@@ -37,6 +37,12 @@ async function readCreatorSkill() {
37
37
  async function readCreatorSkillMetadata() {
38
38
  return readAsset('./build-game-mode/agents/openai.yaml', '../../../.agents/skills/build-game-mode/agents/openai.yaml');
39
39
  }
40
+ async function readServerSkill() {
41
+ return readAsset('./run-a-game-server/SKILL.md', '../../../.agents/skills/run-a-game-server/SKILL.md');
42
+ }
43
+ async function readServerSkillMetadata() {
44
+ return readAsset('./run-a-game-server/agents/openai.yaml', '../../../.agents/skills/run-a-game-server/agents/openai.yaml');
45
+ }
40
46
  async function readAsset(packaged, source) {
41
47
  try {
42
48
  return await readFile(new URL(packaged, import.meta.url), 'utf8');
@@ -67,16 +73,19 @@ const files = async (name) => {
67
73
  type: 'module',
68
74
  scripts: {
69
75
  dev: 'vite',
76
+ gate: 'tsx src/gate.ts',
70
77
  test: 'gamemode check src/game/rules.ts && vitest run',
71
78
  check: 'gamemode check src/game/rules.ts',
72
79
  typecheck: 'tsc --noEmit',
73
80
  },
74
81
  dependencies: {
75
82
  '@flayerlabs/gamemode-client': sdk,
83
+ '@flayerlabs/gamemode-gate': sdk,
76
84
  '@flayerlabs/gamemode-spec': sdk,
77
85
  },
78
86
  devDependencies: {
79
87
  '@flayerlabs/gamemode-cli': sdk,
88
+ tsx: '^4.23.12',
80
89
  typescript: '^5.7.2',
81
90
  vite: '^5.4.21',
82
91
  vitest: '^2.1.8',
@@ -106,6 +115,28 @@ pnpm test
106
115
  pnpm typecheck
107
116
  \`\`\`
108
117
 
118
+ ## Deploy your gate
119
+
120
+ Game Mode needs your game to bring its own round server — a gate — and its public https origin is
121
+ exactly the **Gate URL** you enter when you submit the game at flaunch.gg/game-mode/create. Flaunch
122
+ never runs a gate for you and never holds your signing key.
123
+
124
+ \`src/gate.ts\` is the whole entrypoint; \`.env.example\` is the whole contract. On Railway:
125
+
126
+ 1. Create a project from this repo (\`railpack.json\` names the build and start commands) and
127
+ attach a Postgres whose URL lands in \`DATABASE_URL\`.
128
+ 2. Set the variables from \`.env.example\`, generating the key and secret fresh.
129
+ 3. Give the service a public domain, set it as \`GATE_ORIGIN\`, and smoke-test it:
130
+
131
+ \`\`\`bash
132
+ curl https://<your-gate>/health # { "ok": true }
133
+ curl https://<your-gate>/config # chainId must be the chain you meant; note "signer"
134
+ \`\`\`
135
+
136
+ A gate that boots is a gate whose signatures a swap can verify — boot refuses loudly on a
137
+ wrong-chain RPC, a demo key, or an economy your rewardBounds cannot honour. The full environment
138
+ table lives in \`@flayerlabs/gamemode-gate\`'s DEPLOY.md.
139
+
109
140
  ## Work with an agent
110
141
 
111
142
  Ask the agent to read \`AGENTS.md\` before it edits the game. Use \`$build-game-mode\` when the
@@ -131,6 +162,8 @@ For the rules and client workflow, read the
131
162
  `,
132
163
  '.agents/skills/build-game-mode/SKILL.md': await readCreatorSkill(),
133
164
  '.agents/skills/build-game-mode/agents/openai.yaml': await readCreatorSkillMetadata(),
165
+ '.agents/skills/run-a-game-server/SKILL.md': await readServerSkill(),
166
+ '.agents/skills/run-a-game-server/agents/openai.yaml': await readServerSkillMetadata(),
134
167
  'src/game/rules.ts': `import { defineGame, type Decision, type PlayerId, type Refusal } from '@flayerlabs/gamemode-spec';
135
168
  import { scheduleWithin } from '@flayerlabs/gamemode-spec/schedule';
136
169
 
@@ -321,11 +354,72 @@ export interface PlayerView {
321
354
  wasRight: boolean | null;
322
355
  }
323
356
  `,
324
- 'src/play.ts': `import { createMockRoom, type Snapshot } from '@flayerlabs/gamemode-client';
357
+ 'src/gate.ts': `import { startGate } from '@flayerlabs/gamemode-gate';
358
+ import { rules, type Config } from './game/rules.js';
359
+
360
+ /**
361
+ * Your game's own server, as a deployable process.
362
+ *
363
+ * A gate holds YOUR signing key and runs YOUR rules: a creator who launches a coin through your
364
+ * game writes your signer's address into their pool, and the chain trusts it for that pool alone.
365
+ * Flaunch never holds the key. \`startGate\` does the wiring — database migrations, the chain
366
+ * address book, boot refusals for anything misconfigured, /config for launch tooling — and this
367
+ * file stays what it should be: your round's economy, stated once.
368
+ *
369
+ * Everything environment-shaped comes from the environment; see .env.example.
370
+ */
371
+ const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
372
+
373
+ const { port, signer } = await startGate(rules, config);
374
+
375
+ // The signer address is what a launch must name for this gate to adopt it — it belongs in the
376
+ // deploy log, where an operator can read it back without a debugger.
377
+ console.log(\`[gate] listening on \${port} — signer \${signer}\`);
378
+ `,
379
+ '.env.example': `# The environment contract for \`pnpm gate\` (full table: @flayerlabs/gamemode-gate DEPLOY.md).
380
+ # Generate SIGNER_PRIVATE_KEY and SESSION_SECRET fresh for every environment — a key shared
381
+ # between two environments lets one gate's bug mint authorisations against the other's pools.
382
+
383
+ # The one chain this process serves. 84532 (Base Sepolia) is in the address book.
384
+ CHAIN_ID=84532
385
+ # An RPC endpoint for that chain; boot checks its eth_chainId matches.
386
+ RPC_URL=https://sepolia.base.org
387
+ # Your key, generated by you, never shared. openssl rand -hex 32 (prefix with 0x).
388
+ SIGNER_PRIVATE_KEY=
389
+ # Postgres. Migrations run at boot.
390
+ DATABASE_URL=
391
+ # At least 32 characters. openssl rand -hex 32
392
+ SESSION_SECRET=
393
+ # The page the player is looking at when they sign in — the embedding site, not this gate.
394
+ SIGN_IN_DOMAIN=flaunch.gg
395
+ # This gate's own public https origin — the exact value you submit as the Gate URL.
396
+ GATE_ORIGIN=
397
+ # Comma-separated origins allowed to call this gate: your game's hosted origin(s) and the pages
398
+ # that embed it. One * may span a single DNS label.
399
+ ALLOWED_ORIGINS=https://*.games.moongate.com,https://flaunch.gg
400
+ `,
401
+ 'railpack.json': `${JSON.stringify({
402
+ $schema: 'https://schema.railpack.com',
403
+ build: { builder: 'RAILPACK', buildCommand: 'pnpm install' },
404
+ deploy: { startCommand: 'pnpm gate' },
405
+ }, null, 2)}\n`,
406
+ 'src/play.ts': `import { connectHost, createMockRoom, joinRoom, type Snapshot } from '@flayerlabs/gamemode-client';
325
407
  import { rules, type Action, type Config, type PlayerView, type PublicView } from './game/rules.js';
326
408
 
327
409
  const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
328
- const room = createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
410
+
411
+ // Embedded on flaunch.gg, the page tells this game which gate and round to join and services its
412
+ // wallet calls over the frame bridge. Standing alone (pnpm dev), there is no page to ask — null is
413
+ // that answer, and the mock room runs the same rules with the chain and wallet faked.
414
+ const embedded = await connectHost();
415
+ const room = embedded
416
+ ? await joinRoom<PublicView, PlayerView, Action>({
417
+ gateUrl: embedded.context.gateUrl,
418
+ roundId: embedded.context.roundId,
419
+ host: embedded.host,
420
+ ...(embedded.context.launch ? { launch: embedded.context.launch } : {}),
421
+ })
422
+ : createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
329
423
  const app = document.querySelector<HTMLElement>('#app');
330
424
  if (!app) throw new Error('the page needs an #app element');
331
425
  const gameRoot = app;
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;IACvC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAErD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACjE,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,iBAAiB;IAC9B,OAAO,SAAS,CAAC,aAAa,EAAE,sCAAsC,CAAC,CAAC;AAC1E,CAAC;AAED,KAAK,UAAU,gBAAgB;IAC7B,OAAO,SAAS,CAAC,4BAA4B,EAAE,kDAAkD,CAAC,CAAC;AACrG,CAAC;AAED,KAAK,UAAU,wBAAwB;IACrC,OAAO,SAAS,CACd,sCAAsC,EACtC,4DAA4D,CAC7D,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,QAAgB,EAAE,MAAc;IACvD,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACpE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;QACpE,OAAO,QAAQ,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,CAAC,gGAAgG,CAAC,IAAI,CACpG,OAAO,CACR,EACD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AACnE,CAAC;AAED,KAAK,UAAU,eAAe;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAE9F,CAAC;IACF,OAAO,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,KAAK,GAAG,KAAK,EAAE,IAAY,EAAmC,EAAE;IACpE,MAAM,GAAG,GAAG,MAAM,eAAe,EAAE,CAAC;IACpC,OAAO;QACP,cAAc,EAAE,GAAG,IAAI,CAAC,SAAS,CAC/B;YACE,IAAI;YACJ,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE;gBACP,GAAG,EAAE,MAAM;gBACX,IAAI,EAAE,gDAAgD;gBACtD,KAAK,EAAE,kCAAkC;gBACzC,SAAS,EAAE,cAAc;aAC1B;YACD,YAAY,EAAE;gBACZ,6BAA6B,EAAE,GAAG;gBAClC,2BAA2B,EAAE,GAAG;aACjC;YACD,eAAe,EAAE;gBACf,0BAA0B,EAAE,GAAG;gBAC/B,UAAU,EAAE,QAAQ;gBACpB,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,QAAQ;aACjB;SACF,EACD,IAAI,EACJ,CAAC,CACF,IAAI;QAEL,WAAW,EAAE,MAAM,iBAAiB,EAAE;QAEtC,WAAW,EAAE,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CvB;QAEC,yCAAyC,EAAE,MAAM,gBAAgB,EAAE;QACnE,mDAAmD,EAAE,MAAM,wBAAwB,EAAE;QAErF,mBAAmB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAgDd,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6IZ;QAEC,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;2BAoBU,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2C9B;QAEC,YAAY,EAAE;;;;;aAKH,IAAI;;;;;;;;;;;;;;;;;;;;;;;CAuBhB;QAEC,oBAAoB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAkCZ,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqFf;QAEC,eAAe,EAAE,GAAG,IAAI,CAAC,SAAS,CAChC;YACE,eAAe,EAAE;gBACf,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,UAAU;gBAClB,gBAAgB,EAAE,UAAU;gBAC5B,MAAM,EAAE,IAAI;gBACZ,wBAAwB,EAAE,IAAI;gBAC9B,oBAAoB,EAAE,IAAI;gBAC1B,YAAY,EAAE,IAAI;gBAClB,MAAM,EAAE,IAAI;aACb;YACD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC;SACzB,EACD,IAAI,EACJ,CAAC,CACF,IAAI;QAEL,YAAY,EAAE,wBAAwB;KACrC,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flayerlabs/gamemode-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Create and validate Flaunch Game Modes",
5
5
  "license": "MIT",
6
6
  "author": "Flayer Labs",
package/src/scaffold.ts CHANGED
@@ -47,6 +47,17 @@ async function readCreatorSkillMetadata(): Promise<string> {
47
47
  );
48
48
  }
49
49
 
50
+ async function readServerSkill(): Promise<string> {
51
+ return readAsset('./run-a-game-server/SKILL.md', '../../../.agents/skills/run-a-game-server/SKILL.md');
52
+ }
53
+
54
+ async function readServerSkillMetadata(): Promise<string> {
55
+ return readAsset(
56
+ './run-a-game-server/agents/openai.yaml',
57
+ '../../../.agents/skills/run-a-game-server/agents/openai.yaml',
58
+ );
59
+ }
60
+
50
61
  async function readAsset(packaged: string, source: string): Promise<string> {
51
62
  try {
52
63
  return await readFile(new URL(packaged, import.meta.url), 'utf8');
@@ -86,16 +97,19 @@ const files = async (name: string): Promise<Record<string, string>> => {
86
97
  type: 'module',
87
98
  scripts: {
88
99
  dev: 'vite',
100
+ gate: 'tsx src/gate.ts',
89
101
  test: 'gamemode check src/game/rules.ts && vitest run',
90
102
  check: 'gamemode check src/game/rules.ts',
91
103
  typecheck: 'tsc --noEmit',
92
104
  },
93
105
  dependencies: {
94
106
  '@flayerlabs/gamemode-client': sdk,
107
+ '@flayerlabs/gamemode-gate': sdk,
95
108
  '@flayerlabs/gamemode-spec': sdk,
96
109
  },
97
110
  devDependencies: {
98
111
  '@flayerlabs/gamemode-cli': sdk,
112
+ tsx: '^4.23.12',
99
113
  typescript: '^5.7.2',
100
114
  vite: '^5.4.21',
101
115
  vitest: '^2.1.8',
@@ -130,6 +144,28 @@ pnpm test
130
144
  pnpm typecheck
131
145
  \`\`\`
132
146
 
147
+ ## Deploy your gate
148
+
149
+ Game Mode needs your game to bring its own round server — a gate — and its public https origin is
150
+ exactly the **Gate URL** you enter when you submit the game at flaunch.gg/game-mode/create. Flaunch
151
+ never runs a gate for you and never holds your signing key.
152
+
153
+ \`src/gate.ts\` is the whole entrypoint; \`.env.example\` is the whole contract. On Railway:
154
+
155
+ 1. Create a project from this repo (\`railpack.json\` names the build and start commands) and
156
+ attach a Postgres whose URL lands in \`DATABASE_URL\`.
157
+ 2. Set the variables from \`.env.example\`, generating the key and secret fresh.
158
+ 3. Give the service a public domain, set it as \`GATE_ORIGIN\`, and smoke-test it:
159
+
160
+ \`\`\`bash
161
+ curl https://<your-gate>/health # { "ok": true }
162
+ curl https://<your-gate>/config # chainId must be the chain you meant; note "signer"
163
+ \`\`\`
164
+
165
+ A gate that boots is a gate whose signatures a swap can verify — boot refuses loudly on a
166
+ wrong-chain RPC, a demo key, or an economy your rewardBounds cannot honour. The full environment
167
+ table lives in \`@flayerlabs/gamemode-gate\`'s DEPLOY.md.
168
+
133
169
  ## Work with an agent
134
170
 
135
171
  Ask the agent to read \`AGENTS.md\` before it edits the game. Use \`$build-game-mode\` when the
@@ -156,6 +192,8 @@ For the rules and client workflow, read the
156
192
 
157
193
  '.agents/skills/build-game-mode/SKILL.md': await readCreatorSkill(),
158
194
  '.agents/skills/build-game-mode/agents/openai.yaml': await readCreatorSkillMetadata(),
195
+ '.agents/skills/run-a-game-server/SKILL.md': await readServerSkill(),
196
+ '.agents/skills/run-a-game-server/agents/openai.yaml': await readServerSkillMetadata(),
159
197
 
160
198
  'src/game/rules.ts': `import { defineGame, type Decision, type PlayerId, type Refusal } from '@flayerlabs/gamemode-spec';
161
199
  import { scheduleWithin } from '@flayerlabs/gamemode-spec/schedule';
@@ -348,11 +386,79 @@ export interface PlayerView {
348
386
  }
349
387
  `,
350
388
 
351
- 'src/play.ts': `import { createMockRoom, type Snapshot } from '@flayerlabs/gamemode-client';
389
+ 'src/gate.ts': `import { startGate } from '@flayerlabs/gamemode-gate';
390
+ import { rules, type Config } from './game/rules.js';
391
+
392
+ /**
393
+ * Your game's own server, as a deployable process.
394
+ *
395
+ * A gate holds YOUR signing key and runs YOUR rules: a creator who launches a coin through your
396
+ * game writes your signer's address into their pool, and the chain trusts it for that pool alone.
397
+ * Flaunch never holds the key. \`startGate\` does the wiring — database migrations, the chain
398
+ * address book, boot refusals for anything misconfigured, /config for launch tooling — and this
399
+ * file stays what it should be: your round's economy, stated once.
400
+ *
401
+ * Everything environment-shaped comes from the environment; see .env.example.
402
+ */
403
+ const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
404
+
405
+ const { port, signer } = await startGate(rules, config);
406
+
407
+ // The signer address is what a launch must name for this gate to adopt it — it belongs in the
408
+ // deploy log, where an operator can read it back without a debugger.
409
+ console.log(\`[gate] listening on \${port} — signer \${signer}\`);
410
+ `,
411
+
412
+ '.env.example': `# The environment contract for \`pnpm gate\` (full table: @flayerlabs/gamemode-gate DEPLOY.md).
413
+ # Generate SIGNER_PRIVATE_KEY and SESSION_SECRET fresh for every environment — a key shared
414
+ # between two environments lets one gate's bug mint authorisations against the other's pools.
415
+
416
+ # The one chain this process serves. 84532 (Base Sepolia) is in the address book.
417
+ CHAIN_ID=84532
418
+ # An RPC endpoint for that chain; boot checks its eth_chainId matches.
419
+ RPC_URL=https://sepolia.base.org
420
+ # Your key, generated by you, never shared. openssl rand -hex 32 (prefix with 0x).
421
+ SIGNER_PRIVATE_KEY=
422
+ # Postgres. Migrations run at boot.
423
+ DATABASE_URL=
424
+ # At least 32 characters. openssl rand -hex 32
425
+ SESSION_SECRET=
426
+ # The page the player is looking at when they sign in — the embedding site, not this gate.
427
+ SIGN_IN_DOMAIN=flaunch.gg
428
+ # This gate's own public https origin — the exact value you submit as the Gate URL.
429
+ GATE_ORIGIN=
430
+ # Comma-separated origins allowed to call this gate: your game's hosted origin(s) and the pages
431
+ # that embed it. One * may span a single DNS label.
432
+ ALLOWED_ORIGINS=https://*.games.moongate.com,https://flaunch.gg
433
+ `,
434
+
435
+ 'railpack.json': `${JSON.stringify(
436
+ {
437
+ $schema: 'https://schema.railpack.com',
438
+ build: { builder: 'RAILPACK', buildCommand: 'pnpm install' },
439
+ deploy: { startCommand: 'pnpm gate' },
440
+ },
441
+ null,
442
+ 2,
443
+ )}\n`,
444
+
445
+ 'src/play.ts': `import { connectHost, createMockRoom, joinRoom, type Snapshot } from '@flayerlabs/gamemode-client';
352
446
  import { rules, type Action, type Config, type PlayerView, type PublicView } from './game/rules.js';
353
447
 
354
448
  const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
355
- const room = createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
449
+
450
+ // Embedded on flaunch.gg, the page tells this game which gate and round to join and services its
451
+ // wallet calls over the frame bridge. Standing alone (pnpm dev), there is no page to ask — null is
452
+ // that answer, and the mock room runs the same rules with the chain and wallet faked.
453
+ const embedded = await connectHost();
454
+ const room = embedded
455
+ ? await joinRoom<PublicView, PlayerView, Action>({
456
+ gateUrl: embedded.context.gateUrl,
457
+ roundId: embedded.context.roundId,
458
+ host: embedded.host,
459
+ ...(embedded.context.launch ? { launch: embedded.context.launch } : {}),
460
+ })
461
+ : createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
356
462
  const app = document.querySelector<HTMLElement>('#app');
357
463
  if (!app) throw new Error('the page needs an #app element');
358
464
  const gameRoot = app;