@flayerlabs/gamemode-gate 0.1.0

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.
Files changed (78) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +79 -0
  3. package/dist/chain/discover.d.ts +158 -0
  4. package/dist/chain/discover.d.ts.map +1 -0
  5. package/dist/chain/discover.js +169 -0
  6. package/dist/chain/discover.js.map +1 -0
  7. package/dist/chain/signer.d.ts +46 -0
  8. package/dist/chain/signer.d.ts.map +1 -0
  9. package/dist/chain/signer.js +60 -0
  10. package/dist/chain/signer.js.map +1 -0
  11. package/dist/claims.d.ts +62 -0
  12. package/dist/claims.d.ts.map +1 -0
  13. package/dist/claims.js +128 -0
  14. package/dist/claims.js.map +1 -0
  15. package/dist/demo.d.ts +40 -0
  16. package/dist/demo.d.ts.map +1 -0
  17. package/dist/demo.js +90 -0
  18. package/dist/demo.js.map +1 -0
  19. package/dist/economy.d.ts +46 -0
  20. package/dist/economy.d.ts.map +1 -0
  21. package/dist/economy.js +100 -0
  22. package/dist/economy.js.map +1 -0
  23. package/dist/game-registry.d.ts +17 -0
  24. package/dist/game-registry.d.ts.map +1 -0
  25. package/dist/game-registry.js +90 -0
  26. package/dist/game-registry.js.map +1 -0
  27. package/dist/index.d.ts +23 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +14 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/ledger.d.ts +87 -0
  32. package/dist/ledger.d.ts.map +1 -0
  33. package/dist/ledger.js +222 -0
  34. package/dist/ledger.js.map +1 -0
  35. package/dist/registry.d.ts +55 -0
  36. package/dist/registry.d.ts.map +1 -0
  37. package/dist/registry.js +164 -0
  38. package/dist/registry.js.map +1 -0
  39. package/dist/room.d.ts +98 -0
  40. package/dist/room.d.ts.map +1 -0
  41. package/dist/room.js +215 -0
  42. package/dist/room.js.map +1 -0
  43. package/dist/server.d.ts +92 -0
  44. package/dist/server.d.ts.map +1 -0
  45. package/dist/server.js +695 -0
  46. package/dist/server.js.map +1 -0
  47. package/dist/sessions.d.ts +40 -0
  48. package/dist/sessions.d.ts.map +1 -0
  49. package/dist/sessions.js +144 -0
  50. package/dist/sessions.js.map +1 -0
  51. package/dist/settlement.d.ts +24 -0
  52. package/dist/settlement.d.ts.map +1 -0
  53. package/dist/settlement.js +69 -0
  54. package/dist/settlement.js.map +1 -0
  55. package/dist/store.d.ts +24 -0
  56. package/dist/store.d.ts.map +1 -0
  57. package/dist/store.js +125 -0
  58. package/dist/store.js.map +1 -0
  59. package/dist/turnstile.d.ts +19 -0
  60. package/dist/turnstile.d.ts.map +1 -0
  61. package/dist/turnstile.js +61 -0
  62. package/dist/turnstile.js.map +1 -0
  63. package/package.json +54 -0
  64. package/src/chain/discover.ts +243 -0
  65. package/src/chain/signer.ts +118 -0
  66. package/src/claims.ts +140 -0
  67. package/src/demo.ts +149 -0
  68. package/src/economy.ts +126 -0
  69. package/src/game-registry.ts +107 -0
  70. package/src/index.ts +45 -0
  71. package/src/ledger.ts +355 -0
  72. package/src/registry.ts +212 -0
  73. package/src/room.ts +297 -0
  74. package/src/server.ts +833 -0
  75. package/src/sessions.ts +155 -0
  76. package/src/settlement.ts +72 -0
  77. package/src/store.ts +127 -0
  78. package/src/turnstile.ts +74 -0
package/dist/room.d.ts ADDED
@@ -0,0 +1,98 @@
1
+ import type { Pool } from 'pg';
2
+ import type { Command, Decision, GameModule, PlayerId, Refusal, RoundWindow } from '@flayerlabs/gamemode-spec';
3
+ import type { LaunchContext } from '@flayerlabs/gamemode-spec/live';
4
+ /**
5
+ * Everything except a wake.
6
+ *
7
+ * Wakes belong to {@link Room.tick}. Passing one to {@link Room.apply} would fire the same deadline
8
+ * twice — `send` runs due wakes on the way to the command — collapsing whatever phase sat between
9
+ * them. Excluding it from the type is better than checking for it: the mistake stops compiling.
10
+ */
11
+ type PlayerCommand<Action> = Exclude<Command<Action>, {
12
+ kind: 'wake';
13
+ }>;
14
+ /**
15
+ * A round, made durable.
16
+ *
17
+ * One command at a time, and each one commits in a single transaction: the command itself, the
18
+ * resulting state, and every point it awarded. Nothing can be half-applied, so a process that dies
19
+ * mid-command leaves a round that either took it or did not.
20
+ *
21
+ * Rebuilding is a snapshot read, not a replay. The command log exists to answer "why does this
22
+ * state look like this" during an incident, which is a different job from reconstructing it.
23
+ */
24
+ export declare class Room<Config, State, Event, Action, PublicView, PlayerView> {
25
+ private readonly pool;
26
+ private readonly game;
27
+ readonly id: string;
28
+ private round;
29
+ readonly window: RoundWindow;
30
+ readonly launch: LaunchContext;
31
+ readonly weiPerPoint: bigint;
32
+ private seq;
33
+ /**
34
+ * HTTP actions and the server ticker can arrive together. Keep the one-writer promise true
35
+ * inside a process by running both through the same serial tail.
36
+ */
37
+ private pending;
38
+ private constructor();
39
+ static create<Config, State, Event, Action, PublicView, PlayerView>(pool: Pool, game: GameModule<Config, State, Event, Action, PublicView, PlayerView>, round: {
40
+ id: string;
41
+ config: Config;
42
+ seed: number;
43
+ window: RoundWindow;
44
+ weiPerPoint: bigint;
45
+ walletCapWei: bigint;
46
+ poolId: string;
47
+ booksCloseAt?: number;
48
+ launch?: {
49
+ coinAddress?: string | null;
50
+ name?: string;
51
+ symbol?: string;
52
+ imageUrl?: string | null;
53
+ };
54
+ }): Promise<Room<Config, State, Event, Action, PublicView, PlayerView>>;
55
+ /** Pick up a round that already exists. Returns null if there is no such round. */
56
+ static load<Config, State, Event, Action, PublicView, PlayerView>(pool: Pool, game: GameModule<Config, State, Event, Action, PublicView, PlayerView>, id: string): Promise<Room<Config, State, Event, Action, PublicView, PlayerView> | null>;
57
+ /**
58
+ * Apply one command and commit everything it caused.
59
+ *
60
+ * A refusal is not persisted. It changed nothing, and writing a row for every rejected action
61
+ * would let anyone with a keyboard grow the database.
62
+ */
63
+ apply(command: PlayerCommand<Action>): Promise<Decision<Event> | Refusal>;
64
+ private applyNow;
65
+ /**
66
+ * A round over a COPY of the committed state.
67
+ *
68
+ * `evolve` is supposed to be pure, and a copy is what makes that a rule rather than a hope: a
69
+ * game that mutates its state in place would otherwise corrupt the live round even when the
70
+ * transaction that was meant to save it failed.
71
+ */
72
+ private working;
73
+ /**
74
+ * Write one step: the command, the state it produced, and the points it awarded.
75
+ *
76
+ * The update is conditional on the sequence we read, so a second writer for the same round —
77
+ * another process, or a second Room in this one — loses instead of silently overwriting. There
78
+ * is meant to be one writer per round; this is what makes a broken assumption loud.
79
+ */
80
+ private commit;
81
+ /**
82
+ * Catch the round up to `now`, firing every wake it has missed.
83
+ *
84
+ * This lets the round advance itself rather than sending it a wake command. Sending one would
85
+ * fire the same deadline twice — `send` runs due wakes before applying anything — which skips
86
+ * whatever phase sat between them and reveals a quiz answer early.
87
+ */
88
+ tick(now: number): Promise<void>;
89
+ private tickNow;
90
+ private serially;
91
+ nextWakeAt(): number | null;
92
+ publicView(): PublicView;
93
+ playerView(player: PlayerId): PlayerView;
94
+ /** The cursor a reconnecting client resumes from. */
95
+ sequence(): number;
96
+ }
97
+ export {};
98
+ //# sourceMappingURL=room.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room.d.ts","sourceRoot":"","sources":["../src/room.ts"],"names":[],"mappings":"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"}
package/dist/room.js ADDED
@@ -0,0 +1,215 @@
1
+ import { Round } from '@flayerlabs/gamemode-spec/round';
2
+ import { isRefusal } from '@flayerlabs/gamemode-spec';
3
+ import { inTransaction, toBigInt } from './store.js';
4
+ /**
5
+ * A round, made durable.
6
+ *
7
+ * One command at a time, and each one commits in a single transaction: the command itself, the
8
+ * resulting state, and every point it awarded. Nothing can be half-applied, so a process that dies
9
+ * mid-command leaves a round that either took it or did not.
10
+ *
11
+ * Rebuilding is a snapshot read, not a replay. The command log exists to answer "why does this
12
+ * state look like this" during an incident, which is a different job from reconstructing it.
13
+ */
14
+ export class Room {
15
+ pool;
16
+ game;
17
+ id;
18
+ round;
19
+ window;
20
+ launch;
21
+ weiPerPoint;
22
+ seq;
23
+ /**
24
+ * HTTP actions and the server ticker can arrive together. Keep the one-writer promise true
25
+ * inside a process by running both through the same serial tail.
26
+ */
27
+ pending = Promise.resolve();
28
+ constructor(pool, game, id, round, window, launch, weiPerPoint, seq) {
29
+ this.pool = pool;
30
+ this.game = game;
31
+ this.id = id;
32
+ this.round = round;
33
+ this.window = window;
34
+ this.launch = launch;
35
+ this.weiPerPoint = weiPerPoint;
36
+ this.seq = seq;
37
+ }
38
+ static async create(pool, game, round) {
39
+ const booksCloseAt = round.booksCloseAt ?? round.window.closesAt;
40
+ if (!Number.isSafeInteger(booksCloseAt) || booksCloseAt < round.window.closesAt) {
41
+ throw new RangeError('booksCloseAt must be a safe timestamp at or after closesAt');
42
+ }
43
+ const launch = Object.freeze({
44
+ roundId: round.id,
45
+ poolId: round.poolId,
46
+ coinAddress: round.launch?.coinAddress ?? null,
47
+ name: round.launch?.name ?? '',
48
+ symbol: round.launch?.symbol ?? '',
49
+ imageUrl: round.launch?.imageUrl ?? null,
50
+ opensAt: round.window.opensAt,
51
+ closesAt: round.window.closesAt,
52
+ booksCloseAt,
53
+ });
54
+ const started = Round.start(game, round.config, round.seed, round.window);
55
+ await pool.query(`insert into rounds (
56
+ id, wei_per_point, wallet_cap_wei, pool_id, opens_at, closes_at, books_close_at,
57
+ coin_address, coin_name, coin_symbol, coin_image_url, state, seq
58
+ ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 0)`, [
59
+ round.id,
60
+ round.weiPerPoint.toString(),
61
+ round.walletCapWei.toString(),
62
+ round.poolId,
63
+ round.window.opensAt,
64
+ round.window.closesAt,
65
+ booksCloseAt,
66
+ launch.coinAddress,
67
+ launch.name,
68
+ launch.symbol,
69
+ launch.imageUrl,
70
+ serialise(game.id, started.snapshot()),
71
+ ]);
72
+ return new Room(pool, game, round.id, started, round.window, launch, round.weiPerPoint, 0);
73
+ }
74
+ /** Pick up a round that already exists. Returns null if there is no such round. */
75
+ static async load(pool, game, id) {
76
+ const result = await pool.query(`select state, seq, pool_id, opens_at, closes_at, books_close_at, wei_per_point,
77
+ coin_address, coin_name, coin_symbol, coin_image_url
78
+ from rounds where id = $1`, [id]);
79
+ const row = result.rows[0];
80
+ if (!row)
81
+ return null;
82
+ const window = { opensAt: Number(row.opens_at), closesAt: Number(row.closes_at) };
83
+ const launch = Object.freeze({
84
+ roundId: id,
85
+ poolId: row.pool_id,
86
+ coinAddress: row.coin_address,
87
+ name: row.coin_name,
88
+ symbol: row.coin_symbol,
89
+ imageUrl: row.coin_image_url,
90
+ ...window,
91
+ booksCloseAt: Number(row.books_close_at),
92
+ });
93
+ return new Room(pool, game, id, Round.resume(game, row.state, window), window, launch, toBigInt(row.wei_per_point), row.seq);
94
+ }
95
+ /**
96
+ * Apply one command and commit everything it caused.
97
+ *
98
+ * A refusal is not persisted. It changed nothing, and writing a row for every rejected action
99
+ * would let anyone with a keyboard grow the database.
100
+ */
101
+ async apply(command) {
102
+ return this.serially(() => this.applyNow(command));
103
+ }
104
+ async applyNow(command) {
105
+ const awards = [];
106
+ const round = this.working((award) => awards.push(award));
107
+ // Catch up first, and keep whatever that produced even if the command itself is refused. A
108
+ // reveal that fired on the way to a rejected action really happened: discarding it would
109
+ // silently un-reveal the round and delay everyone's points until something else arrived.
110
+ const caughtUp = round.advanceTo(command.at) > 0;
111
+ const result = round.send(command);
112
+ const refused = isRefusal(result);
113
+ if (refused && !caughtUp)
114
+ return result;
115
+ const seq = this.seq + 1;
116
+ await this.commit(seq, refused ? { kind: 'wake', at: command.at } : command, round.snapshot(), awards);
117
+ this.round = round;
118
+ this.seq = seq;
119
+ return result;
120
+ }
121
+ /**
122
+ * A round over a COPY of the committed state.
123
+ *
124
+ * `evolve` is supposed to be pure, and a copy is what makes that a rule rather than a hope: a
125
+ * game that mutates its state in place would otherwise corrupt the live round even when the
126
+ * transaction that was meant to save it failed.
127
+ */
128
+ working(onAward) {
129
+ return Round.resume(this.game, structuredClone(this.round.snapshot()), this.window, onAward);
130
+ }
131
+ /**
132
+ * Write one step: the command, the state it produced, and the points it awarded.
133
+ *
134
+ * The update is conditional on the sequence we read, so a second writer for the same round —
135
+ * another process, or a second Room in this one — loses instead of silently overwriting. There
136
+ * is meant to be one writer per round; this is what makes a broken assumption loud.
137
+ */
138
+ async commit(seq, command, state, awards) {
139
+ await inTransaction(this.pool, async (tx) => {
140
+ const updated = await tx.query(`update rounds set state = $2, seq = $3 where id = $1 and seq = $4`, [this.id, serialise(this.game.id, state), seq, seq - 1]);
141
+ if (updated.rowCount !== 1) {
142
+ throw new Error(`${this.id}: round moved on while this command was being applied`);
143
+ }
144
+ await tx.query(`insert into commands (round_id, seq, command) values ($1, $2, $3)`, [
145
+ this.id,
146
+ seq,
147
+ JSON.stringify(command),
148
+ ]);
149
+ for (const award of awards) {
150
+ // Same transaction as the state that earned them. Points credited without the state that
151
+ // justifies them, or the reverse, is a ledger nobody can reconcile.
152
+ await tx.query(`insert into balances (round_id, player, points) values ($1, $2, $3)
153
+ on conflict (round_id, player) do update set points = balances.points + excluded.points`, [this.id, award.player, Math.floor(award.points)]);
154
+ }
155
+ });
156
+ }
157
+ /**
158
+ * Catch the round up to `now`, firing every wake it has missed.
159
+ *
160
+ * This lets the round advance itself rather than sending it a wake command. Sending one would
161
+ * fire the same deadline twice — `send` runs due wakes before applying anything — which skips
162
+ * whatever phase sat between them and reveals a quiz answer early.
163
+ */
164
+ async tick(now) {
165
+ return this.serially(() => this.tickNow(now));
166
+ }
167
+ async tickNow(now) {
168
+ const awards = [];
169
+ const round = this.working((award) => awards.push(award));
170
+ if (round.advanceTo(now) === 0)
171
+ return;
172
+ const seq = this.seq + 1;
173
+ await this.commit(seq, { kind: 'wake', at: now }, round.snapshot(), awards);
174
+ this.round = round;
175
+ this.seq = seq;
176
+ }
177
+ serially(work) {
178
+ const result = this.pending.then(work, work);
179
+ this.pending = result.then(() => undefined, () => undefined);
180
+ return result;
181
+ }
182
+ nextWakeAt() {
183
+ return this.game.nextWakeAt(this.round.snapshot());
184
+ }
185
+ publicView() {
186
+ return this.round.publicView();
187
+ }
188
+ playerView(player) {
189
+ return this.round.playerView(player);
190
+ }
191
+ /** The cursor a reconnecting client resumes from. */
192
+ sequence() {
193
+ return this.seq;
194
+ }
195
+ }
196
+ /**
197
+ * Game state has to survive a round trip through JSON, and one thing silently does not: `bigint`
198
+ * makes `JSON.stringify` throw. It is an easy mistake — a game holding wei or a large counter — and
199
+ * the default message says only "Do not know how to serialize a BigInt", naming neither the game
200
+ * nor the field. Saying which game, and what to do instead, turns a confusing crash into an
201
+ * instruction.
202
+ */
203
+ function serialise(gameId, state) {
204
+ try {
205
+ return JSON.stringify(state);
206
+ }
207
+ catch (error) {
208
+ if (error instanceof TypeError && /BigInt/i.test(error.message)) {
209
+ throw new TypeError(`${gameId}: game state contains a bigint, which cannot be stored. Keep bigints out of state — ` +
210
+ 'points are plain numbers and the platform owns anything measured in wei.');
211
+ }
212
+ throw error;
213
+ }
214
+ }
215
+ //# sourceMappingURL=room.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room.js","sourceRoot":"","sources":["../src/room.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AAGxD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWrD;;;;;;;;;GASG;AACH,MAAM,OAAO,IAAI;IAQI;IACA;IACR;IACD;IACC;IACA;IACA;IACD;IAdV;;;OAGG;IACK,OAAO,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEnD,YACmB,IAAU,EACV,IAAsE,EAC9E,EAAU,EACX,KAAkE,EACjE,MAAmB,EACnB,MAAqB,EACrB,WAAmB,EACpB,GAAW;QAPF,SAAI,GAAJ,IAAI,CAAM;QACV,SAAI,GAAJ,IAAI,CAAkE;QAC9E,OAAE,GAAF,EAAE,CAAQ;QACX,UAAK,GAAL,KAAK,CAA6D;QACjE,WAAM,GAAN,MAAM,CAAa;QACnB,WAAM,GAAN,MAAM,CAAe;QACrB,gBAAW,GAAX,WAAW,CAAQ;QACpB,QAAG,GAAH,GAAG,CAAQ;IAClB,CAAC;IAEJ,MAAM,CAAC,KAAK,CAAC,MAAM,CACjB,IAAU,EACV,IAAsE,EACtE,KAeC;QAED,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC;QACjE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAChF,MAAM,IAAI,UAAU,CAAC,4DAA4D,CAAC,CAAC;QACrF,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,KAAK,CAAC,EAAE;YACjB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,WAAW,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,IAAI,IAAI;YAC9C,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE;YAC9B,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,IAAI,EAAE;YAClC,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE,QAAQ,IAAI,IAAI;YACxC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,OAAO;YAC7B,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;YAC/B,YAAY;SACb,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1E,MAAM,IAAI,CAAC,KAAK,CACd;;;uEAGiE,EACjE;YACE,KAAK,CAAC,EAAE;YACR,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE;YAC5B,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE;YAC7B,KAAK,CAAC,MAAM;YACZ,KAAK,CAAC,MAAM,CAAC,OAAO;YACpB,KAAK,CAAC,MAAM,CAAC,QAAQ;YACrB,YAAY;YACZ,MAAM,CAAC,WAAW;YAClB,MAAM,CAAC,IAAI;YACX,MAAM,CAAC,MAAM;YACb,MAAM,CAAC,QAAQ;YACf,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;SACvC,CACF,CAAC;QACF,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC7F,CAAC;IAED,mFAAmF;IACnF,MAAM,CAAC,KAAK,CAAC,IAAI,CACf,IAAU,EACV,IAAsE,EACtE,EAAU;QAEV,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAa7B;;iCAE2B,EAC3B,CAAC,EAAE,CAAC,CACL,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QAEtB,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAClF,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC3B,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,GAAG,CAAC,OAAO;YACnB,WAAW,EAAE,GAAG,CAAC,YAAY;YAC7B,IAAI,EAAE,GAAG,CAAC,SAAS;YACnB,MAAM,EAAE,GAAG,CAAC,WAAW;YACvB,QAAQ,EAAE,GAAG,CAAC,cAAc;YAC5B,GAAG,MAAM;YACT,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;SACzC,CAAC,CAAC;QACH,OAAO,IAAI,IAAI,CACb,IAAI,EACJ,IAAI,EACJ,EAAE,EACF,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,EACrC,MAAM,EACN,MAAM,EACN,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,EAC3B,GAAG,CAAC,GAAG,CACR,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,OAA8B;QACxC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,OAA8B;QACnD,MAAM,MAAM,GAAY,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAE1D,2FAA2F;QAC3F,yFAAyF;QACzF,yFAAyF;QACzF,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAEjD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAClC,IAAI,OAAO,IAAI,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACzB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;QACvG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACK,OAAO,CAAC,OAA+B;QAC7C,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,OAAwB,EAAE,KAAY,EAAE,MAAe;QACvF,MAAM,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;YAC1C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,KAAK,CAC5B,mEAAmE,EACnE,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,CACxD,CAAC;YACF,IAAI,OAAO,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,EAAE,uDAAuD,CAAC,CAAC;YACrF,CAAC;YACD,MAAM,EAAE,CAAC,KAAK,CAAC,mEAAmE,EAAE;gBAClF,IAAI,CAAC,EAAE;gBACP,GAAG;gBACH,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;aACxB,CAAC,CAAC;YACH,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,yFAAyF;gBACzF,oEAAoE;gBACpE,MAAM,EAAE,CAAC,KAAK,CACZ;mGACyF,EACzF,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAClD,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAC,GAAW;QACpB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,GAAW;QAC/B,MAAM,MAAM,GAAY,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,IAAI,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,OAAO;QAEvC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACzB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAC;QAC5E,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAEO,QAAQ,CAAI,IAAsB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CACxB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IACjC,CAAC;IAED,UAAU,CAAC,MAAgB;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IAED,qDAAqD;IACrD,QAAQ;QACN,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;CAEF;AAED;;;;;;GAMG;AACH,SAAS,SAAS,CAAC,MAAc,EAAE,KAAc;IAC/C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,SAAS,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,SAAS,CACjB,GAAG,MAAM,sFAAsF;gBAC7F,0EAA0E,CAC7E,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { type FastifyInstance } from 'fastify';
2
+ import type { Pool } from 'pg';
3
+ import type { Command, GameModule, PlayerId } from '@flayerlabs/gamemode-spec';
4
+ import { Room } from './room.js';
5
+ import { Claims } from './claims.js';
6
+ import { Sessions } from './sessions.js';
7
+ import type { Discovery, Launch, PoolKey } from './chain/discover.js';
8
+ import type { Settlement } from './settlement.js';
9
+ /**
10
+ * The gate's door.
11
+ *
12
+ * Deliberately small. Everything interesting already lives behind it — the room decides, the
13
+ * ledger and signer authorise — so this file is transport and nothing else. Any rule that appears
14
+ * here is a rule in the wrong place.
15
+ *
16
+ * Views are sent whole rather than as deltas. Whole snapshots stop being sensible somewhere in the
17
+ * low thousands of players in one room; deltas are the fix, and adding them before there is a room
18
+ * that size would be guessing at the shape of a problem nobody has.
19
+ */
20
+ interface GateBaseOptions<Config, State, Event, Action, PublicView, PlayerView> {
21
+ pool: Pool;
22
+ game: GameModule<Config, State, Event, Action, PublicView, PlayerView>;
23
+ sessions: Sessions;
24
+ claims: Claims;
25
+ /** How many points a dollar buys. The only economic number anyone sets by hand. */
26
+ pointsPerDollar: number;
27
+ usdPerEth: number;
28
+ /** Verifies from the chain that a coin really is a game launch. */
29
+ discovery?: Discovery;
30
+ /** Keeps the ledger's view of spending level with the chain's. */
31
+ settlement?: Settlement;
32
+ /** How long charts and final spend reconciliation remain live after gameplay closes. */
33
+ settlementTailMs?: number;
34
+ /** Origins allowed to call this gate: the game's own, and nothing else. */
35
+ allowedOrigins: string[];
36
+ /**
37
+ * Decide whether a caller may open a session or spend a request, or refuse them.
38
+ *
39
+ * One optional seam rather than a bot layer, because the pieces of a bot layer are not
40
+ * game-neutral and this library is self-hosted. A human-verification vendor needs keys only the
41
+ * operator has; a request budget needs a cadence only the game knows — a quiz taking one answer
42
+ * every ten seconds and a shooter taking thirty a second cannot share a number; and a false
43
+ * refusal on a claim costs a player a purchase inside a window that will not come back.
44
+ *
45
+ * So the policy belongs to whoever runs the gate. What belongs here is the place to put it, and
46
+ * the guarantee it is consulted before a session is issued and before allowance is committed.
47
+ *
48
+ * Omitted, everything is admitted, which is what a laptop and a test want.
49
+ */
50
+ admit?: Admit;
51
+ /** The only cosmetic reaction IDs this game may broadcast. Omitted disables reactions. */
52
+ reactionIds?: readonly string[];
53
+ }
54
+ /** Resolve one immutable rules config after the launch has been verified. */
55
+ export type VerifiedLaunch = Readonly<Omit<Launch, 'poolKey'>> & {
56
+ readonly poolKey: Readonly<PoolKey>;
57
+ };
58
+ export type ConfigForLaunch<Config> = (launch: VerifiedLaunch) => Promise<Config>;
59
+ export type GateOptions<Config, State, Event, Action, PublicView, PlayerView> = GateBaseOptions<Config, State, Event, Action, PublicView, PlayerView> & {
60
+ /** A static serialisable config, or a factory resolved once after each launch is verified. */
61
+ config: Config | ConfigForLaunch<Config>;
62
+ };
63
+ /** Where an admission check is being made. Enough to tell a sign-in from a spend. */
64
+ export interface AdmissionRequest {
65
+ /** `session` on sign-in, `action` on gameplay, `claim` when allowance is about to be committed. */
66
+ at: 'session' | 'action' | 'claim';
67
+ /** Null at `session`, because nobody has proved who they are yet. */
68
+ player: PlayerId | null;
69
+ roundId: string | null;
70
+ /** Straight from the socket, so a limiter can key on it. Absent behind some proxies. */
71
+ ip: string | null;
72
+ /** Opaque and operator-defined: a captcha solution, a device token, whatever was sent. */
73
+ evidence: unknown;
74
+ }
75
+ /**
76
+ * Allow, or refuse with a reason a player can be shown.
77
+ *
78
+ * The reason reaches the player, so write it for them: "too many tries, wait a minute", not a rule
79
+ * name or a score.
80
+ */
81
+ export type Admission = {
82
+ ok: true;
83
+ } | {
84
+ ok: false;
85
+ reason: string;
86
+ };
87
+ export type Admit = (request: AdmissionRequest) => Admission | Promise<Admission>;
88
+ export declare const DEFAULT_SETTLEMENT_TAIL_MS: number;
89
+ export declare function createGate<Config, State, Event, Action, PublicView, PlayerView>(options: GateOptions<Config, State, Event, Action, PublicView, PlayerView>): FastifyInstance;
90
+ export { Room, Claims, Sessions };
91
+ export type { Command };
92
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}