@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.
- package/README.md +507 -0
- package/dist/Phalanx.d.ts +85 -0
- package/dist/Phalanx.d.ts.map +1 -0
- package/dist/Phalanx.js +600 -0
- package/dist/Phalanx.js.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +51 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/validation.d.ts +15 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +74 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/services/GameRoom.d.ts +359 -0
- package/dist/services/GameRoom.d.ts.map +1 -0
- package/dist/services/GameRoom.js +1272 -0
- package/dist/services/GameRoom.js.map +1 -0
- package/dist/services/MatchmakingService.d.ts +102 -0
- package/dist/services/MatchmakingService.d.ts.map +1 -0
- package/dist/services/MatchmakingService.js +286 -0
- package/dist/services/MatchmakingService.js.map +1 -0
- package/dist/services/OAuthExchangeService.d.ts +49 -0
- package/dist/services/OAuthExchangeService.d.ts.map +1 -0
- package/dist/services/OAuthExchangeService.js +96 -0
- package/dist/services/OAuthExchangeService.js.map +1 -0
- package/dist/services/PrivateRoomService.d.ts +136 -0
- package/dist/services/PrivateRoomService.d.ts.map +1 -0
- package/dist/services/PrivateRoomService.js +395 -0
- package/dist/services/PrivateRoomService.js.map +1 -0
- package/dist/services/TokenValidator.d.ts +60 -0
- package/dist/services/TokenValidator.d.ts.map +1 -0
- package/dist/services/TokenValidator.js +213 -0
- package/dist/services/TokenValidator.js.map +1 -0
- package/dist/types/index.d.ts +390 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/DeterministicRandom.d.ts +88 -0
- package/dist/utils/DeterministicRandom.d.ts.map +1 -0
- package/dist/utils/DeterministicRandom.js +140 -0
- package/dist/utils/DeterministicRandom.js.map +1 -0
- package/dist/utils/FixedMath.d.ts +22 -0
- package/dist/utils/FixedMath.d.ts.map +1 -0
- package/dist/utils/FixedMath.js +26 -0
- package/dist/utils/FixedMath.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- 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":"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"}
|
|
@@ -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"}
|