@air-jam/server 0.1.2 → 0.1.3
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.
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
controllerStateSchema,
|
|
7
7
|
controllerSystemSchema,
|
|
8
8
|
ErrorCode,
|
|
9
|
+
hostCreateRoomSchema,
|
|
9
10
|
hostJoinAsChildSchema,
|
|
11
|
+
hostReconnectSchema,
|
|
10
12
|
hostRegisterSystemSchema,
|
|
11
13
|
hostRegistrationSchema,
|
|
12
14
|
systemLaunchGameSchema
|
|
@@ -198,7 +200,21 @@ var RoomManager = class {
|
|
|
198
200
|
};
|
|
199
201
|
var roomManager = new RoomManager();
|
|
200
202
|
|
|
203
|
+
// src/utils/ids.ts
|
|
204
|
+
import { roomCodeSchema } from "@air-jam/sdk/protocol";
|
|
205
|
+
import { randomInt } from "crypto";
|
|
206
|
+
var alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
207
|
+
var generateRoomCode = () => {
|
|
208
|
+
const code = Array.from(
|
|
209
|
+
{ length: 4 },
|
|
210
|
+
() => alphabet[randomInt(0, alphabet.length)]
|
|
211
|
+
).join("");
|
|
212
|
+
return roomCodeSchema.parse(code);
|
|
213
|
+
};
|
|
214
|
+
|
|
201
215
|
// src/index.ts
|
|
216
|
+
var lastServerInputLogTime = 0;
|
|
217
|
+
var lastServerInputFailLogTime = 0;
|
|
202
218
|
var PORT = Number(process.env.PORT ?? 4e3);
|
|
203
219
|
var app = express();
|
|
204
220
|
app.use(cors());
|
|
@@ -268,6 +284,127 @@ io.on(
|
|
|
268
284
|
io.to(roomId).emit("server:roomReady", { roomId });
|
|
269
285
|
}
|
|
270
286
|
);
|
|
287
|
+
socket.on(
|
|
288
|
+
"host:createRoom",
|
|
289
|
+
async (payload, callback) => {
|
|
290
|
+
const parsed = hostCreateRoomSchema.safeParse(payload);
|
|
291
|
+
if (!parsed.success) {
|
|
292
|
+
callback({
|
|
293
|
+
ok: false,
|
|
294
|
+
message: parsed.error.message,
|
|
295
|
+
code: ErrorCode.INVALID_PAYLOAD
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const { maxPlayers, apiKey } = parsed.data;
|
|
300
|
+
if (apiKey) {
|
|
301
|
+
const verification = await authService.verifyApiKey(apiKey);
|
|
302
|
+
if (!verification.isVerified) {
|
|
303
|
+
callback({
|
|
304
|
+
ok: false,
|
|
305
|
+
message: verification.error,
|
|
306
|
+
code: ErrorCode.INVALID_API_KEY
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const existingRoomId = roomManager.getRoomByHostId(socket.id);
|
|
312
|
+
if (existingRoomId) {
|
|
313
|
+
const existingSession = roomManager.getRoom(existingRoomId);
|
|
314
|
+
if (existingSession && existingSession.masterHostSocketId === socket.id) {
|
|
315
|
+
console.log(
|
|
316
|
+
`[server] Host ${socket.id} already has room ${existingRoomId}, returning existing.`
|
|
317
|
+
);
|
|
318
|
+
callback({ ok: true, roomId: existingRoomId });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
let roomId;
|
|
323
|
+
let attempts = 0;
|
|
324
|
+
do {
|
|
325
|
+
roomId = generateRoomCode();
|
|
326
|
+
attempts++;
|
|
327
|
+
if (attempts > 10) {
|
|
328
|
+
callback({
|
|
329
|
+
ok: false,
|
|
330
|
+
message: "Failed to generate unique room ID",
|
|
331
|
+
code: ErrorCode.CONNECTION_FAILED
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
} while (roomManager.getRoom(roomId));
|
|
336
|
+
const session = {
|
|
337
|
+
roomId,
|
|
338
|
+
masterHostSocketId: socket.id,
|
|
339
|
+
focus: "SYSTEM",
|
|
340
|
+
controllers: /* @__PURE__ */ new Map(),
|
|
341
|
+
maxPlayers: maxPlayers ?? 8,
|
|
342
|
+
gameState: "paused"
|
|
343
|
+
};
|
|
344
|
+
roomManager.setRoom(roomId, session);
|
|
345
|
+
roomManager.setHostRoom(socket.id, roomId);
|
|
346
|
+
socket.join(roomId);
|
|
347
|
+
console.log(`[server] Created room ${roomId} for host ${socket.id}`);
|
|
348
|
+
callback({ ok: true, roomId });
|
|
349
|
+
io.to(roomId).emit("server:roomReady", { roomId });
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
socket.on(
|
|
353
|
+
"host:reconnect",
|
|
354
|
+
async (payload, callback) => {
|
|
355
|
+
const parsed = hostReconnectSchema.safeParse(payload);
|
|
356
|
+
if (!parsed.success) {
|
|
357
|
+
callback({
|
|
358
|
+
ok: false,
|
|
359
|
+
message: parsed.error.message,
|
|
360
|
+
code: ErrorCode.INVALID_PAYLOAD
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const { roomId, apiKey } = parsed.data;
|
|
365
|
+
if (apiKey) {
|
|
366
|
+
const verification = await authService.verifyApiKey(apiKey);
|
|
367
|
+
if (!verification.isVerified) {
|
|
368
|
+
callback({
|
|
369
|
+
ok: false,
|
|
370
|
+
message: verification.error,
|
|
371
|
+
code: ErrorCode.INVALID_API_KEY
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const session = roomManager.getRoom(roomId);
|
|
377
|
+
if (!session) {
|
|
378
|
+
callback({
|
|
379
|
+
ok: false,
|
|
380
|
+
message: "Room not found",
|
|
381
|
+
code: ErrorCode.ROOM_NOT_FOUND
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const previousMasterSocket = io.sockets.sockets.get(
|
|
386
|
+
session.masterHostSocketId
|
|
387
|
+
);
|
|
388
|
+
const isPreviousHostConnected = previousMasterSocket?.connected ?? false;
|
|
389
|
+
if (!isPreviousHostConnected || session.masterHostSocketId === socket.id) {
|
|
390
|
+
session.masterHostSocketId = socket.id;
|
|
391
|
+
roomManager.setRoom(roomId, session);
|
|
392
|
+
roomManager.setHostRoom(socket.id, roomId);
|
|
393
|
+
socket.join(roomId);
|
|
394
|
+
console.log(
|
|
395
|
+
`[server] Host ${socket.id} reconnected to room ${roomId}`
|
|
396
|
+
);
|
|
397
|
+
callback({ ok: true, roomId });
|
|
398
|
+
io.to(roomId).emit("server:roomReady", { roomId });
|
|
399
|
+
} else {
|
|
400
|
+
callback({
|
|
401
|
+
ok: false,
|
|
402
|
+
message: "Room already has an active host",
|
|
403
|
+
code: ErrorCode.ALREADY_CONNECTED
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
);
|
|
271
408
|
socket.on(
|
|
272
409
|
"system:launchGame",
|
|
273
410
|
(payload, callback) => {
|
|
@@ -397,6 +534,21 @@ io.on(
|
|
|
397
534
|
session.joinToken = void 0;
|
|
398
535
|
session.activeControllerUrl = void 0;
|
|
399
536
|
io.to(roomId).emit("client:unloadUi");
|
|
537
|
+
if (session.masterHostSocketId) {
|
|
538
|
+
const masterSocket = io.sockets.sockets.get(session.masterHostSocketId);
|
|
539
|
+
if (masterSocket) {
|
|
540
|
+
setTimeout(() => {
|
|
541
|
+
session.controllers.forEach((c) => {
|
|
542
|
+
const notice = {
|
|
543
|
+
controllerId: c.controllerId,
|
|
544
|
+
nickname: c.nickname,
|
|
545
|
+
player: c.playerProfile
|
|
546
|
+
};
|
|
547
|
+
masterSocket.emit("server:controllerJoined", notice);
|
|
548
|
+
});
|
|
549
|
+
}, 100);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
400
552
|
});
|
|
401
553
|
socket.on(
|
|
402
554
|
"host:register",
|
|
@@ -566,18 +718,37 @@ io.on(
|
|
|
566
718
|
socket.leave(roomId);
|
|
567
719
|
});
|
|
568
720
|
socket.on("controller:input", (payload) => {
|
|
721
|
+
const now = Date.now();
|
|
722
|
+
const input = payload?.input;
|
|
723
|
+
const hasActiveInput = input && (input.action === true || typeof input.vector === "object" && input.vector !== null && (Math.abs(input.vector.x ?? 0) > 0.01 || Math.abs(input.vector.y ?? 0) > 0.01));
|
|
724
|
+
if (hasActiveInput && (!lastServerInputLogTime || now - lastServerInputLogTime > 1e3)) {
|
|
725
|
+
lastServerInputLogTime = now;
|
|
726
|
+
}
|
|
569
727
|
const result = controllerInputSchema.safeParse(payload);
|
|
570
728
|
if (!result.success) {
|
|
729
|
+
if (!lastServerInputFailLogTime || now - lastServerInputFailLogTime > 1e3) {
|
|
730
|
+
lastServerInputFailLogTime = now;
|
|
731
|
+
}
|
|
571
732
|
return;
|
|
572
733
|
}
|
|
573
734
|
const { roomId } = result.data;
|
|
574
735
|
const session = roomManager.getRoom(roomId);
|
|
575
736
|
if (!session) {
|
|
737
|
+
if (!lastServerInputFailLogTime || now - lastServerInputFailLogTime > 1e3) {
|
|
738
|
+
lastServerInputFailLogTime = now;
|
|
739
|
+
}
|
|
576
740
|
return;
|
|
577
741
|
}
|
|
578
742
|
const targetHostId = roomManager.getActiveHostId(session);
|
|
579
743
|
if (targetHostId) {
|
|
744
|
+
if (!lastServerInputLogTime || now - lastServerInputLogTime > 1e3) {
|
|
745
|
+
lastServerInputLogTime = now;
|
|
746
|
+
}
|
|
580
747
|
io.to(targetHostId).emit("server:input", result.data);
|
|
748
|
+
} else {
|
|
749
|
+
if (!lastServerInputFailLogTime || now - lastServerInputFailLogTime > 1e3) {
|
|
750
|
+
lastServerInputFailLogTime = now;
|
|
751
|
+
}
|
|
581
752
|
}
|
|
582
753
|
});
|
|
583
754
|
socket.on("controller:system", (payload) => {
|
|
@@ -723,9 +894,6 @@ io.on(
|
|
|
723
894
|
return;
|
|
724
895
|
}
|
|
725
896
|
if (controllerSession.socketId !== socket.id) {
|
|
726
|
-
roomManager.deleteController(controllerSession.socketId);
|
|
727
|
-
controllerSession.socketId = socket.id;
|
|
728
|
-
roomManager.setController(socket.id, { roomId, controllerId });
|
|
729
897
|
socket.join(roomId);
|
|
730
898
|
}
|
|
731
899
|
const hostId = roomManager.getActiveHostId(session);
|
|
@@ -754,6 +922,23 @@ io.on(
|
|
|
754
922
|
session.joinToken = void 0;
|
|
755
923
|
session.activeControllerUrl = void 0;
|
|
756
924
|
io.to(roomId).emit("client:unloadUi");
|
|
925
|
+
if (session.masterHostSocketId) {
|
|
926
|
+
const masterSocket = io.sockets.sockets.get(
|
|
927
|
+
session.masterHostSocketId
|
|
928
|
+
);
|
|
929
|
+
if (masterSocket) {
|
|
930
|
+
setTimeout(() => {
|
|
931
|
+
session.controllers.forEach((c) => {
|
|
932
|
+
const notice = {
|
|
933
|
+
controllerId: c.controllerId,
|
|
934
|
+
nickname: c.nickname,
|
|
935
|
+
player: c.playerProfile
|
|
936
|
+
};
|
|
937
|
+
masterSocket.emit("server:controllerJoined", notice);
|
|
938
|
+
});
|
|
939
|
+
}, 100);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
757
942
|
} else if (socket.id === session.masterHostSocketId) {
|
|
758
943
|
console.log(`[server] Host disconnected from room ${roomId}`);
|
|
759
944
|
setTimeout(() => {
|
|
@@ -788,4 +973,4 @@ io.on(
|
|
|
788
973
|
httpServer.listen(PORT, () => {
|
|
789
974
|
console.log(`[air-jam] server listening on http://localhost:${PORT}`);
|
|
790
975
|
});
|
|
791
|
-
//# sourceMappingURL=chunk-
|
|
976
|
+
//# sourceMappingURL=chunk-T6BT7WE5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/services/auth-service.ts","../src/db.ts","../src/services/room-manager.ts","../src/utils/ids.ts"],"sourcesContent":["import {\n controllerInputSchema,\n controllerJoinSchema,\n controllerLeaveSchema,\n controllerStateSchema,\n controllerSystemSchema,\n ErrorCode,\n hostCreateRoomSchema,\n hostJoinAsChildSchema,\n hostReconnectSchema,\n hostRegisterSystemSchema,\n hostRegistrationSchema,\n PlaySoundEventPayload,\n SignalPayload,\n systemLaunchGameSchema,\n type AirJamActionRpcPayload,\n type AirJamStateSyncPayload,\n type ClientToServerEvents,\n type ControllerActionRpcPayload,\n type ControllerInputEvent,\n type ControllerJoinedNotice,\n type ControllerJoinPayload,\n type ControllerLeavePayload,\n type ControllerLeftNotice,\n type ControllerStateMessage,\n type HostCreateRoomPayload,\n type HostJoinAsChildPayload,\n type HostReconnectPayload,\n type HostRegisterSystemPayload,\n type HostRegistrationPayload,\n type HostStateSyncPayload,\n type InterServerEvents,\n type PlayerProfile,\n type ServerErrorPayload,\n type ServerToClientEvents,\n type SocketData,\n type SystemLaunchGamePayload,\n} from \"@air-jam/sdk/protocol\";\nimport Color from \"color\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport { createServer } from \"node:http\";\nimport { Server, type Socket } from \"socket.io\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { authService } from \"./services/auth-service.js\";\nimport { roomManager } from \"./services/room-manager.js\";\nimport type { ControllerSession, RoomSession } from \"./types.js\";\nimport { generateRoomCode } from \"./utils/ids.js\";\n\n// Throttling variables for logging\nlet lastServerInputLogTime = 0;\nlet lastServerInputFailLogTime = 0;\n\nconst PORT = Number(process.env.PORT ?? 4000);\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\napp.get(\"/health\", (_, res) => {\n res.json({ ok: true });\n});\n\nconst httpServer = createServer(app);\n\nconst io = new Server<\n ClientToServerEvents,\n ServerToClientEvents,\n InterServerEvents,\n SocketData\n>(httpServer, {\n cors: {\n origin: \"*\",\n },\n pingInterval: 2000,\n pingTimeout: 5000,\n});\n\nconst emitError = (socketId: string, payload: ServerErrorPayload): void => {\n io.to(socketId).emit(\"server:error\", payload);\n};\n\nio.on(\n \"connection\",\n (\n socket: Socket<\n ClientToServerEvents,\n ServerToClientEvents,\n InterServerEvents,\n SocketData\n >,\n ) => {\n // --- SYSTEM HOST REGISTRATION (Arcade) ---\n socket.on(\n \"host:registerSystem\",\n async (payload: HostRegisterSystemPayload, callback) => {\n const parsed = hostRegisterSystemSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, apiKey } = parsed.data;\n\n // API Key Validation using authService\n const verification = await authService.verifyApiKey(apiKey);\n if (!verification.isVerified) {\n console.warn(\n `[server] Unauthorized host registration attempt for room ${roomId}`,\n );\n callback({\n ok: false,\n message: verification.error,\n code: ErrorCode.INVALID_API_KEY,\n });\n return;\n }\n\n let session = roomManager.getRoom(roomId);\n\n if (session) {\n // Reconnect logic for System Host\n session.masterHostSocketId = socket.id;\n roomManager.setRoom(roomId, session);\n } else {\n // Create new room\n console.log(`[server] Creating room ${roomId}`);\n session = {\n roomId,\n masterHostSocketId: socket.id,\n focus: \"SYSTEM\",\n controllers: new Map(),\n maxPlayers: 32, // Default increased to 32 to allow for observers/queue\n gameState: \"paused\",\n };\n roomManager.setRoom(roomId, session);\n }\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n },\n );\n\n // --- CREATE ROOM (Server-Issued Room ID) ---\n socket.on(\n \"host:createRoom\",\n async (\n payload: HostCreateRoomPayload,\n callback: (ack: {\n ok: boolean;\n roomId?: string;\n message?: string;\n code?: ErrorCode | string;\n }) => void,\n ) => {\n const parsed = hostCreateRoomSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { maxPlayers, apiKey } = parsed.data;\n\n // API Key Validation (if provided)\n if (apiKey) {\n const verification = await authService.verifyApiKey(apiKey);\n if (!verification.isVerified) {\n callback({\n ok: false,\n message: verification.error,\n code: ErrorCode.INVALID_API_KEY,\n });\n return;\n }\n }\n\n // IDEMPOTENCY: Check if this socket already has a room\n const existingRoomId = roomManager.getRoomByHostId(socket.id);\n if (existingRoomId) {\n const existingSession = roomManager.getRoom(existingRoomId);\n // Verify the socket is still connected and is the master host\n if (\n existingSession &&\n existingSession.masterHostSocketId === socket.id\n ) {\n console.log(\n `[server] Host ${socket.id} already has room ${existingRoomId}, returning existing.`,\n );\n callback({ ok: true, roomId: existingRoomId });\n return;\n }\n }\n\n // Generate unique room ID\n let roomId: string;\n let attempts = 0;\n do {\n roomId = generateRoomCode();\n attempts++;\n if (attempts > 10) {\n callback({\n ok: false,\n message: \"Failed to generate unique room ID\",\n code: ErrorCode.CONNECTION_FAILED,\n });\n return;\n }\n } while (roomManager.getRoom(roomId));\n\n // Create new room session\n const session: RoomSession = {\n roomId,\n masterHostSocketId: socket.id,\n focus: \"SYSTEM\",\n controllers: new Map(),\n maxPlayers: maxPlayers ?? 8,\n gameState: \"paused\",\n };\n\n roomManager.setRoom(roomId, session);\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n console.log(`[server] Created room ${roomId} for host ${socket.id}`);\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n },\n );\n\n // --- RECONNECT TO ROOM ---\n socket.on(\n \"host:reconnect\",\n async (\n payload: HostReconnectPayload,\n callback: (ack: {\n ok: boolean;\n roomId?: string;\n message?: string;\n code?: ErrorCode | string;\n }) => void,\n ) => {\n const parsed = hostReconnectSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, apiKey } = parsed.data;\n\n // API Key Validation (if provided)\n if (apiKey) {\n const verification = await authService.verifyApiKey(apiKey);\n if (!verification.isVerified) {\n callback({\n ok: false,\n message: verification.error,\n code: ErrorCode.INVALID_API_KEY,\n });\n return;\n }\n }\n\n const session = roomManager.getRoom(roomId);\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n return;\n }\n\n // Check if the previous master host socket is still connected\n const previousMasterSocket = io.sockets.sockets.get(\n session.masterHostSocketId,\n );\n const isPreviousHostConnected =\n previousMasterSocket?.connected ?? false;\n\n // Allow reconnect if:\n // 1. Previous host is not connected (disconnected/reload)\n // 2. OR this socket is already the master host (reconnection from same client)\n if (\n !isPreviousHostConnected ||\n session.masterHostSocketId === socket.id\n ) {\n session.masterHostSocketId = socket.id;\n roomManager.setRoom(roomId, session);\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n console.log(\n `[server] Host ${socket.id} reconnected to room ${roomId}`,\n );\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n } else {\n callback({\n ok: false,\n message: \"Room already has an active host\",\n code: ErrorCode.ALREADY_CONNECTED,\n });\n }\n },\n );\n\n // --- LAUNCH GAME (System -> Server) ---\n socket.on(\n \"system:launchGame\",\n (payload: SystemLaunchGamePayload, callback) => {\n const parsed = systemLaunchGameSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, gameUrl } = parsed.data;\n const session = roomManager.getRoom(roomId);\n\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n return;\n }\n\n if (session.masterHostSocketId !== socket.id) {\n callback({\n ok: false,\n message: \"Unauthorized: Not System Host\",\n code: ErrorCode.UNAUTHORIZED,\n });\n return;\n }\n\n // Check if game is already active\n if (session.childHostSocketId) {\n callback({\n ok: false,\n message: \"Game already active\",\n code: ErrorCode.ALREADY_CONNECTED,\n });\n return;\n }\n\n // Check if a launch is already in progress (joinToken exists but child hasn't joined yet)\n if (session.joinToken && !session.childHostSocketId) {\n callback({ ok: true, joinToken: session.joinToken });\n return;\n }\n\n // Generate Join Token\n const joinToken = uuidv4();\n session.joinToken = joinToken;\n session.activeControllerUrl = gameUrl;\n\n console.log(`[server] Launching game in room ${roomId}`);\n\n // Broadcast to controllers to load UI\n io.to(roomId).emit(\"client:loadUi\", { url: gameUrl });\n\n callback({ ok: true, joinToken });\n },\n );\n\n // --- JOIN AS CHILD (Game -> Server) ---\n socket.on(\n \"host:joinAsChild\",\n (payload: HostJoinAsChildPayload, callback) => {\n const parsed = hostJoinAsChildSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, joinToken } = parsed.data;\n const session = roomManager.getRoom(roomId);\n\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n return;\n }\n\n if (session.joinToken !== joinToken) {\n console.warn(\n `[server] Invalid join token for room ${roomId}. Expected ${session.joinToken}, got ${joinToken}`,\n );\n callback({\n ok: false,\n message: \"Invalid Join Token\",\n code: ErrorCode.INVALID_TOKEN,\n });\n return;\n }\n\n console.log(`[server] Game host joined room ${roomId}`);\n session.childHostSocketId = socket.id;\n session.focus = \"GAME\"; // Auto-focus on join\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n // Send initial state to the game\n\n // Small delay to ensure client is ready to receive events after ack\n setTimeout(() => {\n session.controllers.forEach((c) => {\n const notice: ControllerJoinedNotice = {\n controllerId: c.controllerId,\n nickname: c.nickname,\n player: c.playerProfile,\n };\n socket.emit(\"server:controllerJoined\", notice);\n });\n\n // Send current game state to the child host\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n socket.emit(\"server:state\", statePayload);\n }, 100);\n\n callback({ ok: true, roomId });\n },\n );\n\n // --- CLOSE GAME (System -> Server) ---\n socket.on(\"system:closeGame\", (payload: { roomId: string }) => {\n const { roomId } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (session.masterHostSocketId !== socket.id) {\n return;\n }\n\n console.log(`[server] Closing game in room ${roomId}`);\n\n // Disconnect child host if still connected\n if (session.childHostSocketId) {\n const childSocket = io.sockets.sockets.get(session.childHostSocketId);\n if (childSocket) {\n childSocket.disconnect(true);\n }\n }\n\n session.focus = \"SYSTEM\";\n session.childHostSocketId = undefined;\n session.joinToken = undefined;\n session.activeControllerUrl = undefined;\n\n // Tell controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n\n // Resync player list to master host when returning to SYSTEM focus\n // This ensures the master host has all players for arcade navigation\n if (session.masterHostSocketId) {\n const masterSocket = io.sockets.sockets.get(session.masterHostSocketId);\n if (masterSocket) {\n setTimeout(() => {\n session.controllers.forEach((c) => {\n const notice: ControllerJoinedNotice = {\n controllerId: c.controllerId,\n nickname: c.nickname,\n player: c.playerProfile,\n };\n masterSocket.emit(\"server:controllerJoined\", notice);\n });\n }, 100);\n }\n }\n });\n\n // --- LEGACY/STANDALONE HOST REGISTER ---\n // Keeping this for standalone development where there is no \"System\"\n socket.on(\n \"host:register\",\n async (payload: HostRegistrationPayload, callback) => {\n const parsed = hostRegistrationSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n const { roomId, maxPlayers } = parsed.data;\n\n // If mode is 'child', we should redirect them to use host:join_as_child if possible,\n // but for standalone dev, they might use this.\n // For now, we treat 'host:register' as creating a STANDALONE room or joining as master.\n\n let session = roomManager.getRoom(roomId);\n if (session) {\n // If room exists, we assume they are taking over or reconnecting as Master\n session.masterHostSocketId = socket.id;\n session.focus = \"SYSTEM\"; // Default to system/master focus\n } else {\n console.log(`[server] Creating standalone room ${roomId}`);\n session = {\n roomId,\n masterHostSocketId: socket.id,\n focus: \"SYSTEM\",\n controllers: new Map(),\n maxPlayers,\n gameState: \"paused\",\n };\n roomManager.setRoom(roomId, session);\n }\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n },\n );\n\n socket.on(\"controller:join\", (payload: ControllerJoinPayload, callback) => {\n const parsed = controllerJoinSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n const { roomId, controllerId, nickname } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n emitError(socket.id, {\n code: ErrorCode.ROOM_NOT_FOUND,\n message: \"Room not found\",\n });\n return;\n }\n\n // When a controller joins, we usually check maxPlayers.\n // However, for the ARCADE room, we want to allow MORE players than the game might support,\n // so they can queue up or watch.\n // But the current logic enforces `session.maxPlayers`.\n // If we want to allow \"observers\" or \"queue\", we should increase maxPlayers for the Arcade room itself.\n // The GAME itself (Child Host) might enforce its own player limit logic by ignoring inputs from extra players.\n\n // For now, let's keep the hard limit on the Room but maybe bump the default.\n if (session.controllers.size >= session.maxPlayers) {\n callback({\n ok: false,\n message: \"Room full\",\n code: ErrorCode.ROOM_FULL,\n });\n emitError(socket.id, {\n code: ErrorCode.ROOM_FULL,\n message: \"Room is full\",\n });\n return;\n }\n\n const existing = session.controllers.get(controllerId);\n if (existing) {\n roomManager.deleteController(existing.socketId);\n }\n\n const PLAYER_COLORS = [\n \"#38bdf8\",\n \"#a78bfa\",\n \"#f472b6\",\n \"#34d399\",\n \"#fbbf24\",\n \"#60a5fa\",\n \"#c084fc\",\n \"#fb7185\",\n \"#4ade80\",\n \"#f87171\",\n \"#22d3ee\",\n \"#a855f7\",\n \"#ec4899\",\n \"#10b981\",\n \"#f59e0b\",\n \"#3b82f6\",\n \"#8b5cf6\",\n \"#ef4444\",\n \"#14b8a6\",\n \"#f97316\",\n ];\n const colorHex =\n PLAYER_COLORS[session.controllers.size % PLAYER_COLORS.length];\n\n let color: string;\n try {\n color = Color(colorHex).hex();\n } catch {\n color = Color(\"#38bdf8\").hex();\n }\n\n const playerProfile: PlayerProfile = {\n id: controllerId,\n label: nickname ?? `Player ${session.controllers.size}`,\n color,\n };\n\n const controllerSession: ControllerSession = {\n controllerId,\n nickname,\n socketId: socket.id,\n playerProfile,\n };\n\n session.controllers.set(controllerId, controllerSession);\n roomManager.setController(socket.id, { roomId, controllerId });\n\n socket.join(roomId);\n\n const notice: ControllerJoinedNotice = {\n controllerId,\n nickname,\n player: playerProfile,\n };\n\n // Emit to Active Host based on Focus\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerJoined\",\n notice,\n );\n\n callback({ ok: true, controllerId, roomId });\n\n const welcomePayload = {\n controllerId,\n roomId,\n player: playerProfile,\n };\n\n socket.emit(\"server:welcome\", welcomePayload);\n\n // Send current game state to the new controller\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n socket.emit(\"server:state\", statePayload);\n\n // IMPORTANT: If a game is already active (activeControllerUrl set),\n // we must tell the new controller to load the game UI immediately.\n // We check activeControllerUrl instead of childHostSocketId because the game might be\n // in the process of loading (launched but not yet connected) or momentarily disconnected.\n if (session.activeControllerUrl) {\n socket.emit(\"client:loadUi\", { url: session.activeControllerUrl });\n }\n\n console.log(\n `[server] Controller joined room ${roomId} (${session.controllers.size}/${session.maxPlayers} players)`,\n );\n });\n\n socket.on(\"controller:leave\", (payload: ControllerLeavePayload) => {\n const parsed = controllerLeaveSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n const { roomId, controllerId } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n session.controllers.delete(controllerId);\n roomManager.deleteController(socket.id);\n const notice: ControllerLeftNotice = { controllerId };\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerLeft\",\n notice,\n );\n socket.leave(roomId);\n });\n\n socket.on(\"controller:input\", (payload: ControllerInputEvent) => {\n const now = Date.now();\n\n // Only log when there's actual user input (not just the loop sending zeros)\n const input = payload?.input;\n const hasActiveInput =\n input &&\n (input.action === true ||\n (typeof input.vector === \"object\" &&\n input.vector !== null &&\n (Math.abs((input.vector as { x?: number; y?: number }).x ?? 0) >\n 0.01 ||\n Math.abs((input.vector as { x?: number; y?: number }).y ?? 0) >\n 0.01)));\n\n // Throttled logging - only log active input, once per second max\n if (\n hasActiveInput &&\n (!lastServerInputLogTime || now - lastServerInputLogTime > 1000)\n ) {\n lastServerInputLogTime = now;\n }\n\n // Validate roomId and controllerId, but accept arbitrary input structure\n const result = controllerInputSchema.safeParse(payload);\n if (!result.success) {\n if (\n !lastServerInputFailLogTime ||\n now - lastServerInputFailLogTime > 1000\n ) {\n lastServerInputFailLogTime = now;\n }\n return;\n }\n\n const { roomId } = result.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n if (\n !lastServerInputFailLogTime ||\n now - lastServerInputFailLogTime > 1000\n ) {\n lastServerInputFailLogTime = now;\n }\n return;\n }\n\n // Route based on FOCUS - pass through arbitrary input to host\n const targetHostId = roomManager.getActiveHostId(session);\n if (targetHostId) {\n // Only log routing success if throttled\n if (!lastServerInputLogTime || now - lastServerInputLogTime > 1000) {\n lastServerInputLogTime = now;\n }\n io.to(targetHostId).emit(\"server:input\", result.data);\n } else {\n if (\n !lastServerInputFailLogTime ||\n now - lastServerInputFailLogTime > 1000\n ) {\n lastServerInputFailLogTime = now;\n }\n }\n });\n\n socket.on(\"controller:system\", (payload) => {\n const parsed = controllerSystemSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n\n const { roomId, command } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (command === \"exit\") {\n // Controller wants to exit the game - close the game on the server\n console.log(`[server] Controller exit request in room ${roomId}`);\n\n // Disconnect child host if still connected\n if (session.childHostSocketId) {\n const childSocket = io.sockets.sockets.get(session.childHostSocketId);\n if (childSocket) {\n childSocket.disconnect(true);\n }\n }\n\n // Update session state\n session.focus = \"SYSTEM\";\n session.childHostSocketId = undefined;\n session.joinToken = undefined;\n session.activeControllerUrl = undefined;\n session.gameState = \"paused\"; // Reset game state on exit\n\n // Tell all controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n\n // Tell the system host (arcade) to return to browser view\n if (session.masterHostSocketId) {\n io.to(session.masterHostSocketId).emit(\"server:closeChild\");\n }\n } else if (command === \"toggle_pause\") {\n // Toggle game state\n session.gameState =\n session.gameState === \"playing\" ? \"paused\" : \"playing\";\n\n // Broadcast new state to Room (Host + Controllers)\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n\n io.to(roomId).emit(\"server:state\", statePayload);\n }\n });\n\n socket.on(\"host:system\", (payload) => {\n const parsed = controllerSystemSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n\n const { roomId, command } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (command === \"toggle_pause\") {\n // Toggle game state - server is source of truth\n session.gameState =\n session.gameState === \"playing\" ? \"paused\" : \"playing\";\n\n // Broadcast new state to Room (Host + Controllers)\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n\n io.to(roomId).emit(\"server:state\", statePayload);\n }\n });\n\n socket.on(\"host:state\", (payload: ControllerStateMessage) => {\n const result = controllerStateSchema.safeParse(payload);\n if (!result.success) return;\n\n const { roomId, state } = result.data;\n const session = roomManager.getRoom(roomId);\n if (session) {\n // Sync state if provided\n if (state.gameState) {\n session.gameState = state.gameState;\n }\n\n // Broadcast to all controllers\n session.controllers.forEach((c) => {\n io.to(c.socketId).emit(\"server:state\", result.data);\n });\n\n // Broadcast to all hosts (system + child) to keep them in sync\n if (session.masterHostSocketId) {\n io.to(session.masterHostSocketId).emit(\"server:state\", result.data);\n }\n if (session.childHostSocketId) {\n io.to(session.childHostSocketId).emit(\"server:state\", result.data);\n }\n }\n });\n\n socket.on(\"host:signal\", (payload: SignalPayload) => {\n const roomId = roomManager.getRoomByHostId(socket.id);\n if (!roomId) return;\n\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n if (payload.targetId) {\n const controller = session.controllers.get(payload.targetId);\n if (controller) {\n io.to(controller.socketId).emit(\"server:signal\", payload);\n }\n } else {\n socket.to(roomId).emit(\"server:signal\", payload);\n }\n });\n\n socket.on(\"host:play_sound\", (payload: PlaySoundEventPayload) => {\n const { roomId, targetControllerId, soundId, volume, loop } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n const message = { id: soundId, volume, loop };\n\n if (targetControllerId) {\n const controller = session.controllers.get(targetControllerId);\n if (controller) {\n io.to(controller.socketId).emit(\"server:playSound\", message);\n }\n } else {\n socket.to(roomId).emit(\"server:playSound\", message);\n }\n });\n\n socket.on(\"controller:play_sound\", (payload: PlaySoundEventPayload) => {\n const { roomId, soundId, volume, loop } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n io.to(roomManager.getActiveHostId(session)).emit(\"server:playSound\", {\n id: soundId,\n volume,\n loop,\n });\n });\n\n // --- STORE SYNC (Host -> Server -> All) ---\n socket.on(\"host:state_sync\", (payload: HostStateSyncPayload) => {\n const { roomId, data } = payload;\n const session = roomManager.getRoom(roomId);\n\n // Security: Validate socket.id is a host for this room\n if (!session) {\n return;\n }\n if (\n session.masterHostSocketId !== socket.id &&\n session.childHostSocketId !== socket.id\n ) {\n return;\n }\n\n // Broadcast to room (Controllers + Other Hosts)\n const syncPayload: AirJamStateSyncPayload = {\n roomId,\n data,\n };\n // Use io.to() instead of socket.to() to ensure all sockets in the room receive the broadcast\n io.to(roomId).emit(\"airjam:state_sync\", syncPayload);\n });\n\n // --- ACTION RPC (Controller -> Server -> Host) ---\n socket.on(\n \"controller:action_rpc\",\n (payload: ControllerActionRpcPayload) => {\n const { roomId, actionName, args, controllerId } = payload;\n\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n // 1. Verify controller exists in session (by controllerId, not socket.id to handle reconnections)\n const controllerSession = session.controllers.get(controllerId);\n if (!controllerSession) {\n // Controller not found in session - might be a stale connection\n return;\n }\n\n // NOTE: We intentionally do NOT update the controller's socket ID in controllerIndex.\n // The action_rpc may come from a different socket (e.g., game iframe's socket)\n // than the original controller:join socket (shell socket).\n // The shell socket is the persistent connection, so we should NOT replace it\n // with the iframe's socket, or else when the iframe unloads we'd trigger\n // server:controllerLeft and remove the player incorrectly.\n //\n // HOWEVER, we DO need to add this socket to the room so it can receive\n // state sync broadcasts (airjam:state_sync). The game UI's createAirJamStore\n // registers listeners on this socket, so it needs to be in the room.\n if (controllerSession.socketId !== socket.id) {\n // Join the room for state sync broadcasts, but don't update controllerIndex\n socket.join(roomId);\n }\n\n // 2. Find the Active Host\n const hostId = roomManager.getActiveHostId(session);\n if (hostId) {\n // 3. Forward to Host (include controllerId so Host knows who sent it)\n const rpcPayload: AirJamActionRpcPayload = {\n actionName,\n args,\n controllerId,\n };\n io.to(hostId).emit(\"airjam:action_rpc\", rpcPayload);\n }\n },\n );\n\n socket.on(\"disconnect\", () => {\n const roomId = roomManager.getRoomByHostId(socket.id);\n if (roomId) {\n const session = roomManager.getRoom(roomId);\n if (!session) {\n roomManager.deleteHost(socket.id);\n return;\n }\n\n if (socket.id === session.childHostSocketId) {\n // Child disconnected\n console.log(`[server] Game host disconnected from room ${roomId}`);\n session.childHostSocketId = undefined;\n session.focus = \"SYSTEM\";\n session.joinToken = undefined;\n session.activeControllerUrl = undefined; // Clear active game URL\n\n // Tell controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n\n // Resync player list to master host when returning to SYSTEM focus\n // This ensures the master host has all players for arcade navigation\n if (session.masterHostSocketId) {\n const masterSocket = io.sockets.sockets.get(\n session.masterHostSocketId,\n );\n if (masterSocket) {\n setTimeout(() => {\n session.controllers.forEach((c) => {\n const notice: ControllerJoinedNotice = {\n controllerId: c.controllerId,\n nickname: c.nickname,\n player: c.playerProfile,\n };\n masterSocket.emit(\"server:controllerJoined\", notice);\n });\n }, 100);\n }\n }\n } else if (socket.id === session.masterHostSocketId) {\n // Master disconnected\n console.log(`[server] Host disconnected from room ${roomId}`);\n\n setTimeout(() => {\n const currentSession = roomManager.getRoom(roomId);\n if (\n currentSession &&\n currentSession.masterHostSocketId === socket.id\n ) {\n console.log(`[server] Removing room ${roomId}`);\n roomManager.removeRoom(roomId, io, \"Host disconnected\");\n }\n }, 3000);\n }\n\n roomManager.deleteHost(socket.id);\n return;\n }\n\n const controller = roomManager.getControllerInfo(socket.id);\n if (controller) {\n const session = roomManager.getRoom(controller.roomId);\n if (session) {\n session.controllers.delete(controller.controllerId);\n const notice: ControllerLeftNotice = {\n controllerId: controller.controllerId,\n };\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerLeft\",\n notice,\n );\n }\n roomManager.deleteController(socket.id);\n }\n });\n },\n);\n\nhttpServer.listen(PORT, () => {\n console.log(`[air-jam] server listening on http://localhost:${PORT}`);\n});\n","import { and, eq } from \"drizzle-orm\";\nimport { apiKeys, db } from \"../db.js\";\n\n/**\n * API key verification result\n */\nexport interface VerificationResult {\n isVerified: boolean;\n error?: string;\n}\n\n/**\n * Authentication service\n * Handles API key verification\n * In dev mode (no master key, no database), allows all connections\n */\nexport class AuthService {\n private masterKey: string | undefined;\n private databaseUrl: string | undefined;\n private isDevMode: boolean;\n\n constructor() {\n this.masterKey = process.env.AIR_JAM_MASTER_KEY;\n this.databaseUrl = process.env.DATABASE_URL;\n this.isDevMode = !this.masterKey && !this.databaseUrl;\n\n if (this.isDevMode) {\n console.log(\n \"[server] Running in development mode - authentication disabled\",\n );\n } else if (this.masterKey && !this.databaseUrl) {\n console.log(\n \"[server] Running with master key authentication (no database required)\",\n );\n } else if (this.databaseUrl) {\n console.log(\"[server] Running with database authentication\");\n }\n }\n\n /**\n * Verify an API key\n * Returns verification result with optional error message\n * In dev mode, always returns success\n */\n async verifyApiKey(apiKey?: string): Promise<VerificationResult> {\n // Dev mode: no auth required\n if (this.isDevMode) {\n return { isVerified: true };\n }\n\n if (!apiKey) {\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n }\n\n // Check master key first\n if (this.masterKey && apiKey === this.masterKey) {\n return { isVerified: true };\n }\n\n // Check database (only if database URL is configured)\n if (!this.databaseUrl || !db) {\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n }\n\n try {\n const [keyRecord] = await db\n .select()\n .from(apiKeys)\n .where(and(eq(apiKeys.key, apiKey), eq(apiKeys.isActive, true)))\n .limit(1);\n\n if (keyRecord) {\n // Update last used timestamp (fire and forget)\n db.update(apiKeys)\n .set({ lastUsedAt: new Date() })\n .where(eq(apiKeys.id, keyRecord.id))\n .catch((err: unknown) =>\n console.error(\"[server] Failed to update lastUsedAt\", err),\n );\n\n return { isVerified: true };\n }\n\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n } catch (error) {\n console.error(\"[server] Database error during key verification\", error);\n return {\n isVerified: false,\n error: \"Internal Server Error\",\n };\n }\n }\n}\n\n/**\n * Singleton instance\n */\nexport const authService = new AuthService();\n","import * as dotenv from \"dotenv\";\nimport { boolean, pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\n\ndotenv.config();\n\n// Define only the schema we need for verification\nexport const apiKeys = pgTable(\"api_keys\", {\n id: text(\"id\").primaryKey(),\n gameId: text(\"game_id\").notNull().unique(), // One API key per game\n key: text(\"key\").notNull().unique(),\n isActive: boolean(\"is_active\").default(true).notNull(),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n lastUsedAt: timestamp(\"last_used_at\"),\n});\n\nconst connectionString = process.env.DATABASE_URL;\n\n// Only create database client if DATABASE_URL is provided\n// In dev mode (no DATABASE_URL), the server runs without database\nconst client = connectionString ? postgres(connectionString) : null;\nexport const db = client ? drizzle(client) : null;\n","import type { RoomCode } from \"@air-jam/sdk/protocol\";\nimport type { Server } from \"socket.io\";\nimport type { ControllerIndexEntry, RoomSession } from \"../types.js\";\n\n/**\n * Room manager service\n * Handles all room state management and lookup operations\n */\nexport class RoomManager {\n private rooms = new Map<RoomCode, RoomSession>();\n private hostIndex = new Map<string, RoomCode>();\n private controllerIndex = new Map<string, ControllerIndexEntry>();\n\n /**\n * Get a room by ID\n */\n getRoom(roomId: RoomCode): RoomSession | undefined {\n return this.rooms.get(roomId);\n }\n\n /**\n * Create or update a room\n */\n setRoom(roomId: RoomCode, session: RoomSession): void {\n this.rooms.set(roomId, session);\n }\n\n /**\n * Delete a room\n */\n deleteRoom(roomId: RoomCode): void {\n this.rooms.delete(roomId);\n }\n\n /**\n * Get room ID by host socket ID\n */\n getRoomByHostId(socketId: string): RoomCode | undefined {\n return this.hostIndex.get(socketId);\n }\n\n /**\n * Associate a host socket with a room\n */\n setHostRoom(socketId: string, roomId: RoomCode): void {\n this.hostIndex.set(socketId, roomId);\n }\n\n /**\n * Remove host association\n */\n deleteHost(socketId: string): void {\n this.hostIndex.delete(socketId);\n }\n\n /**\n * Get controller info by socket ID\n */\n getControllerInfo(socketId: string): ControllerIndexEntry | undefined {\n return this.controllerIndex.get(socketId);\n }\n\n /**\n * Associate a controller socket with room and controller ID\n */\n setController(socketId: string, entry: ControllerIndexEntry): void {\n this.controllerIndex.set(socketId, entry);\n }\n\n /**\n * Remove controller association\n */\n deleteController(socketId: string): void {\n this.controllerIndex.delete(socketId);\n }\n\n /**\n * Get the active host socket ID based on focus\n */\n getActiveHostId(session: RoomSession): string {\n return session.focus === \"GAME\" && session.childHostSocketId\n ? session.childHostSocketId\n : session.masterHostSocketId;\n }\n\n /**\n * Remove a room and clean up all associations\n */\n removeRoom(roomId: RoomCode, io: Server, reason: string): void {\n const session = this.rooms.get(roomId);\n if (!session) return;\n\n // Notify all clients\n io.to(roomId).emit(\"server:hostLeft\", { roomId, reason });\n\n // Clean up controller indices\n session.controllers.forEach((controller) => {\n this.controllerIndex.delete(controller.socketId);\n });\n\n // Clean up host indices\n this.hostIndex.delete(session.masterHostSocketId);\n if (session.childHostSocketId) {\n this.hostIndex.delete(session.childHostSocketId);\n }\n\n // Remove room\n this.rooms.delete(roomId);\n }\n\n /**\n * Get all rooms (for debugging/monitoring)\n */\n getAllRooms(): Map<RoomCode, RoomSession> {\n return this.rooms;\n }\n}\n\n/**\n * Singleton instance\n */\nexport const roomManager = new RoomManager();\n","import { roomCodeSchema } from \"@air-jam/sdk/protocol\";\nimport { randomInt } from \"node:crypto\";\n\nconst alphabet = \"ABCDEFGHJKLMNPQRSTUVWXYZ23456789\";\n\n/**\n * Generate a random 4-character room code.\n * Uses Node.js crypto for secure random generation.\n */\nexport const generateRoomCode = (): string => {\n const code = Array.from(\n { length: 4 },\n () => alphabet[randomInt(0, alphabet.length)],\n ).join(\"\");\n\n return roomCodeSchema.parse(code);\n};\n"],"mappings":"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","names":["io"]}
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-T6BT7WE5.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@air-jam/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Air Jam game server for local development and production",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"build": "tsup",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
|
-
"prepublishOnly": "pnpm build"
|
|
29
|
+
"prepublishOnly": "node scripts/prepublish.js && pnpm build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@air-jam/sdk": "^0.1.
|
|
32
|
+
"@air-jam/sdk": "^0.1.2",
|
|
33
33
|
"color": "^5.0.3",
|
|
34
34
|
"cors": "^2.8.5",
|
|
35
35
|
"dotenv": "^17.2.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/services/auth-service.ts","../src/db.ts","../src/services/room-manager.ts"],"sourcesContent":["import {\n controllerInputSchema,\n controllerJoinSchema,\n controllerLeaveSchema,\n controllerStateSchema,\n controllerSystemSchema,\n ErrorCode,\n hostJoinAsChildSchema,\n hostRegisterSystemSchema,\n hostRegistrationSchema,\n PlaySoundEventPayload,\n SignalPayload,\n systemLaunchGameSchema,\n type AirJamActionRpcPayload,\n type AirJamStateSyncPayload,\n type ClientToServerEvents,\n type ControllerActionRpcPayload,\n type ControllerInputEvent,\n type ControllerJoinedNotice,\n type ControllerJoinPayload,\n type ControllerLeavePayload,\n type ControllerLeftNotice,\n type ControllerStateMessage,\n type HostJoinAsChildPayload,\n type HostRegisterSystemPayload,\n type HostRegistrationPayload,\n type HostStateSyncPayload,\n type InterServerEvents,\n type PlayerProfile,\n type ServerErrorPayload,\n type ServerToClientEvents,\n type SocketData,\n type SystemLaunchGamePayload,\n} from \"@air-jam/sdk/protocol\";\nimport Color from \"color\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport { createServer } from \"node:http\";\nimport { Server, type Socket } from \"socket.io\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { authService } from \"./services/auth-service.js\";\nimport { roomManager } from \"./services/room-manager.js\";\nimport type { ControllerSession } from \"./types.js\";\n\nconst PORT = Number(process.env.PORT ?? 4000);\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\napp.get(\"/health\", (_, res) => {\n res.json({ ok: true });\n});\n\nconst httpServer = createServer(app);\n\nconst io = new Server<\n ClientToServerEvents,\n ServerToClientEvents,\n InterServerEvents,\n SocketData\n>(httpServer, {\n cors: {\n origin: \"*\",\n },\n pingInterval: 2000,\n pingTimeout: 5000,\n});\n\nconst emitError = (socketId: string, payload: ServerErrorPayload): void => {\n io.to(socketId).emit(\"server:error\", payload);\n};\n\nio.on(\n \"connection\",\n (\n socket: Socket<\n ClientToServerEvents,\n ServerToClientEvents,\n InterServerEvents,\n SocketData\n >,\n ) => {\n // --- SYSTEM HOST REGISTRATION (Arcade) ---\n socket.on(\n \"host:registerSystem\",\n async (payload: HostRegisterSystemPayload, callback) => {\n const parsed = hostRegisterSystemSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, apiKey } = parsed.data;\n\n // API Key Validation using authService\n const verification = await authService.verifyApiKey(apiKey);\n if (!verification.isVerified) {\n console.warn(\n `[server] Unauthorized host registration attempt for room ${roomId}`,\n );\n callback({\n ok: false,\n message: verification.error,\n code: ErrorCode.INVALID_API_KEY,\n });\n return;\n }\n\n let session = roomManager.getRoom(roomId);\n\n if (session) {\n // Reconnect logic for System Host\n session.masterHostSocketId = socket.id;\n roomManager.setRoom(roomId, session);\n } else {\n // Create new room\n console.log(`[server] Creating room ${roomId}`);\n session = {\n roomId,\n masterHostSocketId: socket.id,\n focus: \"SYSTEM\",\n controllers: new Map(),\n maxPlayers: 32, // Default increased to 32 to allow for observers/queue\n gameState: \"paused\",\n };\n roomManager.setRoom(roomId, session);\n }\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n },\n );\n\n // --- LAUNCH GAME (System -> Server) ---\n socket.on(\n \"system:launchGame\",\n (payload: SystemLaunchGamePayload, callback) => {\n const parsed = systemLaunchGameSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, gameUrl } = parsed.data;\n const session = roomManager.getRoom(roomId);\n\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n return;\n }\n\n if (session.masterHostSocketId !== socket.id) {\n callback({\n ok: false,\n message: \"Unauthorized: Not System Host\",\n code: ErrorCode.UNAUTHORIZED,\n });\n return;\n }\n\n // Check if game is already active\n if (session.childHostSocketId) {\n callback({\n ok: false,\n message: \"Game already active\",\n code: ErrorCode.ALREADY_CONNECTED,\n });\n return;\n }\n\n // Check if a launch is already in progress (joinToken exists but child hasn't joined yet)\n if (session.joinToken && !session.childHostSocketId) {\n callback({ ok: true, joinToken: session.joinToken });\n return;\n }\n\n // Generate Join Token\n const joinToken = uuidv4();\n session.joinToken = joinToken;\n session.activeControllerUrl = gameUrl;\n\n console.log(`[server] Launching game in room ${roomId}`);\n\n // Broadcast to controllers to load UI\n io.to(roomId).emit(\"client:loadUi\", { url: gameUrl });\n\n callback({ ok: true, joinToken });\n },\n );\n\n // --- JOIN AS CHILD (Game -> Server) ---\n socket.on(\n \"host:joinAsChild\",\n (payload: HostJoinAsChildPayload, callback) => {\n const parsed = hostJoinAsChildSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n\n const { roomId, joinToken } = parsed.data;\n const session = roomManager.getRoom(roomId);\n\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n return;\n }\n\n if (session.joinToken !== joinToken) {\n console.warn(\n `[server] Invalid join token for room ${roomId}. Expected ${session.joinToken}, got ${joinToken}`,\n );\n callback({\n ok: false,\n message: \"Invalid Join Token\",\n code: ErrorCode.INVALID_TOKEN,\n });\n return;\n }\n\n console.log(`[server] Game host joined room ${roomId}`);\n session.childHostSocketId = socket.id;\n session.focus = \"GAME\"; // Auto-focus on join\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n\n // Send initial state to the game\n\n // Small delay to ensure client is ready to receive events after ack\n setTimeout(() => {\n session.controllers.forEach((c) => {\n const notice: ControllerJoinedNotice = {\n controllerId: c.controllerId,\n nickname: c.nickname,\n player: c.playerProfile,\n };\n socket.emit(\"server:controllerJoined\", notice);\n });\n\n // Send current game state to the child host\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n socket.emit(\"server:state\", statePayload);\n }, 100);\n\n callback({ ok: true, roomId });\n },\n );\n\n // --- CLOSE GAME (System -> Server) ---\n socket.on(\"system:closeGame\", (payload: { roomId: string }) => {\n const { roomId } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (session.masterHostSocketId !== socket.id) {\n return;\n }\n\n console.log(`[server] Closing game in room ${roomId}`);\n\n // Disconnect child host if still connected\n if (session.childHostSocketId) {\n const childSocket = io.sockets.sockets.get(session.childHostSocketId);\n if (childSocket) {\n childSocket.disconnect(true);\n }\n }\n\n session.focus = \"SYSTEM\";\n session.childHostSocketId = undefined;\n session.joinToken = undefined;\n session.activeControllerUrl = undefined;\n\n // Tell controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n });\n\n // --- LEGACY/STANDALONE HOST REGISTER ---\n // Keeping this for standalone development where there is no \"System\"\n socket.on(\n \"host:register\",\n async (payload: HostRegistrationPayload, callback) => {\n const parsed = hostRegistrationSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n const { roomId, maxPlayers } = parsed.data;\n\n // If mode is 'child', we should redirect them to use host:join_as_child if possible,\n // but for standalone dev, they might use this.\n // For now, we treat 'host:register' as creating a STANDALONE room or joining as master.\n\n let session = roomManager.getRoom(roomId);\n if (session) {\n // If room exists, we assume they are taking over or reconnecting as Master\n session.masterHostSocketId = socket.id;\n session.focus = \"SYSTEM\"; // Default to system/master focus\n } else {\n console.log(`[server] Creating standalone room ${roomId}`);\n session = {\n roomId,\n masterHostSocketId: socket.id,\n focus: \"SYSTEM\",\n controllers: new Map(),\n maxPlayers,\n gameState: \"paused\",\n };\n roomManager.setRoom(roomId, session);\n }\n\n roomManager.setHostRoom(socket.id, roomId);\n socket.join(roomId);\n callback({ ok: true, roomId });\n io.to(roomId).emit(\"server:roomReady\", { roomId });\n },\n );\n\n socket.on(\"controller:join\", (payload: ControllerJoinPayload, callback) => {\n const parsed = controllerJoinSchema.safeParse(payload);\n if (!parsed.success) {\n callback({\n ok: false,\n message: parsed.error.message,\n code: ErrorCode.INVALID_PAYLOAD,\n });\n return;\n }\n const { roomId, controllerId, nickname } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n callback({\n ok: false,\n message: \"Room not found\",\n code: ErrorCode.ROOM_NOT_FOUND,\n });\n emitError(socket.id, {\n code: ErrorCode.ROOM_NOT_FOUND,\n message: \"Room not found\",\n });\n return;\n }\n\n // When a controller joins, we usually check maxPlayers.\n // However, for the ARCADE room, we want to allow MORE players than the game might support,\n // so they can queue up or watch.\n // But the current logic enforces `session.maxPlayers`.\n // If we want to allow \"observers\" or \"queue\", we should increase maxPlayers for the Arcade room itself.\n // The GAME itself (Child Host) might enforce its own player limit logic by ignoring inputs from extra players.\n\n // For now, let's keep the hard limit on the Room but maybe bump the default.\n if (session.controllers.size >= session.maxPlayers) {\n callback({\n ok: false,\n message: \"Room full\",\n code: ErrorCode.ROOM_FULL,\n });\n emitError(socket.id, {\n code: ErrorCode.ROOM_FULL,\n message: \"Room is full\",\n });\n return;\n }\n\n const existing = session.controllers.get(controllerId);\n if (existing) {\n roomManager.deleteController(existing.socketId);\n }\n\n const PLAYER_COLORS = [\n \"#38bdf8\",\n \"#a78bfa\",\n \"#f472b6\",\n \"#34d399\",\n \"#fbbf24\",\n \"#60a5fa\",\n \"#c084fc\",\n \"#fb7185\",\n \"#4ade80\",\n \"#f87171\",\n \"#22d3ee\",\n \"#a855f7\",\n \"#ec4899\",\n \"#10b981\",\n \"#f59e0b\",\n \"#3b82f6\",\n \"#8b5cf6\",\n \"#ef4444\",\n \"#14b8a6\",\n \"#f97316\",\n ];\n const colorHex =\n PLAYER_COLORS[session.controllers.size % PLAYER_COLORS.length];\n\n let color: string;\n try {\n color = Color(colorHex).hex();\n } catch {\n color = Color(\"#38bdf8\").hex();\n }\n\n const playerProfile: PlayerProfile = {\n id: controllerId,\n label: nickname ?? `Player ${session.controllers.size}`,\n color,\n };\n\n const controllerSession: ControllerSession = {\n controllerId,\n nickname,\n socketId: socket.id,\n playerProfile,\n };\n\n session.controllers.set(controllerId, controllerSession);\n roomManager.setController(socket.id, { roomId, controllerId });\n socket.join(roomId);\n\n const notice: ControllerJoinedNotice = {\n controllerId,\n nickname,\n player: playerProfile,\n };\n\n // Emit to Active Host based on Focus\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerJoined\",\n notice,\n );\n\n callback({ ok: true, controllerId, roomId });\n\n const welcomePayload = {\n controllerId,\n roomId,\n player: playerProfile,\n };\n\n socket.emit(\"server:welcome\", welcomePayload);\n\n // Send current game state to the new controller\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n socket.emit(\"server:state\", statePayload);\n\n // IMPORTANT: If a game is already active (activeControllerUrl set),\n // we must tell the new controller to load the game UI immediately.\n // We check activeControllerUrl instead of childHostSocketId because the game might be\n // in the process of loading (launched but not yet connected) or momentarily disconnected.\n if (session.activeControllerUrl) {\n socket.emit(\"client:loadUi\", { url: session.activeControllerUrl });\n }\n\n console.log(\n `[server] Controller joined room ${roomId} (${session.controllers.size}/${session.maxPlayers} players)`,\n );\n });\n\n socket.on(\"controller:leave\", (payload: ControllerLeavePayload) => {\n const parsed = controllerLeaveSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n const { roomId, controllerId } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n session.controllers.delete(controllerId);\n roomManager.deleteController(socket.id);\n const notice: ControllerLeftNotice = { controllerId };\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerLeft\",\n notice,\n );\n socket.leave(roomId);\n });\n\n socket.on(\"controller:input\", (payload: ControllerInputEvent) => {\n // Validate roomId and controllerId, but accept arbitrary input structure\n const result = controllerInputSchema.safeParse(payload);\n if (!result.success) {\n return;\n }\n\n const { roomId } = result.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n // Route based on FOCUS - pass through arbitrary input to host\n const targetHostId = roomManager.getActiveHostId(session);\n if (targetHostId) {\n io.to(targetHostId).emit(\"server:input\", result.data);\n }\n });\n\n socket.on(\"controller:system\", (payload) => {\n const parsed = controllerSystemSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n\n const { roomId, command } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (command === \"exit\") {\n // Controller wants to exit the game - close the game on the server\n console.log(`[server] Controller exit request in room ${roomId}`);\n\n // Disconnect child host if still connected\n if (session.childHostSocketId) {\n const childSocket = io.sockets.sockets.get(session.childHostSocketId);\n if (childSocket) {\n childSocket.disconnect(true);\n }\n }\n\n // Update session state\n session.focus = \"SYSTEM\";\n session.childHostSocketId = undefined;\n session.joinToken = undefined;\n session.activeControllerUrl = undefined;\n session.gameState = \"paused\"; // Reset game state on exit\n\n // Tell all controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n\n // Tell the system host (arcade) to return to browser view\n if (session.masterHostSocketId) {\n io.to(session.masterHostSocketId).emit(\"server:closeChild\");\n }\n } else if (command === \"toggle_pause\") {\n // Toggle game state\n session.gameState =\n session.gameState === \"playing\" ? \"paused\" : \"playing\";\n\n // Broadcast new state to Room (Host + Controllers)\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n\n io.to(roomId).emit(\"server:state\", statePayload);\n }\n });\n\n socket.on(\"host:system\", (payload) => {\n const parsed = controllerSystemSchema.safeParse(payload);\n if (!parsed.success) {\n return;\n }\n\n const { roomId, command } = parsed.data;\n const session = roomManager.getRoom(roomId);\n if (!session) {\n return;\n }\n\n if (command === \"toggle_pause\") {\n // Toggle game state - server is source of truth\n session.gameState =\n session.gameState === \"playing\" ? \"paused\" : \"playing\";\n\n // Broadcast new state to Room (Host + Controllers)\n const statePayload = {\n roomId,\n state: {\n gameState: session.gameState,\n },\n };\n\n io.to(roomId).emit(\"server:state\", statePayload);\n }\n });\n\n socket.on(\"host:state\", (payload: ControllerStateMessage) => {\n const result = controllerStateSchema.safeParse(payload);\n if (!result.success) return;\n\n const { roomId, state } = result.data;\n const session = roomManager.getRoom(roomId);\n if (session) {\n // Sync state if provided\n if (state.gameState) {\n session.gameState = state.gameState;\n }\n\n // Broadcast to all controllers\n session.controllers.forEach((c) => {\n io.to(c.socketId).emit(\"server:state\", result.data);\n });\n\n // Broadcast to all hosts (system + child) to keep them in sync\n if (session.masterHostSocketId) {\n io.to(session.masterHostSocketId).emit(\"server:state\", result.data);\n }\n if (session.childHostSocketId) {\n io.to(session.childHostSocketId).emit(\"server:state\", result.data);\n }\n }\n });\n\n socket.on(\"host:signal\", (payload: SignalPayload) => {\n const roomId = roomManager.getRoomByHostId(socket.id);\n if (!roomId) return;\n\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n if (payload.targetId) {\n const controller = session.controllers.get(payload.targetId);\n if (controller) {\n io.to(controller.socketId).emit(\"server:signal\", payload);\n }\n } else {\n socket.to(roomId).emit(\"server:signal\", payload);\n }\n });\n\n socket.on(\"host:play_sound\", (payload: PlaySoundEventPayload) => {\n const { roomId, targetControllerId, soundId, volume, loop } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n const message = { id: soundId, volume, loop };\n\n if (targetControllerId) {\n const controller = session.controllers.get(targetControllerId);\n if (controller) {\n io.to(controller.socketId).emit(\"server:playSound\", message);\n }\n } else {\n socket.to(roomId).emit(\"server:playSound\", message);\n }\n });\n\n socket.on(\"controller:play_sound\", (payload: PlaySoundEventPayload) => {\n const { roomId, soundId, volume, loop } = payload;\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n io.to(roomManager.getActiveHostId(session)).emit(\"server:playSound\", {\n id: soundId,\n volume,\n loop,\n });\n });\n\n // --- STORE SYNC (Host -> Server -> All) ---\n socket.on(\"host:state_sync\", (payload: HostStateSyncPayload) => {\n const { roomId, data } = payload;\n const session = roomManager.getRoom(roomId);\n\n // Security: Validate socket.id is a host for this room\n if (!session) {\n return;\n }\n if (\n session.masterHostSocketId !== socket.id &&\n session.childHostSocketId !== socket.id\n ) {\n return;\n }\n\n // Broadcast to room (Controllers + Other Hosts)\n const syncPayload: AirJamStateSyncPayload = {\n roomId,\n data,\n };\n // Use io.to() instead of socket.to() to ensure all sockets in the room receive the broadcast\n io.to(roomId).emit(\"airjam:state_sync\", syncPayload);\n });\n\n // --- ACTION RPC (Controller -> Server -> Host) ---\n socket.on(\n \"controller:action_rpc\",\n (payload: ControllerActionRpcPayload) => {\n const { roomId, actionName, args, controllerId } = payload;\n\n const session = roomManager.getRoom(roomId);\n if (!session) return;\n\n // 1. Verify controller exists in session (by controllerId, not socket.id to handle reconnections)\n const controllerSession = session.controllers.get(controllerId);\n if (!controllerSession) {\n // Controller not found in session - might be a stale connection\n return;\n }\n\n // Update controller's socket ID in case it reconnected\n if (controllerSession.socketId !== socket.id) {\n // Remove old socket mapping\n roomManager.deleteController(controllerSession.socketId);\n // Update to new socket ID\n controllerSession.socketId = socket.id;\n roomManager.setController(socket.id, { roomId, controllerId });\n // Ensure the new socket joins the room\n socket.join(roomId);\n }\n\n // 2. Find the Active Host\n const hostId = roomManager.getActiveHostId(session);\n if (hostId) {\n // 3. Forward to Host (include controllerId so Host knows who sent it)\n const rpcPayload: AirJamActionRpcPayload = {\n actionName,\n args,\n controllerId,\n };\n io.to(hostId).emit(\"airjam:action_rpc\", rpcPayload);\n }\n },\n );\n\n socket.on(\"disconnect\", () => {\n const roomId = roomManager.getRoomByHostId(socket.id);\n if (roomId) {\n const session = roomManager.getRoom(roomId);\n if (!session) {\n roomManager.deleteHost(socket.id);\n return;\n }\n\n if (socket.id === session.childHostSocketId) {\n // Child disconnected\n console.log(`[server] Game host disconnected from room ${roomId}`);\n session.childHostSocketId = undefined;\n session.focus = \"SYSTEM\";\n session.joinToken = undefined;\n session.activeControllerUrl = undefined; // Clear active game URL\n\n // Tell controllers to unload UI\n io.to(roomId).emit(\"client:unloadUi\");\n } else if (socket.id === session.masterHostSocketId) {\n // Master disconnected\n console.log(`[server] Host disconnected from room ${roomId}`);\n\n setTimeout(() => {\n const currentSession = roomManager.getRoom(roomId);\n if (\n currentSession &&\n currentSession.masterHostSocketId === socket.id\n ) {\n console.log(`[server] Removing room ${roomId}`);\n roomManager.removeRoom(roomId, io, \"Host disconnected\");\n }\n }, 3000);\n }\n\n roomManager.deleteHost(socket.id);\n return;\n }\n\n const controller = roomManager.getControllerInfo(socket.id);\n if (controller) {\n const session = roomManager.getRoom(controller.roomId);\n if (session) {\n session.controllers.delete(controller.controllerId);\n const notice: ControllerLeftNotice = {\n controllerId: controller.controllerId,\n };\n io.to(roomManager.getActiveHostId(session)).emit(\n \"server:controllerLeft\",\n notice,\n );\n }\n roomManager.deleteController(socket.id);\n }\n });\n },\n);\n\nhttpServer.listen(PORT, () => {\n console.log(`[air-jam] server listening on http://localhost:${PORT}`);\n});\n","import { and, eq } from \"drizzle-orm\";\nimport { apiKeys, db } from \"../db.js\";\n\n/**\n * API key verification result\n */\nexport interface VerificationResult {\n isVerified: boolean;\n error?: string;\n}\n\n/**\n * Authentication service\n * Handles API key verification\n * In dev mode (no master key, no database), allows all connections\n */\nexport class AuthService {\n private masterKey: string | undefined;\n private databaseUrl: string | undefined;\n private isDevMode: boolean;\n\n constructor() {\n this.masterKey = process.env.AIR_JAM_MASTER_KEY;\n this.databaseUrl = process.env.DATABASE_URL;\n this.isDevMode = !this.masterKey && !this.databaseUrl;\n\n if (this.isDevMode) {\n console.log(\n \"[server] Running in development mode - authentication disabled\",\n );\n } else if (this.masterKey && !this.databaseUrl) {\n console.log(\n \"[server] Running with master key authentication (no database required)\",\n );\n } else if (this.databaseUrl) {\n console.log(\"[server] Running with database authentication\");\n }\n }\n\n /**\n * Verify an API key\n * Returns verification result with optional error message\n * In dev mode, always returns success\n */\n async verifyApiKey(apiKey?: string): Promise<VerificationResult> {\n // Dev mode: no auth required\n if (this.isDevMode) {\n return { isVerified: true };\n }\n\n if (!apiKey) {\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n }\n\n // Check master key first\n if (this.masterKey && apiKey === this.masterKey) {\n return { isVerified: true };\n }\n\n // Check database (only if database URL is configured)\n if (!this.databaseUrl || !db) {\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n }\n\n try {\n const [keyRecord] = await db\n .select()\n .from(apiKeys)\n .where(and(eq(apiKeys.key, apiKey), eq(apiKeys.isActive, true)))\n .limit(1);\n\n if (keyRecord) {\n // Update last used timestamp (fire and forget)\n db.update(apiKeys)\n .set({ lastUsedAt: new Date() })\n .where(eq(apiKeys.id, keyRecord.id))\n .catch((err: unknown) =>\n console.error(\"[server] Failed to update lastUsedAt\", err),\n );\n\n return { isVerified: true };\n }\n\n return {\n isVerified: false,\n error: \"Unauthorized: Invalid or Missing API Key\",\n };\n } catch (error) {\n console.error(\"[server] Database error during key verification\", error);\n return {\n isVerified: false,\n error: \"Internal Server Error\",\n };\n }\n }\n}\n\n/**\n * Singleton instance\n */\nexport const authService = new AuthService();\n","import * as dotenv from \"dotenv\";\nimport { boolean, pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\n\ndotenv.config();\n\n// Define only the schema we need for verification\nexport const apiKeys = pgTable(\"api_keys\", {\n id: text(\"id\").primaryKey(),\n gameId: text(\"game_id\").notNull().unique(), // One API key per game\n key: text(\"key\").notNull().unique(),\n isActive: boolean(\"is_active\").default(true).notNull(),\n createdAt: timestamp(\"created_at\").defaultNow().notNull(),\n lastUsedAt: timestamp(\"last_used_at\"),\n});\n\nconst connectionString = process.env.DATABASE_URL;\n\n// Only create database client if DATABASE_URL is provided\n// In dev mode (no DATABASE_URL), the server runs without database\nconst client = connectionString ? postgres(connectionString) : null;\nexport const db = client ? drizzle(client) : null;\n","import type { RoomCode } from \"@air-jam/sdk/protocol\";\nimport type { Server } from \"socket.io\";\nimport type { ControllerIndexEntry, RoomSession } from \"../types.js\";\n\n/**\n * Room manager service\n * Handles all room state management and lookup operations\n */\nexport class RoomManager {\n private rooms = new Map<RoomCode, RoomSession>();\n private hostIndex = new Map<string, RoomCode>();\n private controllerIndex = new Map<string, ControllerIndexEntry>();\n\n /**\n * Get a room by ID\n */\n getRoom(roomId: RoomCode): RoomSession | undefined {\n return this.rooms.get(roomId);\n }\n\n /**\n * Create or update a room\n */\n setRoom(roomId: RoomCode, session: RoomSession): void {\n this.rooms.set(roomId, session);\n }\n\n /**\n * Delete a room\n */\n deleteRoom(roomId: RoomCode): void {\n this.rooms.delete(roomId);\n }\n\n /**\n * Get room ID by host socket ID\n */\n getRoomByHostId(socketId: string): RoomCode | undefined {\n return this.hostIndex.get(socketId);\n }\n\n /**\n * Associate a host socket with a room\n */\n setHostRoom(socketId: string, roomId: RoomCode): void {\n this.hostIndex.set(socketId, roomId);\n }\n\n /**\n * Remove host association\n */\n deleteHost(socketId: string): void {\n this.hostIndex.delete(socketId);\n }\n\n /**\n * Get controller info by socket ID\n */\n getControllerInfo(socketId: string): ControllerIndexEntry | undefined {\n return this.controllerIndex.get(socketId);\n }\n\n /**\n * Associate a controller socket with room and controller ID\n */\n setController(socketId: string, entry: ControllerIndexEntry): void {\n this.controllerIndex.set(socketId, entry);\n }\n\n /**\n * Remove controller association\n */\n deleteController(socketId: string): void {\n this.controllerIndex.delete(socketId);\n }\n\n /**\n * Get the active host socket ID based on focus\n */\n getActiveHostId(session: RoomSession): string {\n return session.focus === \"GAME\" && session.childHostSocketId\n ? session.childHostSocketId\n : session.masterHostSocketId;\n }\n\n /**\n * Remove a room and clean up all associations\n */\n removeRoom(roomId: RoomCode, io: Server, reason: string): void {\n const session = this.rooms.get(roomId);\n if (!session) return;\n\n // Notify all clients\n io.to(roomId).emit(\"server:hostLeft\", { roomId, reason });\n\n // Clean up controller indices\n session.controllers.forEach((controller) => {\n this.controllerIndex.delete(controller.socketId);\n });\n\n // Clean up host indices\n this.hostIndex.delete(session.masterHostSocketId);\n if (session.childHostSocketId) {\n this.hostIndex.delete(session.childHostSocketId);\n }\n\n // Remove room\n this.rooms.delete(roomId);\n }\n\n /**\n * Get all rooms (for debugging/monitoring)\n */\n getAllRooms(): Map<RoomCode, RoomSession> {\n return this.rooms;\n }\n}\n\n/**\n * Singleton instance\n */\nexport const roomManager = new RoomManager();\n"],"mappings":"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","names":["io"]}
|