@air-jam/server 0.1.3 → 0.1.4
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/dist/.tsbuildinfo +1 -1
- package/dist/{chunk-T6BT7WE5.js → chunk-CGONPUNG.js} +195 -30
- package/dist/chunk-CGONPUNG.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +12 -10
- package/dist/chunk-T6BT7WE5.js.map +0 -1
|
@@ -46,12 +46,12 @@ var db = client ? drizzle(client) : null;
|
|
|
46
46
|
var AuthService = class {
|
|
47
47
|
masterKey;
|
|
48
48
|
databaseUrl;
|
|
49
|
-
|
|
49
|
+
authMode;
|
|
50
50
|
constructor() {
|
|
51
51
|
this.masterKey = process.env.AIR_JAM_MASTER_KEY;
|
|
52
52
|
this.databaseUrl = process.env.DATABASE_URL;
|
|
53
|
-
this.
|
|
54
|
-
if (this.
|
|
53
|
+
this.authMode = this.resolveAuthMode();
|
|
54
|
+
if (this.authMode === "disabled") {
|
|
55
55
|
console.log(
|
|
56
56
|
"[server] Running in development mode - authentication disabled"
|
|
57
57
|
);
|
|
@@ -61,15 +61,19 @@ var AuthService = class {
|
|
|
61
61
|
);
|
|
62
62
|
} else if (this.databaseUrl) {
|
|
63
63
|
console.log("[server] Running with database authentication");
|
|
64
|
+
} else {
|
|
65
|
+
console.log(
|
|
66
|
+
"[server] Authentication required, but no auth backend is configured (set AIR_JAM_MASTER_KEY or DATABASE_URL)"
|
|
67
|
+
);
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
71
|
* Verify an API key
|
|
68
72
|
* Returns verification result with optional error message
|
|
69
|
-
* In dev mode, always returns success
|
|
73
|
+
* In local/dev mode, always returns success
|
|
70
74
|
*/
|
|
71
75
|
async verifyApiKey(apiKey) {
|
|
72
|
-
if (this.
|
|
76
|
+
if (this.authMode === "disabled") {
|
|
73
77
|
return { isVerified: true };
|
|
74
78
|
}
|
|
75
79
|
if (!apiKey) {
|
|
@@ -107,9 +111,66 @@ var AuthService = class {
|
|
|
107
111
|
};
|
|
108
112
|
}
|
|
109
113
|
}
|
|
114
|
+
resolveAuthMode() {
|
|
115
|
+
const configuredMode = process.env.AIR_JAM_AUTH_MODE?.toLowerCase();
|
|
116
|
+
if (configuredMode === "disabled") {
|
|
117
|
+
return "disabled";
|
|
118
|
+
}
|
|
119
|
+
if (configuredMode === "required") {
|
|
120
|
+
return "required";
|
|
121
|
+
}
|
|
122
|
+
if (this.masterKey || this.databaseUrl) {
|
|
123
|
+
return "required";
|
|
124
|
+
}
|
|
125
|
+
if (process.env.NODE_ENV === "production") {
|
|
126
|
+
return "required";
|
|
127
|
+
}
|
|
128
|
+
return "disabled";
|
|
129
|
+
}
|
|
110
130
|
};
|
|
111
131
|
var authService = new AuthService();
|
|
112
132
|
|
|
133
|
+
// src/services/rate-limit-service.ts
|
|
134
|
+
var RateLimitService = class {
|
|
135
|
+
entries = /* @__PURE__ */ new Map();
|
|
136
|
+
checks = 0;
|
|
137
|
+
check(key, limit, windowMs) {
|
|
138
|
+
if (limit <= 0 || windowMs <= 0) {
|
|
139
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
140
|
+
}
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const existing = this.entries.get(key);
|
|
143
|
+
if (!existing || now >= existing.resetAt) {
|
|
144
|
+
this.entries.set(key, { count: 1, resetAt: now + windowMs });
|
|
145
|
+
this.maybeCleanup(now);
|
|
146
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
147
|
+
}
|
|
148
|
+
if (existing.count >= limit) {
|
|
149
|
+
this.maybeCleanup(now);
|
|
150
|
+
return {
|
|
151
|
+
allowed: false,
|
|
152
|
+
retryAfterMs: Math.max(existing.resetAt - now, 0)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
existing.count += 1;
|
|
156
|
+
this.entries.set(key, existing);
|
|
157
|
+
this.maybeCleanup(now);
|
|
158
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
159
|
+
}
|
|
160
|
+
maybeCleanup(now) {
|
|
161
|
+
this.checks += 1;
|
|
162
|
+
if (this.checks % 200 !== 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const [key, entry] of this.entries) {
|
|
166
|
+
if (now >= entry.resetAt) {
|
|
167
|
+
this.entries.delete(key);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var rateLimitService = new RateLimitService();
|
|
173
|
+
|
|
113
174
|
// src/services/room-manager.ts
|
|
114
175
|
var RoomManager = class {
|
|
115
176
|
rooms = /* @__PURE__ */ new Map();
|
|
@@ -215,9 +276,27 @@ var generateRoomCode = () => {
|
|
|
215
276
|
// src/index.ts
|
|
216
277
|
var lastServerInputLogTime = 0;
|
|
217
278
|
var lastServerInputFailLogTime = 0;
|
|
279
|
+
var parsePositiveInt = (value, fallback) => {
|
|
280
|
+
const parsed = Number(value);
|
|
281
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
282
|
+
};
|
|
218
283
|
var PORT = Number(process.env.PORT ?? 4e3);
|
|
284
|
+
var RATE_LIMIT_WINDOW_MS = parsePositiveInt(
|
|
285
|
+
process.env.AIR_JAM_RATE_LIMIT_WINDOW_MS,
|
|
286
|
+
6e4
|
|
287
|
+
);
|
|
288
|
+
var HOST_REGISTRATION_RATE_LIMIT_MAX = parsePositiveInt(
|
|
289
|
+
process.env.AIR_JAM_HOST_REGISTRATION_RATE_LIMIT_MAX,
|
|
290
|
+
30
|
|
291
|
+
);
|
|
292
|
+
var CONTROLLER_JOIN_RATE_LIMIT_MAX = parsePositiveInt(
|
|
293
|
+
process.env.AIR_JAM_CONTROLLER_JOIN_RATE_LIMIT_MAX,
|
|
294
|
+
120
|
|
295
|
+
);
|
|
296
|
+
var allowedOrigins = process.env.AIR_JAM_ALLOWED_ORIGINS?.split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
297
|
+
var corsOrigin = allowedOrigins && allowedOrigins.length > 0 ? allowedOrigins : "*";
|
|
219
298
|
var app = express();
|
|
220
|
-
app.use(cors());
|
|
299
|
+
app.use(cors({ origin: corsOrigin }));
|
|
221
300
|
app.use(express.json());
|
|
222
301
|
app.get("/health", (_, res) => {
|
|
223
302
|
res.json({ ok: true });
|
|
@@ -225,7 +304,7 @@ app.get("/health", (_, res) => {
|
|
|
225
304
|
var httpServer = createServer(app);
|
|
226
305
|
var io = new Server(httpServer, {
|
|
227
306
|
cors: {
|
|
228
|
-
origin:
|
|
307
|
+
origin: corsOrigin
|
|
229
308
|
},
|
|
230
309
|
pingInterval: 2e3,
|
|
231
310
|
pingTimeout: 5e3
|
|
@@ -236,9 +315,40 @@ var emitError = (socketId, payload) => {
|
|
|
236
315
|
io.on(
|
|
237
316
|
"connection",
|
|
238
317
|
(socket) => {
|
|
318
|
+
const isHostAuthorizedForRoom = (roomId) => {
|
|
319
|
+
return roomManager.getRoomByHostId(socket.id) === roomId;
|
|
320
|
+
};
|
|
321
|
+
const isControllerAuthorizedForRoom = (roomId, controllerId) => {
|
|
322
|
+
const controllerInfo = roomManager.getControllerInfo(socket.id);
|
|
323
|
+
if (!controllerInfo || controllerInfo.roomId !== roomId) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (controllerId && controllerInfo.controllerId !== controllerId) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
};
|
|
331
|
+
const forwardedFor = socket.handshake.headers["x-forwarded-for"];
|
|
332
|
+
const socketIdentifier = typeof forwardedFor === "string" && forwardedFor.split(",")[0]?.trim() || Array.isArray(forwardedFor) && forwardedFor[0]?.split(",")[0]?.trim() || socket.handshake.address || socket.id;
|
|
333
|
+
const isRateLimited = (bucket, limit) => {
|
|
334
|
+
const result = rateLimitService.check(
|
|
335
|
+
`${bucket}:${socketIdentifier}`,
|
|
336
|
+
limit,
|
|
337
|
+
RATE_LIMIT_WINDOW_MS
|
|
338
|
+
);
|
|
339
|
+
return !result.allowed;
|
|
340
|
+
};
|
|
239
341
|
socket.on(
|
|
240
342
|
"host:registerSystem",
|
|
241
343
|
async (payload, callback) => {
|
|
344
|
+
if (isRateLimited("host-registration", HOST_REGISTRATION_RATE_LIMIT_MAX)) {
|
|
345
|
+
callback({
|
|
346
|
+
ok: false,
|
|
347
|
+
message: "Too many host registration attempts. Please try again.",
|
|
348
|
+
code: ErrorCode.SERVICE_UNAVAILABLE
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
242
352
|
const parsed = hostRegisterSystemSchema.safeParse(payload);
|
|
243
353
|
if (!parsed.success) {
|
|
244
354
|
callback({
|
|
@@ -287,6 +397,14 @@ io.on(
|
|
|
287
397
|
socket.on(
|
|
288
398
|
"host:createRoom",
|
|
289
399
|
async (payload, callback) => {
|
|
400
|
+
if (isRateLimited("host-registration", HOST_REGISTRATION_RATE_LIMIT_MAX)) {
|
|
401
|
+
callback({
|
|
402
|
+
ok: false,
|
|
403
|
+
message: "Too many host registration attempts. Please try again.",
|
|
404
|
+
code: ErrorCode.SERVICE_UNAVAILABLE
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
290
408
|
const parsed = hostCreateRoomSchema.safeParse(payload);
|
|
291
409
|
if (!parsed.success) {
|
|
292
410
|
callback({
|
|
@@ -297,16 +415,14 @@ io.on(
|
|
|
297
415
|
return;
|
|
298
416
|
}
|
|
299
417
|
const { maxPlayers, apiKey } = parsed.data;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
418
|
+
const verification = await authService.verifyApiKey(apiKey);
|
|
419
|
+
if (!verification.isVerified) {
|
|
420
|
+
callback({
|
|
421
|
+
ok: false,
|
|
422
|
+
message: verification.error,
|
|
423
|
+
code: ErrorCode.INVALID_API_KEY
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
310
426
|
}
|
|
311
427
|
const existingRoomId = roomManager.getRoomByHostId(socket.id);
|
|
312
428
|
if (existingRoomId) {
|
|
@@ -352,6 +468,14 @@ io.on(
|
|
|
352
468
|
socket.on(
|
|
353
469
|
"host:reconnect",
|
|
354
470
|
async (payload, callback) => {
|
|
471
|
+
if (isRateLimited("host-registration", HOST_REGISTRATION_RATE_LIMIT_MAX)) {
|
|
472
|
+
callback({
|
|
473
|
+
ok: false,
|
|
474
|
+
message: "Too many host registration attempts. Please try again.",
|
|
475
|
+
code: ErrorCode.SERVICE_UNAVAILABLE
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
355
479
|
const parsed = hostReconnectSchema.safeParse(payload);
|
|
356
480
|
if (!parsed.success) {
|
|
357
481
|
callback({
|
|
@@ -362,16 +486,14 @@ io.on(
|
|
|
362
486
|
return;
|
|
363
487
|
}
|
|
364
488
|
const { roomId, apiKey } = parsed.data;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
489
|
+
const verification = await authService.verifyApiKey(apiKey);
|
|
490
|
+
if (!verification.isVerified) {
|
|
491
|
+
callback({
|
|
492
|
+
ok: false,
|
|
493
|
+
message: verification.error,
|
|
494
|
+
code: ErrorCode.INVALID_API_KEY
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
375
497
|
}
|
|
376
498
|
const session = roomManager.getRoom(roomId);
|
|
377
499
|
if (!session) {
|
|
@@ -553,6 +675,14 @@ io.on(
|
|
|
553
675
|
socket.on(
|
|
554
676
|
"host:register",
|
|
555
677
|
async (payload, callback) => {
|
|
678
|
+
if (isRateLimited("host-registration", HOST_REGISTRATION_RATE_LIMIT_MAX)) {
|
|
679
|
+
callback({
|
|
680
|
+
ok: false,
|
|
681
|
+
message: "Too many host registration attempts. Please try again.",
|
|
682
|
+
code: ErrorCode.SERVICE_UNAVAILABLE
|
|
683
|
+
});
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
556
686
|
const parsed = hostRegistrationSchema.safeParse(payload);
|
|
557
687
|
if (!parsed.success) {
|
|
558
688
|
callback({
|
|
@@ -562,7 +692,16 @@ io.on(
|
|
|
562
692
|
});
|
|
563
693
|
return;
|
|
564
694
|
}
|
|
565
|
-
const { roomId, maxPlayers } = parsed.data;
|
|
695
|
+
const { roomId, maxPlayers, apiKey } = parsed.data;
|
|
696
|
+
const verification = await authService.verifyApiKey(apiKey);
|
|
697
|
+
if (!verification.isVerified) {
|
|
698
|
+
callback({
|
|
699
|
+
ok: false,
|
|
700
|
+
message: verification.error,
|
|
701
|
+
code: ErrorCode.INVALID_API_KEY
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
566
705
|
let session = roomManager.getRoom(roomId);
|
|
567
706
|
if (session) {
|
|
568
707
|
session.masterHostSocketId = socket.id;
|
|
@@ -586,6 +725,14 @@ io.on(
|
|
|
586
725
|
}
|
|
587
726
|
);
|
|
588
727
|
socket.on("controller:join", (payload, callback) => {
|
|
728
|
+
if (isRateLimited("controller-join", CONTROLLER_JOIN_RATE_LIMIT_MAX)) {
|
|
729
|
+
callback({
|
|
730
|
+
ok: false,
|
|
731
|
+
message: "Too many join attempts. Please try again.",
|
|
732
|
+
code: ErrorCode.SERVICE_UNAVAILABLE
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
589
736
|
const parsed = controllerJoinSchema.safeParse(payload);
|
|
590
737
|
if (!parsed.success) {
|
|
591
738
|
callback({
|
|
@@ -704,6 +851,9 @@ io.on(
|
|
|
704
851
|
return;
|
|
705
852
|
}
|
|
706
853
|
const { roomId, controllerId } = parsed.data;
|
|
854
|
+
if (!isControllerAuthorizedForRoom(roomId, controllerId)) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
707
857
|
const session = roomManager.getRoom(roomId);
|
|
708
858
|
if (!session) {
|
|
709
859
|
return;
|
|
@@ -731,7 +881,10 @@ io.on(
|
|
|
731
881
|
}
|
|
732
882
|
return;
|
|
733
883
|
}
|
|
734
|
-
const { roomId } = result.data;
|
|
884
|
+
const { roomId, controllerId } = result.data;
|
|
885
|
+
if (!isControllerAuthorizedForRoom(roomId, controllerId)) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
735
888
|
const session = roomManager.getRoom(roomId);
|
|
736
889
|
if (!session) {
|
|
737
890
|
if (!lastServerInputFailLogTime || now - lastServerInputFailLogTime > 1e3) {
|
|
@@ -757,6 +910,9 @@ io.on(
|
|
|
757
910
|
return;
|
|
758
911
|
}
|
|
759
912
|
const { roomId, command } = parsed.data;
|
|
913
|
+
if (!isControllerAuthorizedForRoom(roomId)) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
760
916
|
const session = roomManager.getRoom(roomId);
|
|
761
917
|
if (!session) {
|
|
762
918
|
return;
|
|
@@ -795,6 +951,9 @@ io.on(
|
|
|
795
951
|
return;
|
|
796
952
|
}
|
|
797
953
|
const { roomId, command } = parsed.data;
|
|
954
|
+
if (!isHostAuthorizedForRoom(roomId)) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
798
957
|
const session = roomManager.getRoom(roomId);
|
|
799
958
|
if (!session) {
|
|
800
959
|
return;
|
|
@@ -814,6 +973,9 @@ io.on(
|
|
|
814
973
|
const result = controllerStateSchema.safeParse(payload);
|
|
815
974
|
if (!result.success) return;
|
|
816
975
|
const { roomId, state } = result.data;
|
|
976
|
+
if (!isHostAuthorizedForRoom(roomId)) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
817
979
|
const session = roomManager.getRoom(roomId);
|
|
818
980
|
if (session) {
|
|
819
981
|
if (state.gameState) {
|
|
@@ -860,6 +1022,9 @@ io.on(
|
|
|
860
1022
|
});
|
|
861
1023
|
socket.on("controller:play_sound", (payload) => {
|
|
862
1024
|
const { roomId, soundId, volume, loop } = payload;
|
|
1025
|
+
if (!isControllerAuthorizedForRoom(roomId)) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
863
1028
|
const session = roomManager.getRoom(roomId);
|
|
864
1029
|
if (!session) return;
|
|
865
1030
|
io.to(roomManager.getActiveHostId(session)).emit("server:playSound", {
|
|
@@ -973,4 +1138,4 @@ io.on(
|
|
|
973
1138
|
httpServer.listen(PORT, () => {
|
|
974
1139
|
console.log(`[air-jam] server listening on http://localhost:${PORT}`);
|
|
975
1140
|
});
|
|
976
|
-
//# sourceMappingURL=chunk-
|
|
1141
|
+
//# sourceMappingURL=chunk-CGONPUNG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/services/auth-service.ts","../src/db.ts","../src/services/rate-limit-service.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 { rateLimitService } from \"./services/rate-limit-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 parsePositiveInt = (\n value: string | undefined,\n fallback: number,\n): number => {\n const parsed = Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;\n};\n\nconst PORT = Number(process.env.PORT ?? 4000);\nconst RATE_LIMIT_WINDOW_MS = parsePositiveInt(\n process.env.AIR_JAM_RATE_LIMIT_WINDOW_MS,\n 60_000,\n);\nconst HOST_REGISTRATION_RATE_LIMIT_MAX = parsePositiveInt(\n process.env.AIR_JAM_HOST_REGISTRATION_RATE_LIMIT_MAX,\n 30,\n);\nconst CONTROLLER_JOIN_RATE_LIMIT_MAX = parsePositiveInt(\n process.env.AIR_JAM_CONTROLLER_JOIN_RATE_LIMIT_MAX,\n 120,\n);\nconst allowedOrigins = process.env.AIR_JAM_ALLOWED_ORIGINS?.split(\",\")\n .map((origin) => origin.trim())\n .filter(Boolean);\nconst corsOrigin =\n allowedOrigins && allowedOrigins.length > 0 ? allowedOrigins : \"*\";\n\nconst app = express();\napp.use(cors({ origin: corsOrigin }));\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: corsOrigin,\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 const isHostAuthorizedForRoom = (roomId: string): boolean => {\n return roomManager.getRoomByHostId(socket.id) === roomId;\n };\n\n const isControllerAuthorizedForRoom = (\n roomId: string,\n controllerId?: string,\n ): boolean => {\n const controllerInfo = roomManager.getControllerInfo(socket.id);\n if (!controllerInfo || controllerInfo.roomId !== roomId) {\n return false;\n }\n if (controllerId && controllerInfo.controllerId !== controllerId) {\n return false;\n }\n return true;\n };\n\n const forwardedFor = socket.handshake.headers[\"x-forwarded-for\"];\n const socketIdentifier =\n (typeof forwardedFor === \"string\" &&\n forwardedFor.split(\",\")[0]?.trim()) ||\n (Array.isArray(forwardedFor) && forwardedFor[0]?.split(\",\")[0]?.trim()) ||\n socket.handshake.address ||\n socket.id;\n\n const isRateLimited = (bucket: string, limit: number): boolean => {\n const result = rateLimitService.check(\n `${bucket}:${socketIdentifier}`,\n limit,\n RATE_LIMIT_WINDOW_MS,\n );\n return !result.allowed;\n };\n\n // --- SYSTEM HOST REGISTRATION (Arcade) ---\n socket.on(\n \"host:registerSystem\",\n async (payload: HostRegisterSystemPayload, callback) => {\n if (\n isRateLimited(\"host-registration\", HOST_REGISTRATION_RATE_LIMIT_MAX)\n ) {\n callback({\n ok: false,\n message: \"Too many host registration attempts. Please try again.\",\n code: ErrorCode.SERVICE_UNAVAILABLE,\n });\n return;\n }\n\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 if (\n isRateLimited(\"host-registration\", HOST_REGISTRATION_RATE_LIMIT_MAX)\n ) {\n callback({\n ok: false,\n message: \"Too many host registration attempts. Please try again.\",\n code: ErrorCode.SERVICE_UNAVAILABLE,\n });\n return;\n }\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 is always executed; in local/dev mode auth is disabled.\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 // 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 if (\n isRateLimited(\"host-registration\", HOST_REGISTRATION_RATE_LIMIT_MAX)\n ) {\n callback({\n ok: false,\n message: \"Too many host registration attempts. Please try again.\",\n code: ErrorCode.SERVICE_UNAVAILABLE,\n });\n return;\n }\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 is always executed; in local/dev mode auth is disabled.\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 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 if (\n isRateLimited(\"host-registration\", HOST_REGISTRATION_RATE_LIMIT_MAX)\n ) {\n callback({\n ok: false,\n message: \"Too many host registration attempts. Please try again.\",\n code: ErrorCode.SERVICE_UNAVAILABLE,\n });\n return;\n }\n\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, apiKey } = parsed.data;\n\n // API key validation is always executed; in local/dev mode auth is disabled.\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 // 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 if (isRateLimited(\"controller-join\", CONTROLLER_JOIN_RATE_LIMIT_MAX)) {\n callback({\n ok: false,\n message: \"Too many join attempts. Please try again.\",\n code: ErrorCode.SERVICE_UNAVAILABLE,\n });\n return;\n }\n\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\n // Prevent forged leave events from other sockets.\n if (!isControllerAuthorizedForRoom(roomId, controllerId)) {\n return;\n }\n\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, controllerId } = result.data;\n\n // Only the socket that joined this controller can send its input.\n if (!isControllerAuthorizedForRoom(roomId, controllerId)) {\n return;\n }\n\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\n // Only joined controllers from this room can trigger system commands.\n if (!isControllerAuthorizedForRoom(roomId)) {\n return;\n }\n\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\n // Only host sockets for this room can mutate host-controlled system state.\n if (!isHostAuthorizedForRoom(roomId)) {\n return;\n }\n\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 if (!isHostAuthorizedForRoom(roomId)) {\n return;\n }\n\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\n if (!isControllerAuthorizedForRoom(roomId)) {\n return;\n }\n\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\ntype AuthMode = \"disabled\" | \"required\";\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 local/dev mode, allows all connections by default.\n * In production, defaults to required auth (fail-closed).\n */\nexport class AuthService {\n private masterKey: string | undefined;\n private databaseUrl: string | undefined;\n private authMode: AuthMode;\n\n constructor() {\n this.masterKey = process.env.AIR_JAM_MASTER_KEY;\n this.databaseUrl = process.env.DATABASE_URL;\n this.authMode = this.resolveAuthMode();\n\n if (this.authMode === \"disabled\") {\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 } else {\n console.log(\n \"[server] Authentication required, but no auth backend is configured (set AIR_JAM_MASTER_KEY or DATABASE_URL)\",\n );\n }\n }\n\n /**\n * Verify an API key\n * Returns verification result with optional error message\n * In local/dev mode, always returns success\n */\n async verifyApiKey(apiKey?: string): Promise<VerificationResult> {\n // Local/dev mode: no auth required\n if (this.authMode === \"disabled\") {\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 private resolveAuthMode(): AuthMode {\n const configuredMode = process.env.AIR_JAM_AUTH_MODE?.toLowerCase();\n\n if (configuredMode === \"disabled\") {\n return \"disabled\";\n }\n\n if (configuredMode === \"required\") {\n return \"required\";\n }\n\n // Auto mode (default):\n // - Require auth whenever credentials are configured.\n // - Fail closed in production even if credentials are missing.\n // - Keep local development friction-free.\n if (this.masterKey || this.databaseUrl) {\n return \"required\";\n }\n\n if (process.env.NODE_ENV === \"production\") {\n return \"required\";\n }\n\n return \"disabled\";\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","interface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\nexport interface RateLimitResult {\n allowed: boolean;\n retryAfterMs: number;\n}\n\n/**\n * Minimal in-memory fixed-window limiter.\n * Good enough for single-instance deployments and lightweight abuse protection.\n */\nexport class RateLimitService {\n private entries = new Map<string, RateLimitEntry>();\n private checks = 0;\n\n check(key: string, limit: number, windowMs: number): RateLimitResult {\n if (limit <= 0 || windowMs <= 0) {\n return { allowed: true, retryAfterMs: 0 };\n }\n\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || now >= existing.resetAt) {\n this.entries.set(key, { count: 1, resetAt: now + windowMs });\n this.maybeCleanup(now);\n return { allowed: true, retryAfterMs: 0 };\n }\n\n if (existing.count >= limit) {\n this.maybeCleanup(now);\n return {\n allowed: false,\n retryAfterMs: Math.max(existing.resetAt - now, 0),\n };\n }\n\n existing.count += 1;\n this.entries.set(key, existing);\n this.maybeCleanup(now);\n return { allowed: true, retryAfterMs: 0 };\n }\n\n private maybeCleanup(now: number): void {\n this.checks += 1;\n if (this.checks % 200 !== 0) {\n return;\n }\n\n for (const [key, entry] of this.entries) {\n if (now >= entry.resetAt) {\n this.entries.delete(key);\n }\n }\n }\n}\n\nexport const rateLimitService = new RateLimitService();\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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-CGONPUNG.js";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import dotenv from "dotenv";
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
7
6
|
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
8
|
var __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
var __dirname = dirname(__filename);
|
|
10
10
|
dotenv.config({ path: join(__dirname, "..", ".env") });
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entry point for @air-jam/server\n * Handles environment variable loading and starts the server\n */\n\nimport dotenv from \"dotenv\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * CLI entry point for @air-jam/server\n * Handles environment variable loading and starts the server\n */\n\nimport dotenv from \"dotenv\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n// Load .env file if it exists\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\ndotenv.config({ path: join(__dirname, \"..\", \".env\") });\n\n// Import and start the server\nimport \"./index.js\";\n"],"mappings":";;;;AAOA,OAAO,YAAY;AACnB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAG9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AACpC,OAAO,OAAO,EAAE,MAAM,KAAK,WAAW,MAAM,MAAM,EAAE,CAAC;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-CGONPUNG.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.4",
|
|
4
4
|
"description": "Air Jam game server for local development and production",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"directory": "packages/server"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/vucinatim/air-jam#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/vucinatim/air-jam/issues"
|
|
16
|
+
},
|
|
14
17
|
"publishConfig": {
|
|
15
18
|
"access": "public"
|
|
16
19
|
},
|
|
@@ -21,15 +24,8 @@
|
|
|
21
24
|
"files": [
|
|
22
25
|
"dist"
|
|
23
26
|
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"dev": "tsx src/index.ts",
|
|
26
|
-
"start": "node dist/cli.js",
|
|
27
|
-
"build": "tsup",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"prepublishOnly": "node scripts/prepublish.js && pnpm build"
|
|
30
|
-
},
|
|
31
27
|
"dependencies": {
|
|
32
|
-
"@air-jam/sdk": "^0.1.
|
|
28
|
+
"@air-jam/sdk": "^0.1.4",
|
|
33
29
|
"color": "^5.0.3",
|
|
34
30
|
"cors": "^2.8.5",
|
|
35
31
|
"dotenv": "^17.2.3",
|
|
@@ -48,5 +44,11 @@
|
|
|
48
44
|
"tsup": "^8.5.1",
|
|
49
45
|
"tsx": "^4.19.2",
|
|
50
46
|
"typescript": "~5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"dev": "tsx src/index.ts",
|
|
50
|
+
"start": "node dist/cli.js",
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
51
53
|
}
|
|
52
|
-
}
|
|
54
|
+
}
|