@chaos-overlords/kernel 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 (104) hide show
  1. package/LICENSE +674 -0
  2. package/NOTICE +21 -0
  3. package/README.md +28 -0
  4. package/dist/domain/entities.d.ts +91 -0
  5. package/dist/domain/entities.d.ts.map +1 -0
  6. package/dist/domain/entities.js +9 -0
  7. package/dist/domain/entities.js.map +1 -0
  8. package/dist/domain/errors.d.ts +30 -0
  9. package/dist/domain/errors.d.ts.map +1 -0
  10. package/dist/domain/errors.js +45 -0
  11. package/dist/domain/errors.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +7 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/logic/canonical-json.d.ts +17 -0
  17. package/dist/logic/canonical-json.d.ts.map +1 -0
  18. package/dist/logic/canonical-json.js +44 -0
  19. package/dist/logic/canonical-json.js.map +1 -0
  20. package/dist/logic/crypto.d.ts +29 -0
  21. package/dist/logic/crypto.d.ts.map +1 -0
  22. package/dist/logic/crypto.js +76 -0
  23. package/dist/logic/crypto.js.map +1 -0
  24. package/dist/logic/index.d.ts +5 -0
  25. package/dist/logic/index.d.ts.map +1 -0
  26. package/dist/logic/index.js +5 -0
  27. package/dist/logic/index.js.map +1 -0
  28. package/dist/logic/password.d.ts +4 -0
  29. package/dist/logic/password.d.ts.map +1 -0
  30. package/dist/logic/password.js +38 -0
  31. package/dist/logic/password.js.map +1 -0
  32. package/dist/logic/turn-logic.d.ts +52 -0
  33. package/dist/logic/turn-logic.d.ts.map +1 -0
  34. package/dist/logic/turn-logic.js +101 -0
  35. package/dist/logic/turn-logic.js.map +1 -0
  36. package/dist/ports/index.d.ts +3 -0
  37. package/dist/ports/index.d.ts.map +1 -0
  38. package/dist/ports/index.js +3 -0
  39. package/dist/ports/index.js.map +1 -0
  40. package/dist/ports/runtime.d.ts +38 -0
  41. package/dist/ports/runtime.d.ts.map +1 -0
  42. package/dist/ports/runtime.js +7 -0
  43. package/dist/ports/runtime.js.map +1 -0
  44. package/dist/ports/storage.d.ts +141 -0
  45. package/dist/ports/storage.d.ts.map +1 -0
  46. package/dist/ports/storage.js +2 -0
  47. package/dist/ports/storage.js.map +1 -0
  48. package/dist/services/AuthService.d.ts +13 -0
  49. package/dist/services/AuthService.d.ts.map +1 -0
  50. package/dist/services/AuthService.js +24 -0
  51. package/dist/services/AuthService.js.map +1 -0
  52. package/dist/services/EventPublisher.d.ts +20 -0
  53. package/dist/services/EventPublisher.d.ts.map +1 -0
  54. package/dist/services/EventPublisher.js +37 -0
  55. package/dist/services/EventPublisher.js.map +1 -0
  56. package/dist/services/LobbyService.d.ts +45 -0
  57. package/dist/services/LobbyService.d.ts.map +1 -0
  58. package/dist/services/LobbyService.js +366 -0
  59. package/dist/services/LobbyService.js.map +1 -0
  60. package/dist/services/MatchQueryService.d.ts +24 -0
  61. package/dist/services/MatchQueryService.d.ts.map +1 -0
  62. package/dist/services/MatchQueryService.js +99 -0
  63. package/dist/services/MatchQueryService.js.map +1 -0
  64. package/dist/services/RateLimiter.d.ts +28 -0
  65. package/dist/services/RateLimiter.d.ts.map +1 -0
  66. package/dist/services/RateLimiter.js +48 -0
  67. package/dist/services/RateLimiter.js.map +1 -0
  68. package/dist/services/RetentionService.d.ts +23 -0
  69. package/dist/services/RetentionService.d.ts.map +1 -0
  70. package/dist/services/RetentionService.js +36 -0
  71. package/dist/services/RetentionService.js.map +1 -0
  72. package/dist/services/SnapshotService.d.ts +40 -0
  73. package/dist/services/SnapshotService.d.ts.map +1 -0
  74. package/dist/services/SnapshotService.js +137 -0
  75. package/dist/services/SnapshotService.js.map +1 -0
  76. package/dist/services/TurnService.d.ts +63 -0
  77. package/dist/services/TurnService.d.ts.map +1 -0
  78. package/dist/services/TurnService.js +392 -0
  79. package/dist/services/TurnService.js.map +1 -0
  80. package/dist/services/createKernel.d.ts +23 -0
  81. package/dist/services/createKernel.d.ts.map +1 -0
  82. package/dist/services/createKernel.js +22 -0
  83. package/dist/services/createKernel.js.map +1 -0
  84. package/dist/services/deps.d.ts +11 -0
  85. package/dist/services/deps.d.ts.map +1 -0
  86. package/dist/services/deps.js +2 -0
  87. package/dist/services/deps.js.map +1 -0
  88. package/dist/services/index.d.ts +10 -0
  89. package/dist/services/index.d.ts.map +1 -0
  90. package/dist/services/index.js +10 -0
  91. package/dist/services/index.js.map +1 -0
  92. package/dist/testing/InMemoryStorage.d.ts +28 -0
  93. package/dist/testing/InMemoryStorage.d.ts.map +1 -0
  94. package/dist/testing/InMemoryStorage.js +279 -0
  95. package/dist/testing/InMemoryStorage.js.map +1 -0
  96. package/dist/testing/fakes.d.ts +38 -0
  97. package/dist/testing/fakes.d.ts.map +1 -0
  98. package/dist/testing/fakes.js +44 -0
  99. package/dist/testing/fakes.js.map +1 -0
  100. package/dist/testing/index.d.ts +3 -0
  101. package/dist/testing/index.d.ts.map +1 -0
  102. package/dist/testing/index.js +3 -0
  103. package/dist/testing/index.js.map +1 -0
  104. package/package.json +68 -0
@@ -0,0 +1,366 @@
1
+ import { LIMITS, } from '@chaos-overlords/contracts';
2
+ import { activePlayers } from '../domain/entities';
3
+ import { ConflictError, ForbiddenError, NotFoundError, UnauthorizedError } from '../domain/errors';
4
+ import { generateJoinCode, generateSeed, generateToken, hashToken } from '../logic/crypto';
5
+ import { hashPassword, verifyPassword } from '../logic/password';
6
+ import { assignSlots } from '../logic/turn-logic';
7
+ import { MatchQueryService, toPlayerView } from './MatchQueryService';
8
+ import { FIRST_TURN } from './TurnService';
9
+ const JOIN_CODE_LENGTH = LIMITS.joinCodeLength;
10
+ const MIN_PLAYERS_TO_START = LIMITS.minPlayers;
11
+ /** Attempts at an unused join code. The space is ~40 bits, so a second attempt is already rare. */
12
+ const JOIN_CODE_ATTEMPTS = 5;
13
+ /** The host always holds the first position in the join sequence. */
14
+ const HOST_JOIN_ORDER = 0;
15
+ /** Match creation, joining, leaving, kicking and the start transition. */
16
+ export class LobbyService {
17
+ deps;
18
+ publisher;
19
+ turns;
20
+ query;
21
+ newId;
22
+ constructor(deps, publisher, turns, options = {}) {
23
+ this.deps = deps;
24
+ this.publisher = publisher;
25
+ this.turns = turns;
26
+ this.query = new MatchQueryService(deps.storage);
27
+ this.newId = options.newId ?? (() => crypto.randomUUID());
28
+ }
29
+ async createMatch(request) {
30
+ const now = this.deps.clock.now();
31
+ const matchId = this.newId();
32
+ const hostId = this.newId();
33
+ const token = generateToken();
34
+ const passwordHash = request.password ? await hashPassword(request.password) : null;
35
+ const match = await this.createWithFreshJoinCode({
36
+ id: matchId,
37
+ status: 'lobby',
38
+ settings: request.settings,
39
+ hostPlayerId: hostId,
40
+ joinCode: '',
41
+ passwordHash,
42
+ seed: null,
43
+ currentTurn: 0,
44
+ seatCount: 1,
45
+ joinCounter: 1,
46
+ createdAt: now,
47
+ updatedAt: now,
48
+ });
49
+ const host = {
50
+ id: hostId,
51
+ matchId,
52
+ slot: -1,
53
+ joinOrder: HOST_JOIN_ORDER,
54
+ displayName: request.hostDisplayName,
55
+ tokenHash: await hashToken(token),
56
+ status: 'active',
57
+ joinedAt: now,
58
+ };
59
+ // The match was inserted a statement ago and is in the lobby, so this cannot legitimately fail;
60
+ // treating it as a conflict rather than ignoring it keeps the host's token from being handed out
61
+ // for a row that does not exist.
62
+ if (!(await this.deps.storage.players.create(host))) {
63
+ throw new ConflictError('The match could not be opened; retry', { reason: 'match_not_open' });
64
+ }
65
+ await this.publisher.publish(matchId, {
66
+ type: 'lobby.playerJoined',
67
+ payload: { player: toPlayerView(host, hostId) },
68
+ });
69
+ return this.membership(match, host, token);
70
+ }
71
+ async join(request) {
72
+ const match = await this.deps.storage.matches.getByJoinCode(request.joinCode);
73
+ if (!match)
74
+ throw new NotFoundError('No match with that join code', { reason: 'unknown_join_code' });
75
+ if (match.status !== 'lobby') {
76
+ throw new ConflictError('The match has already started', { reason: 'match_not_joinable' });
77
+ }
78
+ if (match.passwordHash !== null) {
79
+ if (!request.password) {
80
+ throw new UnauthorizedError('This match needs a password', { reason: 'password_required' });
81
+ }
82
+ if (!(await verifyPassword(request.password, match.passwordHash))) {
83
+ throw new UnauthorizedError('Wrong password', { reason: 'wrong_password' });
84
+ }
85
+ }
86
+ const joinOrder = await this.deps.storage.matches.claimSeat(match.id);
87
+ if (joinOrder === null) {
88
+ throw new ConflictError('The match is full or no longer joinable', { reason: 'match_full' });
89
+ }
90
+ const token = generateToken();
91
+ const player = {
92
+ id: this.newId(),
93
+ matchId: match.id,
94
+ slot: -1,
95
+ joinOrder,
96
+ displayName: request.displayName,
97
+ tokenHash: await hashToken(token),
98
+ status: 'active',
99
+ joinedAt: this.deps.clock.now(),
100
+ };
101
+ // Everything after the seat is claimed has to give it back on failure, or capacity drifts and a
102
+ // phantom member keeps the turn barrier waiting for a player nobody can authenticate as. The
103
+ // insert is itself conditional on the match still being in the lobby: the host may have pressed
104
+ // start between the claim and here, and an unseated player in a running match would wedge it.
105
+ try {
106
+ const seated = await this.deps.storage.players.create(player);
107
+ if (!seated) {
108
+ throw new ConflictError('The match started while you were joining', {
109
+ reason: 'match_not_joinable',
110
+ });
111
+ }
112
+ const membership = await this.membership(match, player, token);
113
+ await this.publisher.publish(match.id, {
114
+ type: 'lobby.playerJoined',
115
+ payload: { player: toPlayerView(player, match.hostPlayerId) },
116
+ });
117
+ return membership;
118
+ }
119
+ catch (error) {
120
+ await this.rollbackJoin(match.id, player.id);
121
+ throw error;
122
+ }
123
+ }
124
+ async leave(principal) {
125
+ await this.remove(principal.match, principal.player, 'left');
126
+ }
127
+ async kick(principal, targetPlayerId) {
128
+ const { match, player } = principal;
129
+ requireHost(principal);
130
+ if (targetPlayerId === player.id) {
131
+ throw new ConflictError('Leave the match instead of kicking yourself', {
132
+ reason: 'self_kick',
133
+ });
134
+ }
135
+ const target = await this.deps.storage.players.get(targetPlayerId);
136
+ if (!target || target.matchId !== match.id) {
137
+ throw new NotFoundError('No such player in this match', { reason: 'unknown_player' });
138
+ }
139
+ await this.remove(match, target, 'kicked');
140
+ }
141
+ /**
142
+ * Record one present player's latest choice. AI control is deliberately unanimous: a single
143
+ * `wait` vote preserves the human seat, and there is no timeout that silently changes it.
144
+ */
145
+ async voteOnTakeover(principal, targetPlayerId, request) {
146
+ const { match, player } = principal;
147
+ if (match.status !== 'running' && match.status !== 'desynced') {
148
+ throw new ConflictError('The match is not in progress', { reason: 'match_not_running' });
149
+ }
150
+ const currentVoter = await this.deps.storage.players.get(player.id);
151
+ if (currentVoter?.status !== 'active') {
152
+ throw new ForbiddenError('Only present players may vote', { reason: 'not_active' });
153
+ }
154
+ const target = await this.deps.storage.players.get(targetPlayerId);
155
+ if (!target || target.matchId !== match.id) {
156
+ throw new NotFoundError('No such player in this match', { reason: 'unknown_player' });
157
+ }
158
+ if (!['takeoverPending', 'left', 'kicked'].includes(target.status)) {
159
+ throw new ConflictError('That player is not awaiting a takeover vote', {
160
+ reason: 'takeover_not_pending',
161
+ });
162
+ }
163
+ await this.publisher.publish(match.id, {
164
+ type: 'match.takeoverVoteCast',
165
+ payload: { playerId: target.id, voterPlayerId: player.id, decision: request.decision },
166
+ });
167
+ if (request.decision === 'wait')
168
+ return;
169
+ const votes = await this.currentTakeoverVotes(match.id, target.id);
170
+ const voters = activePlayers(await this.deps.storage.players.listByMatch(match.id));
171
+ if (voters.length === 0 || voters.some((voter) => votes.get(voter.id) !== 'computer'))
172
+ return;
173
+ if (!(await this.deps.storage.players.transitionStatus(target.id, [target.status], 'computer'))) {
174
+ return;
175
+ }
176
+ await this.deps.storage.players.revokeToken(target.id);
177
+ await this.publisher.publish(match.id, {
178
+ type: 'match.playerTakenOver',
179
+ payload: { playerId: target.id },
180
+ });
181
+ if (target.id === match.hostPlayerId) {
182
+ const successor = activePlayers(await this.deps.storage.players.listByMatch(match.id))[0];
183
+ if (successor) {
184
+ await this.deps.storage.matches.transition(match.id, ['running', 'desynced'], {
185
+ hostPlayerId: successor.id,
186
+ updatedAt: this.deps.clock.now(),
187
+ });
188
+ await this.publisher.publish(match.id, {
189
+ type: 'lobby.hostChanged',
190
+ payload: { hostPlayerId: successor.id },
191
+ });
192
+ }
193
+ }
194
+ await this.turns.reevaluate(match.id);
195
+ }
196
+ async start(principal) {
197
+ const { match } = principal;
198
+ requireHost(principal);
199
+ if (match.status !== 'lobby') {
200
+ throw new ConflictError('The match has already started', { reason: 'match_not_in_lobby' });
201
+ }
202
+ const roster = activePlayers(await this.deps.storage.players.listByMatch(match.id));
203
+ if (roster.length < MIN_PLAYERS_TO_START) {
204
+ throw new ConflictError(`At least ${MIN_PLAYERS_TO_START} players are needed`, {
205
+ reason: 'not_enough_players',
206
+ });
207
+ }
208
+ const seed = generateSeed();
209
+ const started = await this.deps.storage.matches.transition(match.id, ['lobby'], {
210
+ status: 'running',
211
+ seed,
212
+ currentTurn: 0,
213
+ updatedAt: this.deps.clock.now(),
214
+ });
215
+ if (!started) {
216
+ throw new ConflictError('The match has already started', { reason: 'match_not_in_lobby' });
217
+ }
218
+ // Seats can no longer be claimed, so the roster is final from here.
219
+ const finalRoster = await this.deps.storage.players.listByMatch(match.id);
220
+ await this.deps.storage.players.assignSlots(assignSlots(finalRoster, match.hostPlayerId));
221
+ const seated = await this.deps.storage.players.listByMatch(match.id);
222
+ await this.publisher.publish(match.id, {
223
+ type: 'match.started',
224
+ payload: {
225
+ seed,
226
+ players: activePlayers(seated).map((player) => toPlayerView(player, match.hostPlayerId)),
227
+ },
228
+ });
229
+ await this.turns.openTurn({ ...match, status: 'running', seed }, FIRST_TURN);
230
+ }
231
+ async remove(match, target, reason) {
232
+ if (target.status !== 'active')
233
+ return;
234
+ const now = this.deps.clock.now();
235
+ if (match.status === 'lobby') {
236
+ if (target.id === match.hostPlayerId) {
237
+ await this.abandon(match, now);
238
+ return;
239
+ }
240
+ await this.deps.storage.players.delete(target.id);
241
+ await this.deps.storage.matches.releaseSeat(match.id);
242
+ await this.publisher.publish(match.id, {
243
+ type: 'lobby.playerLeft',
244
+ payload: { playerId: target.id, reason },
245
+ });
246
+ return;
247
+ }
248
+ await this.deps.storage.players.setStatus(target.id, reason);
249
+ // Membership is the only thing the token ever proved, so it stops working here: a kicked player
250
+ // keeps neither the event stream nor the sealed order sets of the turns that follow.
251
+ await this.deps.storage.players.revokeToken(target.id);
252
+ await this.publisher.publish(match.id, {
253
+ type: 'lobby.playerLeft',
254
+ payload: { playerId: target.id, reason },
255
+ });
256
+ const remaining = activePlayers(await this.deps.storage.players.listByMatch(match.id));
257
+ if (remaining.length === 0) {
258
+ await this.abandon(match, now);
259
+ return;
260
+ }
261
+ await this.publisher.publish(match.id, {
262
+ type: 'match.takeoverVoteRequested',
263
+ payload: { playerId: target.id, turn: match.currentTurn },
264
+ });
265
+ if (target.id === match.hostPlayerId) {
266
+ const successor = remaining[0];
267
+ await this.deps.storage.matches.transition(match.id, ['running', 'desynced'], {
268
+ hostPlayerId: successor.id,
269
+ updatedAt: now,
270
+ });
271
+ await this.publisher.publish(match.id, {
272
+ type: 'lobby.hostChanged',
273
+ payload: { hostPlayerId: successor.id },
274
+ });
275
+ }
276
+ // A departure can complete readiness or a consensus that was waiting on the leaver.
277
+ await this.turns.reevaluate(match.id);
278
+ }
279
+ async currentTakeoverVotes(matchId, targetPlayerId) {
280
+ const votes = new Map();
281
+ let requested = false;
282
+ let after = 0;
283
+ for (;;) {
284
+ const page = await this.deps.storage.events.listAfter(matchId, after, 200);
285
+ if (page.length === 0)
286
+ return votes;
287
+ for (const event of page) {
288
+ after = event.seq;
289
+ if (event.type === 'match.takeoverVoteRequested' &&
290
+ event.payload.playerId === targetPlayerId) {
291
+ requested = true;
292
+ votes.clear();
293
+ }
294
+ else if ((event.type === 'match.takeoverVoteCancelled' ||
295
+ event.type === 'match.playerTakenOver') &&
296
+ event.payload.playerId === targetPlayerId) {
297
+ requested = false;
298
+ votes.clear();
299
+ }
300
+ else if (requested &&
301
+ event.type === 'match.takeoverVoteCast' &&
302
+ event.payload.playerId === targetPlayerId) {
303
+ votes.set(event.payload.voterPlayerId, event.payload.decision);
304
+ }
305
+ }
306
+ if (page.length < 200)
307
+ return votes;
308
+ }
309
+ }
310
+ /**
311
+ * Undo a join that could not be completed. The token was never returned, so the row is
312
+ * unreachable: leaving it would hold a seat and keep `allActiveReady` waiting forever on a player
313
+ * who does not exist. Deleting the row is safe precisely because nobody ever held its token.
314
+ */
315
+ async rollbackJoin(matchId, playerId) {
316
+ try {
317
+ await this.deps.storage.players.delete(playerId);
318
+ await this.deps.storage.matches.releaseSeat(matchId);
319
+ }
320
+ catch (error) {
321
+ this.deps.logger.error('could not roll back an incomplete join', {
322
+ matchId,
323
+ playerId,
324
+ error: String(error),
325
+ });
326
+ }
327
+ }
328
+ async abandon(match, now) {
329
+ const changed = await this.deps.storage.matches.transition(match.id, ['lobby', 'running', 'desynced'], { status: 'abandoned', updatedAt: now });
330
+ if (changed) {
331
+ await this.publisher.publish(match.id, {
332
+ type: 'match.statusChanged',
333
+ payload: { status: 'abandoned' },
334
+ });
335
+ }
336
+ }
337
+ /**
338
+ * Inserts the match with a join code no other match holds. The uniqueness is the database's to
339
+ * enforce, not ours to check first: a `getByJoinCode` probe before the insert would be a race, so
340
+ * a refused insert is what drives the retry.
341
+ */
342
+ async createWithFreshJoinCode(draft) {
343
+ for (let attempt = 0; attempt < JOIN_CODE_ATTEMPTS; attempt += 1) {
344
+ const match = { ...draft, joinCode: generateJoinCode(JOIN_CODE_LENGTH) };
345
+ if (await this.deps.storage.matches.create(match))
346
+ return match;
347
+ }
348
+ throw new ConflictError('Could not allocate a join code; retry', {
349
+ reason: 'join_code_exhausted',
350
+ });
351
+ }
352
+ async membership(match, player, token) {
353
+ return {
354
+ match: await this.query.view(match),
355
+ player: toPlayerView(player, match.hostPlayerId),
356
+ token,
357
+ joinCode: match.joinCode,
358
+ };
359
+ }
360
+ }
361
+ function requireHost(principal) {
362
+ if (principal.player.id !== principal.match.hostPlayerId) {
363
+ throw new ForbiddenError('Only the host can do that', { reason: 'host_only' });
364
+ }
365
+ }
366
+ //# sourceMappingURL=LobbyService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LobbyService.js","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { LobbyListing, MatchView, OwnSubmissionView, PlayerView, SealedOrdersView } from '@chaos-overlords/contracts';
2
+ import type { Match, Player } from '../domain/entities';
3
+ import type { MultiplayerStorage } from '../ports/storage';
4
+ export declare function toPlayerView(player: Player, hostPlayerId: string): PlayerView;
5
+ /** Read models. Every view is assembled from list reads, never one query per player. */
6
+ export declare class MatchQueryService {
7
+ private readonly storage;
8
+ constructor(storage: MultiplayerStorage);
9
+ view(match: Match): Promise<MatchView>;
10
+ private turnView;
11
+ listPublicLobbies(limit: number): Promise<LobbyListing[]>;
12
+ ownSubmission(match: Match, playerId: string, number: number): Promise<OwnSubmissionView>;
13
+ /**
14
+ * The full order set of a turn, only once sealed: before that, other players' plans are secret.
15
+ *
16
+ * The participants come from the set the seal froze, never from the roster as it stands now. That
17
+ * is what makes the response re-hash to the `orderSetHash` it is served with: someone leaving
18
+ * between the submission and the seal (their orders are excluded while takeover is voted on) or
19
+ * after it (their orders stay in) changes the roster but not this set.
20
+ */
21
+ sealedOrders(match: Match, number: number): Promise<SealedOrdersView>;
22
+ private requireTurn;
23
+ }
24
+ //# sourceMappingURL=MatchQueryService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MatchQueryService.d.ts","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,gBAAgB,EAGjB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAQ,MAAM,oBAAoB,CAAA;AAE7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU,CAQ7E;AAED,wFAAwF;AACxF,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC;YAsB9B,QAAQ;IAoBtB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAInD,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAM/F;;;;;;;OAOG;IACG,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAmB7D,WAAW;CAK1B"}
@@ -0,0 +1,99 @@
1
+ import { ConflictError, NotFoundError } from '../domain/errors';
2
+ export function toPlayerView(player, hostPlayerId) {
3
+ return {
4
+ id: player.id,
5
+ slot: player.slot,
6
+ displayName: player.displayName,
7
+ status: player.status,
8
+ isHost: player.id === hostPlayerId,
9
+ };
10
+ }
11
+ /** Read models. Every view is assembled from list reads, never one query per player. */
12
+ export class MatchQueryService {
13
+ storage;
14
+ constructor(storage) {
15
+ this.storage = storage;
16
+ }
17
+ async view(match) {
18
+ const players = await this.storage.players.listByMatch(match.id);
19
+ const [turn, previousTurn, lastEventSeq] = await Promise.all([
20
+ this.turnView(match.id, match.currentTurn),
21
+ this.turnView(match.id, match.currentTurn - 1),
22
+ this.storage.events.lastSeq(match.id),
23
+ ]);
24
+ return {
25
+ id: match.id,
26
+ status: match.status,
27
+ settings: match.settings,
28
+ hostPlayerId: match.hostPlayerId,
29
+ seed: match.seed,
30
+ currentTurn: match.currentTurn,
31
+ players: players.map((player) => toPlayerView(player, match.hostPlayerId)),
32
+ turn,
33
+ previousTurn,
34
+ lastEventSeq,
35
+ createdAt: match.createdAt.toISOString(),
36
+ };
37
+ }
38
+ async turnView(matchId, number) {
39
+ if (number < 1)
40
+ return null;
41
+ const turn = await this.storage.turns.get(matchId, number);
42
+ if (!turn)
43
+ return null;
44
+ const [orders, reports] = await Promise.all([
45
+ this.storage.turns.listOrders(matchId, number),
46
+ this.storage.turns.listReports(matchId, number),
47
+ ]);
48
+ return {
49
+ number: turn.number,
50
+ status: turn.status,
51
+ openedAt: turn.openedAt.toISOString(),
52
+ deadlineAt: turn.deadlineAt?.toISOString() ?? null,
53
+ sealedAt: turn.sealedAt?.toISOString() ?? null,
54
+ orderSetHash: turn.orderSetHash,
55
+ readyPlayerIds: orders.filter((row) => row.ready).map((row) => row.playerId),
56
+ reportedPlayerIds: reports.map((report) => report.playerId),
57
+ };
58
+ }
59
+ listPublicLobbies(limit) {
60
+ return this.storage.matches.listPublicLobbies(limit);
61
+ }
62
+ async ownSubmission(match, playerId, number) {
63
+ const row = await this.storage.turns.getOrders(match.id, number, playerId);
64
+ if (!row)
65
+ throw new NotFoundError('No such turn', { reason: 'unknown_turn' });
66
+ return { turn: number, orders: row.orders, ready: row.ready, ordersHash: row.ordersHash };
67
+ }
68
+ /**
69
+ * The full order set of a turn, only once sealed: before that, other players' plans are secret.
70
+ *
71
+ * The participants come from the set the seal froze, never from the roster as it stands now. That
72
+ * is what makes the response re-hash to the `orderSetHash` it is served with: someone leaving
73
+ * between the submission and the seal (their orders are excluded while takeover is voted on) or
74
+ * after it (their orders stay in) changes the roster but not this set.
75
+ */
76
+ async sealedOrders(match, number) {
77
+ const turn = await this.requireTurn(match.id, number);
78
+ if (turn.status === 'open' || turn.orderSetHash === null || turn.sealedSlots === null) {
79
+ throw new ConflictError('The turn has not been sealed yet', { reason: 'turn_open' });
80
+ }
81
+ const orders = new Map((await this.storage.turns.listOrders(match.id, number)).map((row) => [row.playerId, row]));
82
+ const rows = [];
83
+ for (const { playerId, slot } of [...turn.sealedSlots].sort((a, b) => a.slot - b.slot)) {
84
+ const row = orders.get(playerId);
85
+ if (!row || row.orders === null || row.ordersHash === null) {
86
+ throw new Error(`sealed turn ${match.id}/${number} is missing orders for ${playerId}`);
87
+ }
88
+ rows.push({ playerId, slot, orders: row.orders, ordersHash: row.ordersHash });
89
+ }
90
+ return { turn: number, orderSetHash: turn.orderSetHash, players: rows };
91
+ }
92
+ async requireTurn(matchId, number) {
93
+ const turn = await this.storage.turns.get(matchId, number);
94
+ if (!turn)
95
+ throw new NotFoundError('No such turn', { reason: 'unknown_turn' });
96
+ return turn;
97
+ }
98
+ }
99
+ //# sourceMappingURL=MatchQueryService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MatchQueryService.js","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { Clock } from '../ports/runtime';
2
+ /**
3
+ * Fixed-window counter per key, in memory. It protects the unauthenticated doors (create, join)
4
+ * from brute force on one process; a horizontally scaled deployment puts a shared limiter (a WAF
5
+ * rule, Cloudflare's rate limiting) in front and treats this as the last line.
6
+ */
7
+ export declare class RateLimiter {
8
+ private readonly clock;
9
+ private readonly options;
10
+ private readonly windows;
11
+ private lastPrune;
12
+ constructor(clock: Clock, options: {
13
+ limit: number;
14
+ windowMs: number;
15
+ });
16
+ /** Returns the retry delay in seconds when over the limit, or null when the call is allowed. */
17
+ take(key: string): number | null;
18
+ /**
19
+ * Drop windows that have expired, at most once per window period.
20
+ *
21
+ * The sweep is O(size), so running it on every new key would be quadratic exactly when the map is
22
+ * busiest — a burst of distinct keys is both what fills the map and what triggers the sweep. The
23
+ * time guard makes the amortised cost one pass per window regardless of traffic, and expired
24
+ * entries can never outlive two windows.
25
+ */
26
+ private prune;
27
+ }
28
+ //# sourceMappingURL=RateLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C;;;;GAIG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;IACpF,OAAO,CAAC,SAAS,CAA2B;gBAGzB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE;IAG/D,gGAAgG;IAChG,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAehC;;;;;;;OAOG;IACH,OAAO,CAAC,KAAK;CAOd"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Fixed-window counter per key, in memory. It protects the unauthenticated doors (create, join)
3
+ * from brute force on one process; a horizontally scaled deployment puts a shared limiter (a WAF
4
+ * rule, Cloudflare's rate limiting) in front and treats this as the last line.
5
+ */
6
+ export class RateLimiter {
7
+ clock;
8
+ options;
9
+ windows = new Map();
10
+ lastPrune = Number.NEGATIVE_INFINITY;
11
+ constructor(clock, options) {
12
+ this.clock = clock;
13
+ this.options = options;
14
+ }
15
+ /** Returns the retry delay in seconds when over the limit, or null when the call is allowed. */
16
+ take(key) {
17
+ const now = this.clock.now().getTime();
18
+ const entry = this.windows.get(key);
19
+ if (!entry || now - entry.windowStart >= this.options.windowMs) {
20
+ this.windows.set(key, { windowStart: now, count: 1 });
21
+ this.prune(now);
22
+ return null;
23
+ }
24
+ if (entry.count >= this.options.limit) {
25
+ return Math.ceil((entry.windowStart + this.options.windowMs - now) / 1000);
26
+ }
27
+ entry.count += 1;
28
+ return null;
29
+ }
30
+ /**
31
+ * Drop windows that have expired, at most once per window period.
32
+ *
33
+ * The sweep is O(size), so running it on every new key would be quadratic exactly when the map is
34
+ * busiest — a burst of distinct keys is both what fills the map and what triggers the sweep. The
35
+ * time guard makes the amortised cost one pass per window regardless of traffic, and expired
36
+ * entries can never outlive two windows.
37
+ */
38
+ prune(now) {
39
+ if (now - this.lastPrune < this.options.windowMs)
40
+ return;
41
+ this.lastPrune = now;
42
+ for (const [key, entry] of this.windows) {
43
+ if (now - entry.windowStart >= this.options.windowMs)
44
+ this.windows.delete(key);
45
+ }
46
+ }
47
+ }
48
+ //# sourceMappingURL=RateLimiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IAKH;IACA;IALF,OAAO,GAAG,IAAI,GAAG,EAAkD,CAAA;IAC5E,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAA;IAE5C,YACmB,KAAY,EACZ,OAA4C;QAD5C,UAAK,GAAL,KAAK,CAAO;QACZ,YAAO,GAAP,OAAO,CAAqC;IAC5D,CAAC;IAEJ,gGAAgG;IAChG,IAAI,CAAC,GAAW;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YACrD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACf,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;QAC5E,CAAC;QACD,KAAK,CAAC,KAAK,IAAI,CAAC,CAAA;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,GAAW;QACvB,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,OAAM;QACxD,IAAI,CAAC,SAAS,GAAG,GAAG,CAAA;QACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxC,IAAI,GAAG,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ import type { KernelDeps } from './deps';
2
+ export interface RetentionPolicy {
3
+ /** Age after which a finished, abandoned or never-started match is deleted. 0 keeps everything. */
4
+ maxAgeMs: number;
5
+ /** Matches deleted per sweep, so one pass cannot monopolise the database. */
6
+ batchSize: number;
7
+ }
8
+ export declare const DEFAULT_RETENTION: RetentionPolicy;
9
+ /**
10
+ * Deletes the matches nobody will come back to. A coordination server accumulates rows that have no
11
+ * second use — a finished match's order documents, a megabyte of snapshot per desync, the lobby
12
+ * someone opened and never started — and a self-hosted SQLite file has no operator watching it
13
+ * grow. Deleting the match row takes everything it owns with it, because every child table cascades.
14
+ *
15
+ * Running and desynced matches are never in scope at any age: a desync pause is not abandonment.
16
+ */
17
+ export declare class RetentionService {
18
+ private readonly deps;
19
+ private readonly policy;
20
+ constructor(deps: Pick<KernelDeps, 'storage' | 'clock' | 'logger'>, policy?: RetentionPolicy);
21
+ collect(): Promise<number>;
22
+ }
23
+ //# sourceMappingURL=RetentionService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RetentionService.d.ts","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAKxC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAA;IAChB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,iBAAiB,EAAE,eAG/B,CAAA;AAED;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtD,MAAM,GAAE,eAAmC;IAGxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CAgBjC"}