@phalanx-engine/server 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 (54) hide show
  1. package/README.md +507 -0
  2. package/dist/Phalanx.d.ts +85 -0
  3. package/dist/Phalanx.d.ts.map +1 -0
  4. package/dist/Phalanx.js +600 -0
  5. package/dist/Phalanx.js.map +1 -0
  6. package/dist/config/defaults.d.ts +10 -0
  7. package/dist/config/defaults.d.ts.map +1 -0
  8. package/dist/config/defaults.js +51 -0
  9. package/dist/config/defaults.js.map +1 -0
  10. package/dist/config/validation.d.ts +15 -0
  11. package/dist/config/validation.d.ts.map +1 -0
  12. package/dist/config/validation.js +74 -0
  13. package/dist/config/validation.js.map +1 -0
  14. package/dist/index.d.ts +12 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +14 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/services/GameRoom.d.ts +359 -0
  19. package/dist/services/GameRoom.d.ts.map +1 -0
  20. package/dist/services/GameRoom.js +1272 -0
  21. package/dist/services/GameRoom.js.map +1 -0
  22. package/dist/services/MatchmakingService.d.ts +102 -0
  23. package/dist/services/MatchmakingService.d.ts.map +1 -0
  24. package/dist/services/MatchmakingService.js +286 -0
  25. package/dist/services/MatchmakingService.js.map +1 -0
  26. package/dist/services/OAuthExchangeService.d.ts +49 -0
  27. package/dist/services/OAuthExchangeService.d.ts.map +1 -0
  28. package/dist/services/OAuthExchangeService.js +96 -0
  29. package/dist/services/OAuthExchangeService.js.map +1 -0
  30. package/dist/services/PrivateRoomService.d.ts +136 -0
  31. package/dist/services/PrivateRoomService.d.ts.map +1 -0
  32. package/dist/services/PrivateRoomService.js +395 -0
  33. package/dist/services/PrivateRoomService.js.map +1 -0
  34. package/dist/services/TokenValidator.d.ts +60 -0
  35. package/dist/services/TokenValidator.d.ts.map +1 -0
  36. package/dist/services/TokenValidator.js +213 -0
  37. package/dist/services/TokenValidator.js.map +1 -0
  38. package/dist/types/index.d.ts +390 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +6 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/utils/DeterministicRandom.d.ts +88 -0
  43. package/dist/utils/DeterministicRandom.d.ts.map +1 -0
  44. package/dist/utils/DeterministicRandom.js +140 -0
  45. package/dist/utils/DeterministicRandom.js.map +1 -0
  46. package/dist/utils/FixedMath.d.ts +22 -0
  47. package/dist/utils/FixedMath.d.ts.map +1 -0
  48. package/dist/utils/FixedMath.js +26 -0
  49. package/dist/utils/FixedMath.js.map +1 -0
  50. package/dist/utils/index.d.ts +7 -0
  51. package/dist/utils/index.d.ts.map +1 -0
  52. package/dist/utils/index.js +6 -0
  53. package/dist/utils/index.js.map +1 -0
  54. package/package.json +62 -0
@@ -0,0 +1,136 @@
1
+ import type { Server as SocketIOServer, Socket } from 'socket.io';
2
+ import type { PhalanxConfig, RoomCreatedEvent, RoomRecoveredEvent, RoomErrorEvent } from '../types/index.js';
3
+ import { GameRoom } from './GameRoom.js';
4
+ export type { RoomCreatedEvent, RoomRecoveredEvent, RoomErrorEvent };
5
+ export interface CreateDisconnectedHostRoomResult extends RoomCreatedEvent {
6
+ reused: boolean;
7
+ }
8
+ /**
9
+ * PrivateRoomService — manages private room creation and joining.
10
+ *
11
+ * When a second player joins a room, the service creates a GameRoom
12
+ * (bypassing the matchmaking queue) and starts the match. The GameRoom
13
+ * itself is responsible for deferring the countdown if any of the
14
+ * players is currently disconnected — the service merely hands it the
15
+ * teams and lets it decide.
16
+ */
17
+ export declare class PrivateRoomService {
18
+ private readonly rooms;
19
+ private readonly matches;
20
+ /**
21
+ * Reverse index: original room code → matchId. Lets a disconnected
22
+ * participant (host or guest) recover into the running/deferred
23
+ * match by presenting the room code they joined with, since neither
24
+ * side may yet know the `matchId` (the host never received
25
+ * `match-found` if the match is still deferred; the guest may also
26
+ * have disconnected before reading it). The code is also our
27
+ * authentication token for the recover — possession of the code is
28
+ * what proves the caller is a legitimate participant in this match.
29
+ *
30
+ * Cleared in `removeMatch` so a finished match can't leak into a
31
+ * future room reusing the same code (the code generator avoids
32
+ * collisions for active rooms but a freshly generated code that
33
+ * happens to match a long-finished one wouldn't).
34
+ */
35
+ private readonly matchByOriginalCode;
36
+ private readonly originalCodeByMatch;
37
+ private readonly io;
38
+ private readonly config;
39
+ private readonly eventEmitter;
40
+ private readonly resolveGameTypeConfig;
41
+ private readonly isPlayerQueued?;
42
+ /** TTL for rooms in ms (5 minutes). */
43
+ private static readonly ROOM_TTL_MS;
44
+ constructor(io: SocketIOServer, config: PhalanxConfig, eventEmitter: (event: string, ...args: unknown[]) => boolean | void, resolveGameTypeConfig: (gameType: string) => PhalanxConfig, isPlayerQueued?: (playerId: string) => boolean);
45
+ /**
46
+ * Create a private room for the given host player.
47
+ */
48
+ createRoom(playerId: string, username: string, socket: Socket, gameType?: string): void;
49
+ /**
50
+ * Create a private room for a host that is not connected over Socket.IO yet.
51
+ * Used by out-of-band integrations such as Telegram bot commands: the bot
52
+ * creates a room, shares the code, and the host later reclaims it via
53
+ * `room-recover` from the Mini App using the same deterministic playerId.
54
+ */
55
+ createRoomForDisconnectedHost(playerId: string, username: string, gameType?: string): CreateDisconnectedHostRoomResult;
56
+ /**
57
+ * Join an existing private room, creating a match.
58
+ *
59
+ * The match is always created and started immediately, but
60
+ * `GameRoom.start()` itself defers the countdown if any player
61
+ * (host OR guest) is currently disconnected — it'll emit
62
+ * `match-waiting-for-players` to whoever is online and only kick
63
+ * off the countdown once everyone reconnects. So the call sites
64
+ * here don't need to special-case an offline host any more.
65
+ */
66
+ joinRoom(playerId: string, username: string, socket: Socket, code: string): void;
67
+ /**
68
+ * Cancel a room that the player previously created.
69
+ *
70
+ * Looks up by `playerId` (not `socketId`) so a host who reconnected with
71
+ * a new socket after a disconnect can still cancel their own room.
72
+ * `room-cancelled` is emitted on the socket that initiated the cancel.
73
+ */
74
+ cancelRoom(playerId: string, socket: Socket): void;
75
+ /**
76
+ * Recover a room or match after a participant's socket disconnected.
77
+ *
78
+ * The caller must present the room `code` along with the `playerId` —
79
+ * possession of the code is what authenticates them in the
80
+ * anonymous-socket case. Without that check, any client that knew
81
+ * (or guessed) a participant's `playerId` could reclaim their room
82
+ * and learn its invite code (or worse, a match in progress).
83
+ *
84
+ * Three cases, tried in order:
85
+ *
86
+ * 1. The code maps to a still-waiting room and `playerId` is the
87
+ * host — re-bind the host's socket to the room (existing
88
+ * "waiting for guest" flow).
89
+ *
90
+ * 2. The code maps to a match (deferred or running) and `playerId`
91
+ * is one of its participants — rebind via
92
+ * `GameRoom.handleReconnect`. If the match was deferred and
93
+ * everyone is now connected, the GameRoom will start its
94
+ * countdown; otherwise it'll deliver a `reconnect-state`
95
+ * snapshot for an in-flight match. Either way the caller gets
96
+ * `room-recovered` so their UI can transition out of the
97
+ * "trying to recover" state.
98
+ *
99
+ * 3. None of the above — `room-error: "Room expired"`.
100
+ *
101
+ * Returns `true` on success, `false` on failure.
102
+ */
103
+ recoverRoom(playerId: string, socket: Socket, code: string): boolean;
104
+ /**
105
+ * Handle socket disconnect. For waiting private rooms we keep the room
106
+ * alive and only mark the host socket as temporarily unavailable, so
107
+ * mobile backgrounding does not destroy share-link rooms.
108
+ */
109
+ handleDisconnect(socketId: string): void;
110
+ /**
111
+ * Get a match created by this service (for command routing etc.)
112
+ */
113
+ getMatch(matchId: string): GameRoom | undefined;
114
+ /**
115
+ * Get all active matches from private rooms.
116
+ */
117
+ getActiveMatches(): GameRoom[];
118
+ /**
119
+ * Remove a finished match from the private matches map.
120
+ * Called from the match-ended listener — the match already stopped itself.
121
+ */
122
+ removeMatch(matchId: string): void;
123
+ /**
124
+ * Remove a room and clear its pending TTL timer.
125
+ */
126
+ private removeRoom;
127
+ /**
128
+ * Stop all active matches and clear rooms, including pending TTL timers.
129
+ */
130
+ stop(): void;
131
+ /**
132
+ * Generate a 6-character uppercase alphanumeric room code.
133
+ */
134
+ private generateCode;
135
+ }
136
+ //# sourceMappingURL=PrivateRoomService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrivateRoomService.d.ts","sourceRoot":"","sources":["../../src/services/PrivateRoomService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,KAAK,EACV,aAAa,EAEb,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAKzC,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,CAAC;AAuBrE,MAAM,WAAW,gCAAiC,SAAQ,gBAAgB;IACxE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAkC;IACtE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAkC;IACtE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAGT;IACpB,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAsC;IAC5E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAgC;IAEhE,uCAAuC;IACvC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAiB;gBAGlD,EAAE,EAAE,cAAc,EAClB,MAAM,EAAE,aAAa,EACrB,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,EACnE,qBAAqB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,aAAa,EAC1D,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO;IAShD;;OAEG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI;IAoEP;;;;;OAKG;IACH,6BAA6B,CAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,gCAAgC;IAmDnC;;;;;;;;;OASG;IACH,QAAQ,CACN,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,IAAI;IAyEP;;;;;;OAMG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWlD;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAqDpE;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAoBxC;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI/C;;OAEG;IACH,gBAAgB,IAAI,QAAQ,EAAE;IAI9B;;;OAGG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IASlC;;OAEG;IACH,OAAO,CAAC,UAAU;IAQlB;;OAEG;IACH,IAAI,IAAI,IAAI;IAaZ;;OAEG;IACH,OAAO,CAAC,YAAY;CAWrB"}
@@ -0,0 +1,395 @@
1
+ import { GameRoom } from './GameRoom.js';
2
+ /**
3
+ * PrivateRoomService — manages private room creation and joining.
4
+ *
5
+ * When a second player joins a room, the service creates a GameRoom
6
+ * (bypassing the matchmaking queue) and starts the match. The GameRoom
7
+ * itself is responsible for deferring the countdown if any of the
8
+ * players is currently disconnected — the service merely hands it the
9
+ * teams and lets it decide.
10
+ */
11
+ export class PrivateRoomService {
12
+ rooms = new Map();
13
+ matches = new Map();
14
+ /**
15
+ * Reverse index: original room code → matchId. Lets a disconnected
16
+ * participant (host or guest) recover into the running/deferred
17
+ * match by presenting the room code they joined with, since neither
18
+ * side may yet know the `matchId` (the host never received
19
+ * `match-found` if the match is still deferred; the guest may also
20
+ * have disconnected before reading it). The code is also our
21
+ * authentication token for the recover — possession of the code is
22
+ * what proves the caller is a legitimate participant in this match.
23
+ *
24
+ * Cleared in `removeMatch` so a finished match can't leak into a
25
+ * future room reusing the same code (the code generator avoids
26
+ * collisions for active rooms but a freshly generated code that
27
+ * happens to match a long-finished one wouldn't).
28
+ */
29
+ matchByOriginalCode = new Map();
30
+ originalCodeByMatch = new Map();
31
+ io;
32
+ config;
33
+ eventEmitter;
34
+ resolveGameTypeConfig;
35
+ isPlayerQueued;
36
+ /** TTL for rooms in ms (5 minutes). */
37
+ static ROOM_TTL_MS = 5 * 60 * 1000;
38
+ constructor(io, config, eventEmitter, resolveGameTypeConfig, isPlayerQueued) {
39
+ this.io = io;
40
+ this.config = config;
41
+ this.eventEmitter = eventEmitter;
42
+ this.resolveGameTypeConfig = resolveGameTypeConfig;
43
+ this.isPlayerQueued = isPlayerQueued;
44
+ }
45
+ /**
46
+ * Create a private room for the given host player.
47
+ */
48
+ createRoom(playerId, username, socket, gameType) {
49
+ // Reject if player is already in a match
50
+ if (socket.data.matchId) {
51
+ socket.emit('room-error', {
52
+ message: 'Already in a match',
53
+ });
54
+ return;
55
+ }
56
+ // Reject if player is currently queued in matchmaking
57
+ if (this.isPlayerQueued?.(playerId)) {
58
+ socket.emit('room-error', {
59
+ message: 'Already in matchmaking queue',
60
+ });
61
+ return;
62
+ }
63
+ // Prevent duplicate rooms per player
64
+ for (const room of this.rooms.values()) {
65
+ if (room.host.playerId === playerId) {
66
+ socket.emit('room-error', {
67
+ message: 'You already have an active room',
68
+ });
69
+ return;
70
+ }
71
+ }
72
+ const code = this.generateCode();
73
+ const resolvedGameType = gameType ?? 'default';
74
+ // Auto-expire room after TTL.
75
+ //
76
+ // Look the room back up inside the timer and emit on its *current*
77
+ // host socket rather than the one captured at creation time, because
78
+ // the host may have reconnected on a new socket via `room-recover`
79
+ // before the TTL elapsed. If the host is disconnected at the moment
80
+ // of expiry, there's simply no one to notify — that's fine.
81
+ const expirationTimer = setTimeout(() => {
82
+ const expired = this.rooms.get(code);
83
+ if (expired) {
84
+ this.removeRoom(code);
85
+ expired.hostSocket?.emit('room-expired', { code });
86
+ console.log(`[PrivateRoom] Room ${code} expired`);
87
+ }
88
+ }, PrivateRoomService.ROOM_TTL_MS);
89
+ const room = {
90
+ code,
91
+ host: {
92
+ playerId,
93
+ username,
94
+ socketId: socket.id,
95
+ joinedAt: Date.now(),
96
+ gameType: resolvedGameType,
97
+ },
98
+ hostSocket: socket,
99
+ hostSocketId: socket.id,
100
+ gameType: resolvedGameType,
101
+ createdAt: Date.now(),
102
+ expirationTimer,
103
+ };
104
+ this.rooms.set(code, room);
105
+ socket.emit('room-created', { code });
106
+ console.log(`[PrivateRoom] Room ${code} created by ${playerId}`);
107
+ }
108
+ /**
109
+ * Create a private room for a host that is not connected over Socket.IO yet.
110
+ * Used by out-of-band integrations such as Telegram bot commands: the bot
111
+ * creates a room, shares the code, and the host later reclaims it via
112
+ * `room-recover` from the Mini App using the same deterministic playerId.
113
+ */
114
+ createRoomForDisconnectedHost(playerId, username, gameType) {
115
+ if (this.isPlayerQueued?.(playerId)) {
116
+ throw new Error('Already in matchmaking queue');
117
+ }
118
+ for (const match of this.matches.values()) {
119
+ if (match.hasPlayer(playerId)) {
120
+ throw new Error('Already in a match');
121
+ }
122
+ }
123
+ for (const room of this.rooms.values()) {
124
+ if (room.host.playerId === playerId) {
125
+ return { code: room.code, reused: true };
126
+ }
127
+ }
128
+ const code = this.generateCode();
129
+ const resolvedGameType = gameType ?? 'default';
130
+ const expirationTimer = setTimeout(() => {
131
+ const expired = this.rooms.get(code);
132
+ if (expired) {
133
+ this.removeRoom(code);
134
+ expired.hostSocket?.emit('room-expired', { code });
135
+ console.log(`[PrivateRoom] Room ${code} expired`);
136
+ }
137
+ }, PrivateRoomService.ROOM_TTL_MS);
138
+ const room = {
139
+ code,
140
+ host: {
141
+ playerId,
142
+ username,
143
+ socketId: '',
144
+ joinedAt: Date.now(),
145
+ gameType: resolvedGameType,
146
+ },
147
+ hostSocket: null,
148
+ hostSocketId: '',
149
+ gameType: resolvedGameType,
150
+ createdAt: Date.now(),
151
+ expirationTimer,
152
+ };
153
+ this.rooms.set(code, room);
154
+ console.log(`[PrivateRoom] Room ${code} created by disconnected host ${playerId}`);
155
+ return { code, reused: false };
156
+ }
157
+ /**
158
+ * Join an existing private room, creating a match.
159
+ *
160
+ * The match is always created and started immediately, but
161
+ * `GameRoom.start()` itself defers the countdown if any player
162
+ * (host OR guest) is currently disconnected — it'll emit
163
+ * `match-waiting-for-players` to whoever is online and only kick
164
+ * off the countdown once everyone reconnects. So the call sites
165
+ * here don't need to special-case an offline host any more.
166
+ */
167
+ joinRoom(playerId, username, socket, code) {
168
+ // Reject if player is already in a match
169
+ if (socket.data.matchId) {
170
+ socket.emit('room-error', {
171
+ message: 'Already in a match',
172
+ });
173
+ return;
174
+ }
175
+ // Reject if player is currently queued in matchmaking
176
+ if (this.isPlayerQueued?.(playerId)) {
177
+ socket.emit('room-error', {
178
+ message: 'Already in matchmaking queue',
179
+ });
180
+ return;
181
+ }
182
+ const normalizedCode = code.toUpperCase();
183
+ const room = this.rooms.get(normalizedCode);
184
+ if (!room) {
185
+ socket.emit('room-error', {
186
+ message: 'Room not found',
187
+ });
188
+ return;
189
+ }
190
+ if (room.host.playerId === playerId) {
191
+ socket.emit('room-error', {
192
+ message: 'Cannot join your own room',
193
+ });
194
+ return;
195
+ }
196
+ // Remove room — it's now consumed
197
+ this.removeRoom(normalizedCode);
198
+ const guest = {
199
+ playerId,
200
+ username,
201
+ socketId: socket.id,
202
+ joinedAt: Date.now(),
203
+ gameType: room.gameType,
204
+ };
205
+ // Build teams: host = team 0, guest = team 1
206
+ const teams = [[room.host], [guest]];
207
+ const resolvedConfig = this.resolveGameTypeConfig(room.gameType);
208
+ const matchId = `match-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
209
+ const gameRoom = new GameRoom(matchId, this.io, resolvedConfig, teams, this.eventEmitter, room.gameType);
210
+ this.matches.set(matchId, gameRoom);
211
+ this.matchByOriginalCode.set(normalizedCode, matchId);
212
+ this.originalCodeByMatch.set(matchId, normalizedCode);
213
+ this.eventEmitter('match-created', gameRoom.getMatchInfo());
214
+ // GameRoom decides whether to start the countdown right away or
215
+ // emit `match-waiting-for-players` and wait for reconnects.
216
+ gameRoom.start();
217
+ console.log(`[PrivateRoom] Room ${code} → match ${matchId} (${room.host.playerId} vs ${playerId})`);
218
+ }
219
+ /**
220
+ * Cancel a room that the player previously created.
221
+ *
222
+ * Looks up by `playerId` (not `socketId`) so a host who reconnected with
223
+ * a new socket after a disconnect can still cancel their own room.
224
+ * `room-cancelled` is emitted on the socket that initiated the cancel.
225
+ */
226
+ cancelRoom(playerId, socket) {
227
+ for (const [code, room] of this.rooms) {
228
+ if (room.host.playerId === playerId) {
229
+ this.removeRoom(code);
230
+ socket.emit('room-cancelled', { code });
231
+ console.log(`[PrivateRoom] Room ${code} cancelled by ${playerId}`);
232
+ return;
233
+ }
234
+ }
235
+ }
236
+ /**
237
+ * Recover a room or match after a participant's socket disconnected.
238
+ *
239
+ * The caller must present the room `code` along with the `playerId` —
240
+ * possession of the code is what authenticates them in the
241
+ * anonymous-socket case. Without that check, any client that knew
242
+ * (or guessed) a participant's `playerId` could reclaim their room
243
+ * and learn its invite code (or worse, a match in progress).
244
+ *
245
+ * Three cases, tried in order:
246
+ *
247
+ * 1. The code maps to a still-waiting room and `playerId` is the
248
+ * host — re-bind the host's socket to the room (existing
249
+ * "waiting for guest" flow).
250
+ *
251
+ * 2. The code maps to a match (deferred or running) and `playerId`
252
+ * is one of its participants — rebind via
253
+ * `GameRoom.handleReconnect`. If the match was deferred and
254
+ * everyone is now connected, the GameRoom will start its
255
+ * countdown; otherwise it'll deliver a `reconnect-state`
256
+ * snapshot for an in-flight match. Either way the caller gets
257
+ * `room-recovered` so their UI can transition out of the
258
+ * "trying to recover" state.
259
+ *
260
+ * 3. None of the above — `room-error: "Room expired"`.
261
+ *
262
+ * Returns `true` on success, `false` on failure.
263
+ */
264
+ recoverRoom(playerId, socket, code) {
265
+ const normalizedCode = code.toUpperCase();
266
+ // Case 1: live room, caller is the host.
267
+ const room = this.rooms.get(normalizedCode);
268
+ if (room && room.host.playerId === playerId) {
269
+ room.hostSocket = socket;
270
+ room.hostSocketId = socket.id;
271
+ // Keep QueuedPlayer.socketId in sync so a guest joining now
272
+ // wires GameRoom up to the host's *current* socket.
273
+ room.host.socketId = socket.id;
274
+ socket.emit('room-recovered', {
275
+ code: room.code,
276
+ });
277
+ console.log(`[PrivateRoom] Room ${room.code} recovered by ${playerId}`);
278
+ return true;
279
+ }
280
+ // Case 2: code belongs to a match in progress / deferred and
281
+ // caller is a participant. We use the original room code as the
282
+ // authentication token — same posture as Case 1 and the previous
283
+ // `pendingHostReconnect` path: an attacker who guesses only a
284
+ // playerId cannot harvest the match without also guessing the code.
285
+ const matchId = this.matchByOriginalCode.get(normalizedCode);
286
+ if (matchId) {
287
+ const match = this.matches.get(matchId);
288
+ if (match && match.hasPlayer(playerId)) {
289
+ socket.emit('room-recovered', {
290
+ code: normalizedCode,
291
+ });
292
+ match.handleReconnect(playerId, socket.id);
293
+ console.log(`[PrivateRoom] Player ${playerId} recovered into match ${matchId} via code ${normalizedCode}`);
294
+ return true;
295
+ }
296
+ // Stale index entry — clean it up rather than letting it
297
+ // accumulate forever for a long-dead match.
298
+ if (!match) {
299
+ this.matchByOriginalCode.delete(normalizedCode);
300
+ this.originalCodeByMatch.delete(matchId);
301
+ }
302
+ }
303
+ // Case 3: nothing matched. Use the same generic error message
304
+ // for "no such code", "wrong playerId", and "match already gone"
305
+ // so an attacker can't distinguish them.
306
+ socket.emit('room-error', {
307
+ message: 'Room expired',
308
+ });
309
+ return false;
310
+ }
311
+ /**
312
+ * Handle socket disconnect. For waiting private rooms we keep the room
313
+ * alive and only mark the host socket as temporarily unavailable, so
314
+ * mobile backgrounding does not destroy share-link rooms.
315
+ */
316
+ handleDisconnect(socketId) {
317
+ for (const [code, room] of this.rooms) {
318
+ if (room.hostSocketId === socketId) {
319
+ room.hostSocket = null;
320
+ console.log(`[PrivateRoom] Host of room ${code} disconnected — room kept alive until TTL/cancel/join`);
321
+ }
322
+ }
323
+ // Forward disconnect to private-room matches (this also drives
324
+ // the deferral path: a guest disconnecting from a deferred match
325
+ // marks them as not-connected so a host reconnect doesn't spuriously
326
+ // start the countdown without the guest).
327
+ for (const match of this.matches.values()) {
328
+ match.handleDisconnect(socketId);
329
+ }
330
+ }
331
+ /**
332
+ * Get a match created by this service (for command routing etc.)
333
+ */
334
+ getMatch(matchId) {
335
+ return this.matches.get(matchId);
336
+ }
337
+ /**
338
+ * Get all active matches from private rooms.
339
+ */
340
+ getActiveMatches() {
341
+ return Array.from(this.matches.values());
342
+ }
343
+ /**
344
+ * Remove a finished match from the private matches map.
345
+ * Called from the match-ended listener — the match already stopped itself.
346
+ */
347
+ removeMatch(matchId) {
348
+ this.matches.delete(matchId);
349
+ const code = this.originalCodeByMatch.get(matchId);
350
+ if (code) {
351
+ this.matchByOriginalCode.delete(code);
352
+ this.originalCodeByMatch.delete(matchId);
353
+ }
354
+ }
355
+ /**
356
+ * Remove a room and clear its pending TTL timer.
357
+ */
358
+ removeRoom(code) {
359
+ const room = this.rooms.get(code);
360
+ if (room) {
361
+ clearTimeout(room.expirationTimer);
362
+ this.rooms.delete(code);
363
+ }
364
+ }
365
+ /**
366
+ * Stop all active matches and clear rooms, including pending TTL timers.
367
+ */
368
+ stop() {
369
+ for (const match of this.matches.values()) {
370
+ match.stop();
371
+ }
372
+ this.matches.clear();
373
+ this.matchByOriginalCode.clear();
374
+ this.originalCodeByMatch.clear();
375
+ for (const room of this.rooms.values()) {
376
+ clearTimeout(room.expirationTimer);
377
+ }
378
+ this.rooms.clear();
379
+ }
380
+ /**
381
+ * Generate a 6-character uppercase alphanumeric room code.
382
+ */
383
+ generateCode() {
384
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no I/O/0/1 to avoid confusion
385
+ let code;
386
+ do {
387
+ code = '';
388
+ for (let i = 0; i < 6; i++) {
389
+ code += chars[Math.floor(Math.random() * chars.length)];
390
+ }
391
+ } while (this.rooms.has(code));
392
+ return code;
393
+ }
394
+ }
395
+ //# sourceMappingURL=PrivateRoomService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrivateRoomService.js","sourceRoot":"","sources":["../../src/services/PrivateRoomService.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAgCzC;;;;;;;;GAQG;AACH,MAAM,OAAO,kBAAkB;IACZ,KAAK,GAA6B,IAAI,GAAG,EAAE,CAAC;IAC5C,OAAO,GAA0B,IAAI,GAAG,EAAE,CAAC;IAC5D;;;;;;;;;;;;;;OAcG;IACc,mBAAmB,GAAwB,IAAI,GAAG,EAAE,CAAC;IACrD,mBAAmB,GAAwB,IAAI,GAAG,EAAE,CAAC;IACrD,EAAE,CAAiB;IACnB,MAAM,CAAgB;IACtB,YAAY,CAGT;IACH,qBAAqB,CAAsC;IAC3D,cAAc,CAAiC;IAEhE,uCAAuC;IAC/B,MAAM,CAAU,WAAW,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IAEpD,YACE,EAAkB,EAClB,MAAqB,EACrB,YAAmE,EACnE,qBAA0D,EAC1D,cAA8C;QAE9C,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,qBAAqB,GAAG,qBAAqB,CAAC;QACnD,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;IAED;;OAEG;IACH,UAAU,CACR,QAAgB,EAChB,QAAgB,EAChB,MAAc,EACd,QAAiB;QAEjB,yCAAyC;QACzC,IAAK,MAAM,CAAC,IAA6B,CAAC,OAAO,EAAE,CAAC;YAClD,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,oBAAoB;aACL,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,sDAAsD;QACtD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,8BAA8B;aACf,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,qCAAqC;QACrC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;oBACxB,OAAO,EAAE,iCAAiC;iBAClB,CAAC,CAAC;gBAC5B,OAAO;YACT,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,MAAM,gBAAgB,GAAG,QAAQ,IAAI,SAAS,CAAC;QAE/C,8BAA8B;QAC9B,EAAE;QACF,mEAAmE;QACnE,qEAAqE;QACrE,mEAAmE;QACnE,oEAAoE;QACpE,4DAA4D;QAC5D,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,EAAE;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACtB,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnD,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,UAAU,CAAC,CAAC;YACpD,CAAC;QACH,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAEnC,MAAM,IAAI,GAAgB;YACxB,IAAI;YACJ,IAAI,EAAE;gBACJ,QAAQ;gBACR,QAAQ;gBACR,QAAQ,EAAE,MAAM,CAAC,EAAE;gBACnB,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;gBACpB,QAAQ,EAAE,gBAAgB;aAC3B;YACD,UAAU,EAAE,MAAM;YAClB,YAAY,EAAE,MAAM,CAAC,EAAE;YACvB,QAAQ,EAAE,gBAAgB;YAC1B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,eAAe;SAChB,CAAC;QAEF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAE3B,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,EAA6B,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,eAAe,QAAQ,EAAE,CAAC,CAAC;IACnE,CAAC;IAED;;;;;OAKG;IACH,6BAA6B,CAC3B,QAAgB,EAChB,QAAgB,EAChB,QAAiB;QAEjB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,IAAI,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACpC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,MAAM,gBAAgB,GAAG,QAAQ,IAAI,SAAS,CAAC;QAE/C,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,EAAE;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,OAAO,EAAE,CAAC;gBACZ,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACtB,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnD,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,UAAU,CAAC,CAAC;YACpD,CAAC;QACH,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAEnC,MAAM,IAAI,GAAgB;YACxB,IAAI;YACJ,IAAI,EAAE;gBACJ,QAAQ;gBACR,QAAQ;gBACR,QAAQ,EAAE,EAAE;gBACZ,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;gBACpB,QAAQ,EAAE,gBAAgB;aAC3B;YACD,UAAU,EAAE,IAAI;YAChB,YAAY,EAAE,EAAE;YAChB,QAAQ,EAAE,gBAAgB;YAC1B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,eAAe;SAChB,CAAC;QAEF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAE3B,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,iCAAiC,QAAQ,EAAE,CAAC,CAAC;QACnF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACjC,CAAC;IAED;;;;;;;;;OASG;IACH,QAAQ,CACN,QAAgB,EAChB,QAAgB,EAChB,MAAc,EACd,IAAY;QAEZ,yCAAyC;QACzC,IAAK,MAAM,CAAC,IAA6B,CAAC,OAAO,EAAE,CAAC;YAClD,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,oBAAoB;aACL,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,sDAAsD;QACtD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,8BAA8B;aACf,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAE5C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,gBAAgB;aACD,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;gBACxB,OAAO,EAAE,2BAA2B;aACZ,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,kCAAkC;QAClC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;QAEhC,MAAM,KAAK,GAAiB;YAC1B,QAAQ;YACR,QAAQ;YACR,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;YACpB,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;QAEF,6CAA6C;QAC7C,MAAM,KAAK,GAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAEvD,MAAM,cAAc,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,SAAS,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAErF,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAC3B,OAAO,EACP,IAAI,CAAC,EAAE,EACP,cAAc,EACd,KAAK,EACL,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,QAAQ,CACd,CAAC;QAEF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;QACtD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;QACtD,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC;QAC5D,gEAAgE;QAChE,4DAA4D;QAC5D,QAAQ,CAAC,KAAK,EAAE,CAAC;QAEjB,OAAO,CAAC,GAAG,CACT,sBAAsB,IAAI,YAAY,OAAO,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,OAAO,QAAQ,GAAG,CACvF,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,UAAU,CAAC,QAAgB,EAAE,MAAc;QACzC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBACtB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,iBAAiB,QAAQ,EAAE,CAAC,CAAC;gBACnE,OAAO;YACT,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,WAAW,CAAC,QAAgB,EAAE,MAAc,EAAE,IAAY;QACxD,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAE1C,yCAAyC;QACzC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC5C,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC5C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC;YACzB,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC;YAC9B,4DAA4D;YAC5D,oDAAoD;YACpD,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE;gBAC5B,IAAI,EAAE,IAAI,CAAC,IAAI;aACa,CAAC,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,CAAC,IAAI,iBAAiB,QAAQ,EAAE,CAAC,CAAC;YACxE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,6DAA6D;QAC7D,gEAAgE;QAChE,iEAAiE;QACjE,8DAA8D;QAC9D,oEAAoE;QACpE,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC7D,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,KAAK,IAAI,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACvC,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE;oBAC5B,IAAI,EAAE,cAAc;iBACQ,CAAC,CAAC;gBAChC,KAAK,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;gBAC3C,OAAO,CAAC,GAAG,CACT,wBAAwB,QAAQ,yBAAyB,OAAO,aAAa,cAAc,EAAE,CAC9F,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YACD,yDAAyD;YACzD,4CAA4C;YAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;gBAChD,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;QAED,8DAA8D;QAC9D,iEAAiE;QACjE,yCAAyC;QACzC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE;YACxB,OAAO,EAAE,cAAc;SACC,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;OAIG;IACH,gBAAgB,CAAC,QAAgB;QAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;gBACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;gBACvB,OAAO,CAAC,GAAG,CACT,8BAA8B,IAAI,uDAAuD,CAC1F,CAAC;YACJ,CAAC;QACH,CAAC;QAED,+DAA+D;QAC/D,iEAAiE;QACjE,qEAAqE;QACrE,0CAA0C;QAC1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAGD;;OAEG;IACH,QAAQ,CAAC,OAAe;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED;;OAEG;IACH,gBAAgB;QACd,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,OAAe;QACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAY;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,IAAI,EAAE,CAAC;YACT,YAAY,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED;;OAEG;IACH,IAAI;QACF,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACvC,YAAY,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED;;OAEG;IACK,YAAY;QAClB,MAAM,KAAK,GAAG,kCAAkC,CAAC,CAAC,gCAAgC;QAClF,IAAI,IAAY,CAAC;QACjB,GAAG,CAAC;YACF,IAAI,GAAG,EAAE,CAAC;YACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC3B,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Phalanx Server Authentication
3
+ *
4
+ * Token validation for securing WebSocket connections.
5
+ */
6
+ import type { AuthConfig, TokenValidator, TokenValidationResult } from '../types/index.js';
7
+ export type { TokenValidator, TokenValidationResult };
8
+ /**
9
+ * Token Validator Service
10
+ *
11
+ * Validates OAuth tokens for server-side authentication.
12
+ */
13
+ export declare class TokenValidatorService {
14
+ private config;
15
+ private tokenCache;
16
+ private googleKeysCache;
17
+ constructor(config: AuthConfig);
18
+ /**
19
+ * Validate a token
20
+ */
21
+ validate(token: string): Promise<TokenValidationResult>;
22
+ /**
23
+ * Validate a Google ID token
24
+ */
25
+ private validateGoogleToken;
26
+ /**
27
+ * Verify token signature using Google's JWKS (for production use)
28
+ * TODO: Implement full signature verification
29
+ */
30
+ private verifyGoogleSignature;
31
+ /**
32
+ * Base64URL decode
33
+ */
34
+ private base64UrlDecode;
35
+ /**
36
+ * Clear the token cache
37
+ */
38
+ clearCache(): void;
39
+ /**
40
+ * Get cache statistics
41
+ */
42
+ getCacheStats(): {
43
+ size: number;
44
+ hits: number;
45
+ };
46
+ }
47
+ /**
48
+ * Create a simple custom validator that accepts any non-empty token.
49
+ * FOR DEVELOPMENT/TESTING ONLY - DO NOT USE IN PRODUCTION!
50
+ */
51
+ export declare function createDevValidator(): TokenValidator;
52
+ /**
53
+ * Create a validator that validates tokens via an external endpoint.
54
+ * Useful when you have a backend auth service.
55
+ */
56
+ export declare function createEndpointValidator(endpointUrl: string, options?: {
57
+ headers?: Record<string, string>;
58
+ timeout?: number;
59
+ }): TokenValidator;
60
+ //# sourceMappingURL=TokenValidator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TokenValidator.d.ts","sourceRoot":"","sources":["../../src/services/TokenValidator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,qBAAqB,EACtB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,CAAC;AAyCtD;;;;GAIG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,UAAU,CAAuC;IACzD,OAAO,CAAC,eAAe,CAChB;gBAEK,MAAM,EAAE,UAAU;IAQ9B;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA2C7D;;OAEG;YACW,mBAAmB;IAiEjC;;;OAGG;YACW,qBAAqB;IAYnC;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;OAEG;IACH,UAAU,IAAI,IAAI;IAIlB;;OAEG;IACH,aAAa,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAMhD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAwBnD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IACR,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GACA,cAAc,CAmChB"}