@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
@@ -0,0 +1,87 @@
1
+ import type { Pool } from 'pg';
2
+ import type { PlayerId } from '@flayerlabs/gamemode-spec';
3
+ import type { EconomyBalance } from '@flayerlabs/gamemode-spec/live';
4
+ /**
5
+ * What a player has earned, what they have been authorised to spend, and what is still available.
6
+ *
7
+ * Two ceilings bind, and they are different in kind. The first is what the game says they earned.
8
+ * The second is the pool's on-chain per-wallet cap, which the contract enforces whatever this file
9
+ * believes — so issuing past it does not overspend, it produces an authorisation that reverts at
10
+ * the swap and looks to the player like theft. Both are checked here.
11
+ *
12
+ * Every claim takes a row lock on the player's balance for the length of the transaction. Two
13
+ * claims arriving at once therefore serialise, rather than each reading the same balance and each
14
+ * deciding it fits. That is the whole reason this is in the database and not in a Map.
15
+ */
16
+ export interface Claim {
17
+ nonce: bigint;
18
+ player: PlayerId;
19
+ requestId: string;
20
+ requestedWei: bigint;
21
+ wei: bigint;
22
+ deadline: bigint;
23
+ status: 'held' | 'released';
24
+ signature: `0x${string}` | null;
25
+ signer: `0x${string}` | null;
26
+ }
27
+ export type ClaimResult = {
28
+ ok: true;
29
+ claim: Claim;
30
+ } | {
31
+ ok: false;
32
+ refuse: string;
33
+ };
34
+ export interface BalanceKey {
35
+ roundId: string;
36
+ player: PlayerId;
37
+ }
38
+ export declare class Ledger {
39
+ private readonly pool;
40
+ constructor(pool: Pool);
41
+ /** Convenience for tests and boot. The schema itself lives in store.ts. */
42
+ migrate(): Promise<void>;
43
+ /** Credit points. Called once per award the game emits. */
44
+ award(roundId: string, player: PlayerId, points: number): Promise<void>;
45
+ /**
46
+ * Authorise up to `upToWei`, or refuse.
47
+ *
48
+ * Returns the largest amount available rather than refusing an over-ask: a player pressing buy
49
+ * wants to spend what they have, and telling them "you asked for too much" when they asked for
50
+ * everything is a worse product than giving them everything.
51
+ */
52
+ claim(roundId: string, player: PlayerId, upToWei: bigint, requestId: string, deadline: bigint, at?: number): Promise<ClaimResult>;
53
+ /** Store exactly one signature and return the stored winner to every concurrent signer. */
54
+ completeClaim(nonce: bigint, signature: `0x${string}`, signer: `0x${string}`): Promise<Claim | null>;
55
+ /**
56
+ * Release only a reservation that provably never became a bearer authorisation.
57
+ *
58
+ * A signed payload cannot be revoked. Releasing one because a wallet dialog was declined would
59
+ * let a new nonce spend the same earned allowance while the first payload remained usable.
60
+ */
61
+ releaseUnsigned(nonce: bigint): Promise<void>;
62
+ /**
63
+ * Record what the chain says a wallet has spent on this pool.
64
+ *
65
+ * Never allowed to go backwards: a read from a lagging node must not undo what a later one saw.
66
+ */
67
+ recordChainSpend(roundId: string, player: PlayerId, spentWei: bigint): Promise<boolean>;
68
+ /** Everyone with a balance in this round, for the reconciler to ask the chain about. */
69
+ playersIn(roundId: string): Promise<PlayerId[]>;
70
+ /**
71
+ * Give back every hold whose deadline has passed.
72
+ *
73
+ * An expired authorisation cannot be spent — the contract checks the same deadline — so a hold
74
+ * still sitting against it is allowance the player earned, cannot use, and would never get back.
75
+ * A player who opens the buy dialog and wanders off would otherwise lose it for the round.
76
+ *
77
+ * Returns how many were returned, so a caller can log something worth reading.
78
+ */
79
+ releaseExpired(nowSeconds: number): Promise<BalanceKey[]>;
80
+ availableWei(roundId: string, player: PlayerId): Promise<bigint>;
81
+ /** The authoritative balance snapshot, derived from the same state and ceiling as `claim`. */
82
+ economyBalance(roundId: string, player: PlayerId): Promise<EconomyBalance>;
83
+ private readState;
84
+ private findClaimWith;
85
+ private findClaimByNonce;
86
+ }
87
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGrE;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAErF,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CAClB;AA8CD,qBAAa,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEvC,2EAA2E;IACrE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B,2DAA2D;IACrD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS7E;;;;;;OAMG;IACG,KAAK,CACT,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,WAAW,CAAC;IAiDvB,2FAA2F;IACrF,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAgB1G;;;;;OAKG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD;;;;OAIG;IACG,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS7F,wFAAwF;IAClF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAQrD;;;;;;;;OAQG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAczD,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAItE,8FAA8F;IACxF,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;YAkBlE,SAAS;YA4CT,aAAa;YAeb,gBAAgB;CAS/B"}
package/dist/ledger.js ADDED
@@ -0,0 +1,222 @@
1
+ import { inTransaction, migrate, toBigInt } from './store.js';
2
+ /**
3
+ * The one place the ceiling rule lives.
4
+ *
5
+ * Two limits bind and the smaller wins: what the game says was earned, and what the pool lets one
6
+ * wallet spend. Issuing past the second does not overspend — the chain refuses it — it mints a
7
+ * signature that reverts at the swap, which to a player is indistinguishable from being robbed.
8
+ */
9
+ function availableFrom({ points, weiPerPoint, capWei, heldWei, chainSpentWei }) {
10
+ const earnedWei = points * weiPerPoint;
11
+ const ceiling = earnedWei < capWei ? earnedWei : capWei;
12
+ // Spent-on-chain PLUS still-outstanding, never the larger of the two.
13
+ //
14
+ // Taking the larger looked safe and was not. Hold 40, spend it, let the hold expire, then hold
15
+ // 60: the chain has 40 gone and 60 is outstanding, so nothing remains — but `max(60, 40)` says
16
+ // 40 is available and a further 40 gets authorised. Against a cap of 100 that is 140 of live
17
+ // authorisations, and the surplus does not overspend, it reverts at the swap, which to a player
18
+ // is indistinguishable from being robbed.
19
+ //
20
+ // Adding them double-counts a hold whose swap has already landed, until that hold expires. That
21
+ // errs towards authorising too little, which is the direction that costs nobody anything.
22
+ const available = ceiling - (chainSpentWei + heldWei);
23
+ return available > 0n ? available : 0n;
24
+ }
25
+ export class Ledger {
26
+ pool;
27
+ constructor(pool) {
28
+ this.pool = pool;
29
+ }
30
+ /** Convenience for tests and boot. The schema itself lives in store.ts. */
31
+ async migrate() {
32
+ await migrate(this.pool);
33
+ }
34
+ /** Credit points. Called once per award the game emits. */
35
+ async award(roundId, player, points) {
36
+ if (points <= 0)
37
+ return;
38
+ await this.pool.query(`insert into balances (round_id, player, points) values ($1, $2, $3)
39
+ on conflict (round_id, player) do update set points = balances.points + excluded.points`, [roundId, player, Math.floor(points)]);
40
+ }
41
+ /**
42
+ * Authorise up to `upToWei`, or refuse.
43
+ *
44
+ * Returns the largest amount available rather than refusing an over-ask: a player pressing buy
45
+ * wants to spend what they have, and telling them "you asked for too much" when they asked for
46
+ * everything is a worse product than giving them everything.
47
+ */
48
+ async claim(roundId, player, upToWei, requestId, deadline, at = Date.now()) {
49
+ return inTransaction(this.pool, async (tx) => {
50
+ // The lock is the point. Held for the transaction, it serialises every concurrent claim by
51
+ // this player, so two requests cannot both read the same balance and both decide they fit.
52
+ //
53
+ // It only works because a claim needs a balance row to exist: `for update` on a row that is
54
+ // not there locks NOTHING, and would serialise nothing. The `points <= 0` refusal below is
55
+ // therefore load-bearing rather than a convenience — it is what guarantees that by the time
56
+ // any claim proceeds past this point, there is a row holding the lock. Do not reorder it, and
57
+ // do not make the ledger issue against a player it has never credited.
58
+ const state = await this.readState(tx, roundId, player, true);
59
+ if (!state)
60
+ return { ok: false, refuse: 'claim.no_such_round' };
61
+ // Inside the same player lock as the balance calculation. Two replicas handling one retried
62
+ // HTTP request therefore converge on one reservation instead of racing an uncommitted row.
63
+ const existing = await this.findClaimWith(tx, roundId, player, requestId);
64
+ if (existing) {
65
+ if (existing.requestedWei !== upToWei)
66
+ return { ok: false, refuse: 'claim.request_mismatch' };
67
+ return { ok: true, claim: existing };
68
+ }
69
+ // Existing requests deliberately win over the clock: they were issued under the earlier
70
+ // call's window. Only a genuinely new reservation is subject to the current instant.
71
+ if (at < state.opensAt)
72
+ return { ok: false, refuse: 'claim.not_open_yet' };
73
+ if (at >= state.closesAt)
74
+ return { ok: false, refuse: 'claim.window_closed' };
75
+ if (state.points <= 0n)
76
+ return { ok: false, refuse: 'claim.nothing_earned' };
77
+ const available = availableFrom(state);
78
+ if (available <= 0n)
79
+ return { ok: false, refuse: 'claim.nothing_available' };
80
+ const wei = upToWei < available ? upToWei : available;
81
+ if (wei <= 0n)
82
+ return { ok: false, refuse: 'claim.nothing_available' };
83
+ // Record the hold BEFORE anything signs against it. A signature that exists without a hold is
84
+ // allowance nobody is tracking; a hold without a signature is merely a refund waiting to
85
+ // happen. Only one of those two failures costs money.
86
+ const inserted = await tx.query(`insert into claims (round_id, player, request_id, requested_wei, wei, status, deadline)
87
+ values ($1, $2, $3, $4, $5, 'held', $6)
88
+ returning nonce::text, player, request_id, requested_wei::text, wei::text, deadline::text,
89
+ status, signature, signer`, [roundId, player, requestId, upToWei.toString(), wei.toString(), deadline.toString()]);
90
+ const created = inserted.rows[0];
91
+ if (!created)
92
+ throw new Error(`${roundId}: claim reservation disappeared before commit`);
93
+ return { ok: true, claim: claimFrom(created) };
94
+ });
95
+ }
96
+ /** Store exactly one signature and return the stored winner to every concurrent signer. */
97
+ async completeClaim(nonce, signature, signer) {
98
+ return inTransaction(this.pool, async (tx) => {
99
+ const current = await this.findClaimByNonce(tx, nonce, true);
100
+ if (!current || current.status !== 'held')
101
+ return current?.signature ? current : null;
102
+ if (!current.signature) {
103
+ await tx.query(`update claims set signature = $2, signer = $3 where nonce = $1`, [
104
+ nonce.toString(),
105
+ signature,
106
+ signer,
107
+ ]);
108
+ }
109
+ return this.findClaimByNonce(tx, nonce);
110
+ });
111
+ }
112
+ /**
113
+ * Release only a reservation that provably never became a bearer authorisation.
114
+ *
115
+ * A signed payload cannot be revoked. Releasing one because a wallet dialog was declined would
116
+ * let a new nonce spend the same earned allowance while the first payload remained usable.
117
+ */
118
+ async releaseUnsigned(nonce) {
119
+ await this.pool.query(`update claims set status = 'released'
120
+ where nonce = $1 and status = 'held' and signature is null`, [nonce.toString()]);
121
+ }
122
+ /**
123
+ * Record what the chain says a wallet has spent on this pool.
124
+ *
125
+ * Never allowed to go backwards: a read from a lagging node must not undo what a later one saw.
126
+ */
127
+ async recordChainSpend(roundId, player, spentWei) {
128
+ const result = await this.pool.query(`update balances set chain_spent_wei = greatest(chain_spent_wei, $3)
129
+ where round_id = $1 and player = $2 and chain_spent_wei < $3`, [roundId, player, spentWei.toString()]);
130
+ return result.rowCount === 1;
131
+ }
132
+ /** Everyone with a balance in this round, for the reconciler to ask the chain about. */
133
+ async playersIn(roundId) {
134
+ const result = await this.pool.query(`select player from balances where round_id = $1`, [roundId]);
135
+ return result.rows.map((row) => row.player);
136
+ }
137
+ /**
138
+ * Give back every hold whose deadline has passed.
139
+ *
140
+ * An expired authorisation cannot be spent — the contract checks the same deadline — so a hold
141
+ * still sitting against it is allowance the player earned, cannot use, and would never get back.
142
+ * A player who opens the buy dialog and wanders off would otherwise lose it for the round.
143
+ *
144
+ * Returns how many were returned, so a caller can log something worth reading.
145
+ */
146
+ async releaseExpired(nowSeconds) {
147
+ const result = await this.pool.query(`update claims set status = 'released'
148
+ where status = 'held' and deadline < $1
149
+ returning round_id, player`, [nowSeconds]);
150
+ const unique = new Map(result.rows.map((row) => [
151
+ `${row.round_id}\u0000${row.player}`,
152
+ { roundId: row.round_id, player: row.player },
153
+ ]));
154
+ return [...unique.values()];
155
+ }
156
+ async availableWei(roundId, player) {
157
+ return (await this.economyBalance(roundId, player)).availableWei;
158
+ }
159
+ /** The authoritative balance snapshot, derived from the same state and ceiling as `claim`. */
160
+ async economyBalance(roundId, player) {
161
+ const state = await this.readState(this.pool, roundId, player);
162
+ if (!state) {
163
+ return { weiPerPoint: 0n, earnedWei: 0n, heldWei: 0n, spentWei: 0n, availableWei: 0n, holdExpiresAt: null };
164
+ }
165
+ return {
166
+ weiPerPoint: state.weiPerPoint,
167
+ earnedWei: state.points * state.weiPerPoint,
168
+ heldWei: state.heldWei,
169
+ spentWei: state.chainSpentWei,
170
+ availableWei: availableFrom(state),
171
+ // Seconds on the way in, because that is what the contract checks against; milliseconds on
172
+ // the way out, because that is what a browser counts down with.
173
+ holdExpiresAt: state.holdExpiresAtSeconds === null ? null : state.holdExpiresAtSeconds * 1000,
174
+ };
175
+ }
176
+ async readState(q, roundId, player, lock = false) {
177
+ const round = await q.query(`select wei_per_point, wallet_cap_wei, opens_at, closes_at from rounds where id = $1`, [roundId]);
178
+ const row = round.rows[0];
179
+ if (!row)
180
+ return null;
181
+ const balance = await q.query(`select points, chain_spent_wei from balances where round_id = $1 and player = $2${lock ? ' for update' : ''}`, [roundId, player]);
182
+ // Sum and earliest deadline together: they come from the same rows, and asking twice would let
183
+ // a hold expire between the two reads and report a total that no deadline explains.
184
+ const held = await q.query(`select coalesce(sum(wei), 0)::text as total, min(deadline)::text as earliest from claims
185
+ where round_id = $1 and player = $2 and status = 'held'`, [roundId, player]);
186
+ const earliest = held.rows[0]?.earliest;
187
+ return {
188
+ points: toBigInt(balance.rows[0]?.points),
189
+ chainSpentWei: toBigInt(balance.rows[0]?.chain_spent_wei),
190
+ weiPerPoint: toBigInt(row.wei_per_point),
191
+ capWei: toBigInt(row.wallet_cap_wei),
192
+ opensAt: Number(row.opens_at),
193
+ closesAt: Number(row.closes_at),
194
+ heldWei: toBigInt(held.rows[0]?.total),
195
+ holdExpiresAtSeconds: earliest == null ? null : Number(earliest),
196
+ };
197
+ }
198
+ async findClaimWith(q, roundId, player, requestId) {
199
+ const result = await q.query(`select nonce::text, player, request_id, requested_wei::text, wei::text, deadline::text,
200
+ status, signature, signer
201
+ from claims where round_id = $1 and player = $2 and request_id = $3`, [roundId, player, requestId]);
202
+ return result.rows[0] ? claimFrom(result.rows[0]) : null;
203
+ }
204
+ async findClaimByNonce(q, nonce, lock = false) {
205
+ const result = await q.query(`select nonce::text, player, request_id, requested_wei::text, wei::text, deadline::text,
206
+ status, signature, signer
207
+ from claims where nonce = $1${lock ? ' for update' : ''}`, [nonce.toString()]);
208
+ return result.rows[0] ? claimFrom(result.rows[0]) : null;
209
+ }
210
+ }
211
+ const claimFrom = (row) => ({
212
+ nonce: BigInt(row.nonce),
213
+ player: row.player,
214
+ requestId: row.request_id,
215
+ requestedWei: BigInt(row.requested_wei),
216
+ wei: BigInt(row.wei),
217
+ deadline: BigInt(row.deadline),
218
+ status: row.status,
219
+ signature: row.signature,
220
+ signer: row.signer,
221
+ });
222
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { Pool } from 'pg';
2
+ import type { GameRegistration } from '@flayerlabs/gamemode-spec/embed';
3
+ /**
4
+ * Which game a coin plays, and everything the page embedding it needs to know.
5
+ *
6
+ * This is our domain model, not the hosting service's. The host stores bytes and serves them under
7
+ * a policy; deciding that this coin plays this game against this backend is a Flaunch concern.
8
+ */
9
+ export type Registration = GameRegistration;
10
+ /**
11
+ * What we hand the hosting service at upload, for it to compile into headers.
12
+ *
13
+ * Semantic fields, never CSP text: a policy assembled by string concatenation somewhere else is a
14
+ * policy someone can inject into.
15
+ */
16
+ export interface SecurityProfile {
17
+ kind: 'game';
18
+ connect: {
19
+ https: string;
20
+ wss: string;
21
+ };
22
+ }
23
+ export declare class InvalidRegistration extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /** A launch is already pinned to different game bytes or a different gate. */
27
+ export declare class RegistrationConflict extends Error {
28
+ constructor();
29
+ }
30
+ /**
31
+ * Check a gate origin and return exactly an origin.
32
+ *
33
+ * This value becomes a Content Security Policy `connect-src` for an untrusted game, so the rule is
34
+ * that the input must ALREADY be an origin. Anything the URL parser would silently rewrite into
35
+ * one is refused instead, because a validator that quietly fixes its input is a validator that
36
+ * quietly widens policy. That single equality check covers dot-segments (`/a/..`), empty query and
37
+ * fragment markers, empty credentials (`https://@host`), and surrounding whitespace — all of which
38
+ * parse cleanly and none of which anyone meant to type.
39
+ *
40
+ * HTTPS only, with no localhost exception, and neither development path needs one: the offline
41
+ * mock talks to nothing, and a local gate is reached directly by a dev server rather than through
42
+ * the hosting service.
43
+ */
44
+ export declare function gateOrigin(value: unknown): string;
45
+ /** The profile for a registration. The websocket address is derived, never supplied separately. */
46
+ export declare function securityProfile(registration: Pick<Registration, 'gateOrigin'>): SecurityProfile;
47
+ export declare function checkRegistration(entry: Registration): Registration;
48
+ /**
49
+ * Pin a coin to a game. Repeating the same registration is safe; changing it is not.
50
+ */
51
+ export declare function pinGameRegistration(pool: Pool, entry: Registration): Promise<Registration>;
52
+ export declare function resolveGameRegistration(pool: Pool, requestedChainId: number, coin: string): Promise<Registration | null>;
53
+ /** Stop serving a game for a coin without freeing its immutable pin. */
54
+ export declare function disableGameRegistration(pool: Pool, requestedChainId: number, coin: string): Promise<void>;
55
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,8EAA8E;AAC9E,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CA4BjD;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,eAAe,CAU/F;AAyBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAWnE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAehG;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,IAAI,EACV,gBAAgB,EAAE,MAAM,EACxB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA0B9B;AAED,wEAAwE;AACxE,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/G"}
@@ -0,0 +1,164 @@
1
+ export class InvalidRegistration extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'InvalidRegistration';
5
+ }
6
+ }
7
+ /** A launch is already pinned to different game bytes or a different gate. */
8
+ export class RegistrationConflict extends Error {
9
+ constructor() {
10
+ super('coin already has a different game registration');
11
+ this.name = 'RegistrationConflict';
12
+ }
13
+ }
14
+ /**
15
+ * A real, public hostname: at least two labels, ending in an alphabetic suffix.
16
+ *
17
+ * Written to exclude rather than to match. It refuses every IP literal — dotted (`127.0.0.1`),
18
+ * decimal (`2130706433`), hex (`0x7f000001`) and IPv6 (`[::1]`) — along with single-label names
19
+ * like `localhost`, and hostnames with a trailing dot, which are a distinct origin from the same
20
+ * name without one. A gate is a named host on the public internet; anything else is either a
21
+ * mistake or an attempt to point a game at something on the machine serving it.
22
+ */
23
+ const PUBLIC_HOSTNAME = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
24
+ const ADDRESS = /^0x[0-9a-f]{40}$/i;
25
+ const DEPLOY_ID = /^[0-9a-f]{32}$/;
26
+ /** Long enough for anything real, short enough that nothing unbounded reaches the database. */
27
+ const MAX_ID = 128;
28
+ /**
29
+ * Check a gate origin and return exactly an origin.
30
+ *
31
+ * This value becomes a Content Security Policy `connect-src` for an untrusted game, so the rule is
32
+ * that the input must ALREADY be an origin. Anything the URL parser would silently rewrite into
33
+ * one is refused instead, because a validator that quietly fixes its input is a validator that
34
+ * quietly widens policy. That single equality check covers dot-segments (`/a/..`), empty query and
35
+ * fragment markers, empty credentials (`https://@host`), and surrounding whitespace — all of which
36
+ * parse cleanly and none of which anyone meant to type.
37
+ *
38
+ * HTTPS only, with no localhost exception, and neither development path needs one: the offline
39
+ * mock talks to nothing, and a local gate is reached directly by a dev server rather than through
40
+ * the hosting service.
41
+ */
42
+ export function gateOrigin(value) {
43
+ if (typeof value !== 'string')
44
+ throw new InvalidRegistration('gate address must be text');
45
+ if (value.length > 253)
46
+ throw new InvalidRegistration('gate address is too long');
47
+ // Punycode is what `new URL` produces for non-ASCII, so `flаunch.gg` with a Cyrillic а becomes a
48
+ // different domain that reads identically in a console. Refuse it at the door rather than store
49
+ // something nobody can eyeball.
50
+ if (/[^\x20-\x7e]/.test(value))
51
+ throw new InvalidRegistration('gate address must be plain ASCII');
52
+ let url;
53
+ try {
54
+ url = new URL(value);
55
+ }
56
+ catch {
57
+ throw new InvalidRegistration('gate address is not a URL');
58
+ }
59
+ if (url.protocol !== 'https:')
60
+ throw new InvalidRegistration('gate address must start with https://');
61
+ if (!PUBLIC_HOSTNAME.test(url.hostname)) {
62
+ throw new InvalidRegistration('gate address must be a public domain name, not an IP address');
63
+ }
64
+ // Two differences from the canonical origin are forgiven, because both are things a person
65
+ // reasonably types and neither changes what is named: a trailing slash, and an explicit :443,
66
+ // which is the default port `new URL` drops for us.
67
+ if (value.replace(/\/$/, '').replace(/:443$/, '') !== url.origin) {
68
+ throw new InvalidRegistration('gate address must be just the address, with nothing after it');
69
+ }
70
+ return url.origin;
71
+ }
72
+ /** The profile for a registration. The websocket address is derived, never supplied separately. */
73
+ export function securityProfile(registration) {
74
+ // Revalidated here on purpose: this is the value that leaves for another system, and checking it
75
+ // at the boundary costs nothing.
76
+ const origin = gateOrigin(registration.gateOrigin);
77
+ return {
78
+ kind: 'game',
79
+ // Same host, same port, different scheme. Deriving it means the two can never name different
80
+ // servers, which a second hand-entered field eventually would.
81
+ connect: { https: origin, wss: `wss://${new URL(origin).host}` },
82
+ };
83
+ }
84
+ function id(value, what) {
85
+ if (typeof value !== 'string')
86
+ throw new InvalidRegistration(`${what} must be text`);
87
+ const trimmed = value.trim();
88
+ if (!trimmed)
89
+ throw new InvalidRegistration(`${what} is required`);
90
+ if (trimmed.length > MAX_ID)
91
+ throw new InvalidRegistration(`${what} is too long`);
92
+ return trimmed;
93
+ }
94
+ function deployId(value) {
95
+ const checked = id(value, 'deploy');
96
+ if (value !== checked || !DEPLOY_ID.test(checked)) {
97
+ throw new InvalidRegistration('deploy must be a lowercase content hash');
98
+ }
99
+ return checked;
100
+ }
101
+ function chainId(value) {
102
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
103
+ throw new InvalidRegistration('chain id must be a positive safe integer');
104
+ }
105
+ return value;
106
+ }
107
+ export function checkRegistration(entry) {
108
+ if (typeof entry?.coin !== 'string' || !ADDRESS.test(entry.coin)) {
109
+ throw new InvalidRegistration('coin must be a wallet address');
110
+ }
111
+ return {
112
+ chainId: chainId(entry.chainId),
113
+ coin: entry.coin.toLowerCase(),
114
+ gameId: id(entry.gameId, 'game'),
115
+ deployId: deployId(entry.deployId),
116
+ gateOrigin: gateOrigin(entry.gateOrigin),
117
+ };
118
+ }
119
+ /**
120
+ * Pin a coin to a game. Repeating the same registration is safe; changing it is not.
121
+ */
122
+ export async function pinGameRegistration(pool, entry) {
123
+ const checked = checkRegistration(entry);
124
+ const result = await pool.query(`insert into registrations (chain_id, coin, game_id, gate_origin, deploy_id) values ($1, $2, $3, $4, $5)
125
+ on conflict (chain_id, coin) do update set
126
+ updated_at = registrations.updated_at
127
+ where registrations.disabled_at is null
128
+ and registrations.game_id = excluded.game_id
129
+ and registrations.gate_origin = excluded.gate_origin
130
+ and registrations.deploy_id = excluded.deploy_id
131
+ returning coin`, [checked.chainId, checked.coin, checked.gameId, checked.gateOrigin, checked.deployId]);
132
+ if (result.rows.length === 0)
133
+ throw new RegistrationConflict();
134
+ return checked;
135
+ }
136
+ export async function resolveGameRegistration(pool, requestedChainId, coin) {
137
+ const checkedChainId = chainId(requestedChainId);
138
+ if (typeof coin !== 'string' || !ADDRESS.test(coin)) {
139
+ throw new InvalidRegistration('coin must be a wallet address');
140
+ }
141
+ const result = await pool.query(`select chain_id::text, coin, game_id, gate_origin, deploy_id
142
+ from registrations where chain_id = $1 and coin = $2 and disabled_at is null`, [checkedChainId, coin.toLowerCase()]);
143
+ const row = result.rows[0];
144
+ return row
145
+ ? checkRegistration({
146
+ chainId: Number(row.chain_id),
147
+ coin: row.coin,
148
+ gameId: row.game_id,
149
+ gateOrigin: row.gate_origin,
150
+ deployId: row.deploy_id,
151
+ })
152
+ : null;
153
+ }
154
+ /** Stop serving a game for a coin without freeing its immutable pin. */
155
+ export async function disableGameRegistration(pool, requestedChainId, coin) {
156
+ const checkedChainId = chainId(requestedChainId);
157
+ if (typeof coin !== 'string' || !ADDRESS.test(coin)) {
158
+ throw new InvalidRegistration('coin must be a wallet address');
159
+ }
160
+ await pool.query(`update registrations
161
+ set disabled_at = coalesce(disabled_at, now()), updated_at = now()
162
+ where chain_id = $1 and coin = $2`, [checkedChainId, coin.toLowerCase()]);
163
+ }
164
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}