@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.
@@ -46,12 +46,12 @@ var db = client ? drizzle(client) : null;
46
46
  var AuthService = class {
47
47
  masterKey;
48
48
  databaseUrl;
49
- isDevMode;
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.isDevMode = !this.masterKey && !this.databaseUrl;
54
- if (this.isDevMode) {
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.isDevMode) {
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
- 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
- }
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
- 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
- }
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-T6BT7WE5.js.map
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":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAGA;AAAA,OAuBK;AACP,OAAO,WAAW;AAClB,OAAO,UAAU;AACjB,OAAO,aAAa;AACpB,SAAS,oBAAoB;AAC7B,SAAS,cAA2B;AACpC,SAAS,MAAM,cAAc;;;AC3C7B,SAAS,KAAK,UAAU;;;ACAxB,YAAY,YAAY;AACxB,SAAS,SAAS,SAAS,MAAM,iBAAiB;AAClD,SAAS,eAAe;AACxB,OAAO,cAAc;AAEd,cAAO;AAGP,IAAM,UAAU,QAAQ,YAAY;AAAA,EACzC,IAAI,KAAK,IAAI,EAAE,WAAW;AAAA,EAC1B,QAAQ,KAAK,SAAS,EAAE,QAAQ,EAAE,OAAO;AAAA;AAAA,EACzC,KAAK,KAAK,KAAK,EAAE,QAAQ,EAAE,OAAO;AAAA,EAClC,UAAU,QAAQ,WAAW,EAAE,QAAQ,IAAI,EAAE,QAAQ;AAAA,EACrD,WAAW,UAAU,YAAY,EAAE,WAAW,EAAE,QAAQ;AAAA,EACxD,YAAY,UAAU,cAAc;AACtC,CAAC;AAED,IAAM,mBAAmB,QAAQ,IAAI;AAIrC,IAAM,SAAS,mBAAmB,SAAS,gBAAgB,IAAI;AACxD,IAAM,KAAK,SAAS,QAAQ,MAAM,IAAI;;;ADHtC,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,QAAQ,IAAI;AAC7B,SAAK,cAAc,QAAQ,IAAI;AAC/B,SAAK,WAAW,KAAK,gBAAgB;AAErC,QAAI,KAAK,aAAa,YAAY;AAChC,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF,WAAW,KAAK,aAAa,CAAC,KAAK,aAAa;AAC9C,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF,WAAW,KAAK,aAAa;AAC3B,cAAQ,IAAI,+CAA+C;AAAA,IAC7D,OAAO;AACL,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,QAA8C;AAE/D,QAAI,KAAK,aAAa,YAAY;AAChC,aAAO,EAAE,YAAY,KAAK;AAAA,IAC5B;AAEA,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,QACL,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF;AAGA,QAAI,KAAK,aAAa,WAAW,KAAK,WAAW;AAC/C,aAAO,EAAE,YAAY,KAAK;AAAA,IAC5B;AAGA,QAAI,CAAC,KAAK,eAAe,CAAC,IAAI;AAC5B,aAAO;AAAA,QACL,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF;AAEA,QAAI;AACF,YAAM,CAAC,SAAS,IAAI,MAAM,GACvB,OAAO,EACP,KAAK,OAAO,EACZ,MAAM,IAAI,GAAG,QAAQ,KAAK,MAAM,GAAG,GAAG,QAAQ,UAAU,IAAI,CAAC,CAAC,EAC9D,MAAM,CAAC;AAEV,UAAI,WAAW;AAEb,WAAG,OAAO,OAAO,EACd,IAAI,EAAE,YAAY,oBAAI,KAAK,EAAE,CAAC,EAC9B,MAAM,GAAG,QAAQ,IAAI,UAAU,EAAE,CAAC,EAClC;AAAA,UAAM,CAAC,QACN,QAAQ,MAAM,wCAAwC,GAAG;AAAA,QAC3D;AAEF,eAAO,EAAE,YAAY,KAAK;AAAA,MAC5B;AAEA,aAAO;AAAA,QACL,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,mDAAmD,KAAK;AACtE,aAAO;AAAA,QACL,YAAY;AAAA,QACZ,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,kBAA4B;AAClC,UAAM,iBAAiB,QAAQ,IAAI,mBAAmB,YAAY;AAElE,QAAI,mBAAmB,YAAY;AACjC,aAAO;AAAA,IACT;AAEA,QAAI,mBAAmB,YAAY;AACjC,aAAO;AAAA,IACT;AAMA,QAAI,KAAK,aAAa,KAAK,aAAa;AACtC,aAAO;AAAA,IACT;AAEA,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AACF;AAKO,IAAM,cAAc,IAAI,YAAY;;;AE7HpC,IAAM,mBAAN,MAAuB;AAAA,EACpB,UAAU,oBAAI,IAA4B;AAAA,EAC1C,SAAS;AAAA,EAEjB,MAAM,KAAa,OAAe,UAAmC;AACnE,QAAI,SAAS,KAAK,YAAY,GAAG;AAC/B,aAAO,EAAE,SAAS,MAAM,cAAc,EAAE;AAAA,IAC1C;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,KAAK,QAAQ,IAAI,GAAG;AAErC,QAAI,CAAC,YAAY,OAAO,SAAS,SAAS;AACxC,WAAK,QAAQ,IAAI,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,SAAS,CAAC;AAC3D,WAAK,aAAa,GAAG;AACrB,aAAO,EAAE,SAAS,MAAM,cAAc,EAAE;AAAA,IAC1C;AAEA,QAAI,SAAS,SAAS,OAAO;AAC3B,WAAK,aAAa,GAAG;AACrB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,cAAc,KAAK,IAAI,SAAS,UAAU,KAAK,CAAC;AAAA,MAClD;AAAA,IACF;AAEA,aAAS,SAAS;AAClB,SAAK,QAAQ,IAAI,KAAK,QAAQ;AAC9B,SAAK,aAAa,GAAG;AACrB,WAAO,EAAE,SAAS,MAAM,cAAc,EAAE;AAAA,EAC1C;AAAA,EAEQ,aAAa,KAAmB;AACtC,SAAK,UAAU;AACf,QAAI,KAAK,SAAS,QAAQ,GAAG;AAC3B;AAAA,IACF;AAEA,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,SAAS;AACvC,UAAI,OAAO,MAAM,SAAS;AACxB,aAAK,QAAQ,OAAO,GAAG;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,mBAAmB,IAAI,iBAAiB;;;ACpD9C,IAAM,cAAN,MAAkB;AAAA,EACf,QAAQ,oBAAI,IAA2B;AAAA,EACvC,YAAY,oBAAI,IAAsB;AAAA,EACtC,kBAAkB,oBAAI,IAAkC;AAAA;AAAA;AAAA;AAAA,EAKhE,QAAQ,QAA2C;AACjD,WAAO,KAAK,MAAM,IAAI,MAAM;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,QAAkB,SAA4B;AACpD,SAAK,MAAM,IAAI,QAAQ,OAAO;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,QAAwB;AACjC,SAAK,MAAM,OAAO,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,UAAwC;AACtD,WAAO,KAAK,UAAU,IAAI,QAAQ;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,UAAkB,QAAwB;AACpD,SAAK,UAAU,IAAI,UAAU,MAAM;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,UAAwB;AACjC,SAAK,UAAU,OAAO,QAAQ;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAkB,UAAoD;AACpE,WAAO,KAAK,gBAAgB,IAAI,QAAQ;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,UAAkB,OAAmC;AACjE,SAAK,gBAAgB,IAAI,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,UAAwB;AACvC,SAAK,gBAAgB,OAAO,QAAQ;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,SAA8B;AAC5C,WAAO,QAAQ,UAAU,UAAU,QAAQ,oBACvC,QAAQ,oBACR,QAAQ;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,WAAW,QAAkBA,KAAY,QAAsB;AAC7D,UAAM,UAAU,KAAK,MAAM,IAAI,MAAM;AACrC,QAAI,CAAC,QAAS;AAGd,IAAAA,IAAG,GAAG,MAAM,EAAE,KAAK,mBAAmB,EAAE,QAAQ,OAAO,CAAC;AAGxD,YAAQ,YAAY,QAAQ,CAAC,eAAe;AAC1C,WAAK,gBAAgB,OAAO,WAAW,QAAQ;AAAA,IACjD,CAAC;AAGD,SAAK,UAAU,OAAO,QAAQ,kBAAkB;AAChD,QAAI,QAAQ,mBAAmB;AAC7B,WAAK,UAAU,OAAO,QAAQ,iBAAiB;AAAA,IACjD;AAGA,SAAK,MAAM,OAAO,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,cAA0C;AACxC,WAAO,KAAK;AAAA,EACd;AACF;AAKO,IAAM,cAAc,IAAI,YAAY;;;ACzH3C,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB;AAE1B,IAAM,WAAW;AAMV,IAAM,mBAAmB,MAAc;AAC5C,QAAM,OAAO,MAAM;AAAA,IACjB,EAAE,QAAQ,EAAE;AAAA,IACZ,MAAM,SAAS,UAAU,GAAG,SAAS,MAAM,CAAC;AAAA,EAC9C,EAAE,KAAK,EAAE;AAET,SAAO,eAAe,MAAM,IAAI;AAClC;;;ALmCA,IAAI,yBAAyB;AAC7B,IAAI,6BAA6B;AAEjC,IAAM,mBAAmB,CACvB,OACA,aACW;AACX,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,UAAU,MAAM,KAAK,SAAS,IAAI,SAAS;AAC3D;AAEA,IAAM,OAAO,OAAO,QAAQ,IAAI,QAAQ,GAAI;AAC5C,IAAM,uBAAuB;AAAA,EAC3B,QAAQ,IAAI;AAAA,EACZ;AACF;AACA,IAAM,mCAAmC;AAAA,EACvC,QAAQ,IAAI;AAAA,EACZ;AACF;AACA,IAAM,iCAAiC;AAAA,EACrC,QAAQ,IAAI;AAAA,EACZ;AACF;AACA,IAAM,iBAAiB,QAAQ,IAAI,yBAAyB,MAAM,GAAG,EAClE,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC,EAC7B,OAAO,OAAO;AACjB,IAAM,aACJ,kBAAkB,eAAe,SAAS,IAAI,iBAAiB;AAEjE,IAAM,MAAM,QAAQ;AACpB,IAAI,IAAI,KAAK,EAAE,QAAQ,WAAW,CAAC,CAAC;AACpC,IAAI,IAAI,QAAQ,KAAK,CAAC;AAEtB,IAAI,IAAI,WAAW,CAAC,GAAG,QAAQ;AAC7B,MAAI,KAAK,EAAE,IAAI,KAAK,CAAC;AACvB,CAAC;AAED,IAAM,aAAa,aAAa,GAAG;AAEnC,IAAM,KAAK,IAAI,OAKb,YAAY;AAAA,EACZ,MAAM;AAAA,IACJ,QAAQ;AAAA,EACV;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AACf,CAAC;AAED,IAAM,YAAY,CAAC,UAAkB,YAAsC;AACzE,KAAG,GAAG,QAAQ,EAAE,KAAK,gBAAgB,OAAO;AAC9C;AAEA,GAAG;AAAA,EACD;AAAA,EACA,CACE,WAMG;AACH,UAAM,0BAA0B,CAAC,WAA4B;AAC3D,aAAO,YAAY,gBAAgB,OAAO,EAAE,MAAM;AAAA,IACpD;AAEA,UAAM,gCAAgC,CACpC,QACA,iBACY;AACZ,YAAM,iBAAiB,YAAY,kBAAkB,OAAO,EAAE;AAC9D,UAAI,CAAC,kBAAkB,eAAe,WAAW,QAAQ;AACvD,eAAO;AAAA,MACT;AACA,UAAI,gBAAgB,eAAe,iBAAiB,cAAc;AAChE,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAEA,UAAM,eAAe,OAAO,UAAU,QAAQ,iBAAiB;AAC/D,UAAM,mBACH,OAAO,iBAAiB,YACvB,aAAa,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAClC,MAAM,QAAQ,YAAY,KAAK,aAAa,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KACrE,OAAO,UAAU,WACjB,OAAO;AAET,UAAM,gBAAgB,CAAC,QAAgB,UAA2B;AAChE,YAAM,SAAS,iBAAiB;AAAA,QAC9B,GAAG,MAAM,IAAI,gBAAgB;AAAA,QAC7B;AAAA,QACA;AAAA,MACF;AACA,aAAO,CAAC,OAAO;AAAA,IACjB;AAGA,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAoC,aAAa;AACtD,YACE,cAAc,qBAAqB,gCAAgC,GACnE;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,SAAS,yBAAyB,UAAU,OAAO;AACzD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,EAAE,QAAQ,OAAO,IAAI,OAAO;AAGlC,cAAM,eAAe,MAAM,YAAY,aAAa,MAAM;AAC1D,YAAI,CAAC,aAAa,YAAY;AAC5B,kBAAQ;AAAA,YACN,4DAA4D,MAAM;AAAA,UACpE;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,aAAa;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,YAAI,UAAU,YAAY,QAAQ,MAAM;AAExC,YAAI,SAAS;AAEX,kBAAQ,qBAAqB,OAAO;AACpC,sBAAY,QAAQ,QAAQ,OAAO;AAAA,QACrC,OAAO;AAEL,kBAAQ,IAAI,0BAA0B,MAAM,EAAE;AAC9C,oBAAU;AAAA,YACR;AAAA,YACA,oBAAoB,OAAO;AAAA,YAC3B,OAAO;AAAA,YACP,aAAa,oBAAI,IAAI;AAAA,YACrB,YAAY;AAAA;AAAA,YACZ,WAAW;AAAA,UACb;AACA,sBAAY,QAAQ,QAAQ,OAAO;AAAA,QACrC;AAEA,oBAAY,YAAY,OAAO,IAAI,MAAM;AACzC,eAAO,KAAK,MAAM;AAElB,iBAAS,EAAE,IAAI,MAAM,OAAO,CAAC;AAC7B,WAAG,GAAG,MAAM,EAAE,KAAK,oBAAoB,EAAE,OAAO,CAAC;AAAA,MACnD;AAAA,IACF;AAGA,WAAO;AAAA,MACL;AAAA,MACA,OACE,SACA,aAMG;AACH,YACE,cAAc,qBAAqB,gCAAgC,GACnE;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,SAAS,qBAAqB,UAAU,OAAO;AACrD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,EAAE,YAAY,OAAO,IAAI,OAAO;AAGtC,cAAM,eAAe,MAAM,YAAY,aAAa,MAAM;AAC1D,YAAI,CAAC,aAAa,YAAY;AAC5B,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,aAAa;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAGA,cAAM,iBAAiB,YAAY,gBAAgB,OAAO,EAAE;AAC5D,YAAI,gBAAgB;AAClB,gBAAM,kBAAkB,YAAY,QAAQ,cAAc;AAE1D,cACE,mBACA,gBAAgB,uBAAuB,OAAO,IAC9C;AACA,oBAAQ;AAAA,cACN,iBAAiB,OAAO,EAAE,qBAAqB,cAAc;AAAA,YAC/D;AACA,qBAAS,EAAE,IAAI,MAAM,QAAQ,eAAe,CAAC;AAC7C;AAAA,UACF;AAAA,QACF;AAGA,YAAI;AACJ,YAAI,WAAW;AACf,WAAG;AACD,mBAAS,iBAAiB;AAC1B;AACA,cAAI,WAAW,IAAI;AACjB,qBAAS;AAAA,cACP,IAAI;AAAA,cACJ,SAAS;AAAA,cACT,MAAM,UAAU;AAAA,YAClB,CAAC;AACD;AAAA,UACF;AAAA,QACF,SAAS,YAAY,QAAQ,MAAM;AAGnC,cAAM,UAAuB;AAAA,UAC3B;AAAA,UACA,oBAAoB,OAAO;AAAA,UAC3B,OAAO;AAAA,UACP,aAAa,oBAAI,IAAI;AAAA,UACrB,YAAY,cAAc;AAAA,UAC1B,WAAW;AAAA,QACb;AAEA,oBAAY,QAAQ,QAAQ,OAAO;AACnC,oBAAY,YAAY,OAAO,IAAI,MAAM;AACzC,eAAO,KAAK,MAAM;AAElB,gBAAQ,IAAI,yBAAyB,MAAM,aAAa,OAAO,EAAE,EAAE;AACnE,iBAAS,EAAE,IAAI,MAAM,OAAO,CAAC;AAC7B,WAAG,GAAG,MAAM,EAAE,KAAK,oBAAoB,EAAE,OAAO,CAAC;AAAA,MACnD;AAAA,IACF;AAGA,WAAO;AAAA,MACL;AAAA,MACA,OACE,SACA,aAMG;AACH,YACE,cAAc,qBAAqB,gCAAgC,GACnE;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,SAAS,oBAAoB,UAAU,OAAO;AACpD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,EAAE,QAAQ,OAAO,IAAI,OAAO;AAGlC,cAAM,eAAe,MAAM,YAAY,aAAa,MAAM;AAC1D,YAAI,CAAC,aAAa,YAAY;AAC5B,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,aAAa;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,YAAI,CAAC,SAAS;AACZ,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAGA,cAAM,uBAAuB,GAAG,QAAQ,QAAQ;AAAA,UAC9C,QAAQ;AAAA,QACV;AACA,cAAM,0BACJ,sBAAsB,aAAa;AAKrC,YACE,CAAC,2BACD,QAAQ,uBAAuB,OAAO,IACtC;AACA,kBAAQ,qBAAqB,OAAO;AACpC,sBAAY,QAAQ,QAAQ,OAAO;AACnC,sBAAY,YAAY,OAAO,IAAI,MAAM;AACzC,iBAAO,KAAK,MAAM;AAElB,kBAAQ;AAAA,YACN,iBAAiB,OAAO,EAAE,wBAAwB,MAAM;AAAA,UAC1D;AACA,mBAAS,EAAE,IAAI,MAAM,OAAO,CAAC;AAC7B,aAAG,GAAG,MAAM,EAAE,KAAK,oBAAoB,EAAE,OAAO,CAAC;AAAA,QACnD,OAAO;AACL,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAkC,aAAa;AAC9C,cAAM,SAAS,uBAAuB,UAAU,OAAO;AACvD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,EAAE,QAAQ,QAAQ,IAAI,OAAO;AACnC,cAAM,UAAU,YAAY,QAAQ,MAAM;AAE1C,YAAI,CAAC,SAAS;AACZ,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,YAAI,QAAQ,uBAAuB,OAAO,IAAI;AAC5C,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAGA,YAAI,QAAQ,mBAAmB;AAC7B,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAGA,YAAI,QAAQ,aAAa,CAAC,QAAQ,mBAAmB;AACnD,mBAAS,EAAE,IAAI,MAAM,WAAW,QAAQ,UAAU,CAAC;AACnD;AAAA,QACF;AAGA,cAAM,YAAY,OAAO;AACzB,gBAAQ,YAAY;AACpB,gBAAQ,sBAAsB;AAE9B,gBAAQ,IAAI,mCAAmC,MAAM,EAAE;AAGvD,WAAG,GAAG,MAAM,EAAE,KAAK,iBAAiB,EAAE,KAAK,QAAQ,CAAC;AAEpD,iBAAS,EAAE,IAAI,MAAM,UAAU,CAAC;AAAA,MAClC;AAAA,IACF;AAGA,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAiC,aAAa;AAC7C,cAAM,SAAS,sBAAsB,UAAU,OAAO;AACtD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,EAAE,QAAQ,UAAU,IAAI,OAAO;AACrC,cAAM,UAAU,YAAY,QAAQ,MAAM;AAE1C,YAAI,CAAC,SAAS;AACZ,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,YAAI,QAAQ,cAAc,WAAW;AACnC,kBAAQ;AAAA,YACN,wCAAwC,MAAM,cAAc,QAAQ,SAAS,SAAS,SAAS;AAAA,UACjG;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,gBAAQ,IAAI,kCAAkC,MAAM,EAAE;AACtD,gBAAQ,oBAAoB,OAAO;AACnC,gBAAQ,QAAQ;AAEhB,oBAAY,YAAY,OAAO,IAAI,MAAM;AACzC,eAAO,KAAK,MAAM;AAKlB,mBAAW,MAAM;AACf,kBAAQ,YAAY,QAAQ,CAAC,MAAM;AACjC,kBAAM,SAAiC;AAAA,cACrC,cAAc,EAAE;AAAA,cAChB,UAAU,EAAE;AAAA,cACZ,QAAQ,EAAE;AAAA,YACZ;AACA,mBAAO,KAAK,2BAA2B,MAAM;AAAA,UAC/C,CAAC;AAGD,gBAAM,eAAe;AAAA,YACnB;AAAA,YACA,OAAO;AAAA,cACL,WAAW,QAAQ;AAAA,YACrB;AAAA,UACF;AACA,iBAAO,KAAK,gBAAgB,YAAY;AAAA,QAC1C,GAAG,GAAG;AAEN,iBAAS,EAAE,IAAI,MAAM,OAAO,CAAC;AAAA,MAC/B;AAAA,IACF;AAGA,WAAO,GAAG,oBAAoB,CAAC,YAAgC;AAC7D,YAAM,EAAE,OAAO,IAAI;AACnB,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AAEA,UAAI,QAAQ,uBAAuB,OAAO,IAAI;AAC5C;AAAA,MACF;AAEA,cAAQ,IAAI,iCAAiC,MAAM,EAAE;AAGrD,UAAI,QAAQ,mBAAmB;AAC7B,cAAM,cAAc,GAAG,QAAQ,QAAQ,IAAI,QAAQ,iBAAiB;AACpE,YAAI,aAAa;AACf,sBAAY,WAAW,IAAI;AAAA,QAC7B;AAAA,MACF;AAEA,cAAQ,QAAQ;AAChB,cAAQ,oBAAoB;AAC5B,cAAQ,YAAY;AACpB,cAAQ,sBAAsB;AAG9B,SAAG,GAAG,MAAM,EAAE,KAAK,iBAAiB;AAIpC,UAAI,QAAQ,oBAAoB;AAC9B,cAAM,eAAe,GAAG,QAAQ,QAAQ,IAAI,QAAQ,kBAAkB;AACtE,YAAI,cAAc;AAChB,qBAAW,MAAM;AACf,oBAAQ,YAAY,QAAQ,CAAC,MAAM;AACjC,oBAAM,SAAiC;AAAA,gBACrC,cAAc,EAAE;AAAA,gBAChB,UAAU,EAAE;AAAA,gBACZ,QAAQ,EAAE;AAAA,cACZ;AACA,2BAAa,KAAK,2BAA2B,MAAM;AAAA,YACrD,CAAC;AAAA,UACH,GAAG,GAAG;AAAA,QACR;AAAA,MACF;AAAA,IACF,CAAC;AAID,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAkC,aAAa;AACpD,YACE,cAAc,qBAAqB,gCAAgC,GACnE;AACA,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS;AAAA,YACT,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAEA,cAAM,SAAS,uBAAuB,UAAU,OAAO;AACvD,YAAI,CAAC,OAAO,SAAS;AACnB,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,OAAO,MAAM;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AACA,cAAM,EAAE,QAAQ,YAAY,OAAO,IAAI,OAAO;AAG9C,cAAM,eAAe,MAAM,YAAY,aAAa,MAAM;AAC1D,YAAI,CAAC,aAAa,YAAY;AAC5B,mBAAS;AAAA,YACP,IAAI;AAAA,YACJ,SAAS,aAAa;AAAA,YACtB,MAAM,UAAU;AAAA,UAClB,CAAC;AACD;AAAA,QACF;AAMA,YAAI,UAAU,YAAY,QAAQ,MAAM;AACxC,YAAI,SAAS;AAEX,kBAAQ,qBAAqB,OAAO;AACpC,kBAAQ,QAAQ;AAAA,QAClB,OAAO;AACL,kBAAQ,IAAI,qCAAqC,MAAM,EAAE;AACzD,oBAAU;AAAA,YACR;AAAA,YACA,oBAAoB,OAAO;AAAA,YAC3B,OAAO;AAAA,YACP,aAAa,oBAAI,IAAI;AAAA,YACrB;AAAA,YACA,WAAW;AAAA,UACb;AACA,sBAAY,QAAQ,QAAQ,OAAO;AAAA,QACrC;AAEA,oBAAY,YAAY,OAAO,IAAI,MAAM;AACzC,eAAO,KAAK,MAAM;AAClB,iBAAS,EAAE,IAAI,MAAM,OAAO,CAAC;AAC7B,WAAG,GAAG,MAAM,EAAE,KAAK,oBAAoB,EAAE,OAAO,CAAC;AAAA,MACnD;AAAA,IACF;AAEA,WAAO,GAAG,mBAAmB,CAAC,SAAgC,aAAa;AACzE,UAAI,cAAc,mBAAmB,8BAA8B,GAAG;AACpE,iBAAS;AAAA,UACP,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,MAAM,UAAU;AAAA,QAClB,CAAC;AACD;AAAA,MACF;AAEA,YAAM,SAAS,qBAAqB,UAAU,OAAO;AACrD,UAAI,CAAC,OAAO,SAAS;AACnB,iBAAS;AAAA,UACP,IAAI;AAAA,UACJ,SAAS,OAAO,MAAM;AAAA,UACtB,MAAM,UAAU;AAAA,QAClB,CAAC;AACD;AAAA,MACF;AACA,YAAM,EAAE,QAAQ,cAAc,SAAS,IAAI,OAAO;AAClD,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ,iBAAS;AAAA,UACP,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,MAAM,UAAU;AAAA,QAClB,CAAC;AACD,kBAAU,OAAO,IAAI;AAAA,UACnB,MAAM,UAAU;AAAA,UAChB,SAAS;AAAA,QACX,CAAC;AACD;AAAA,MACF;AAUA,UAAI,QAAQ,YAAY,QAAQ,QAAQ,YAAY;AAClD,iBAAS;AAAA,UACP,IAAI;AAAA,UACJ,SAAS;AAAA,UACT,MAAM,UAAU;AAAA,QAClB,CAAC;AACD,kBAAU,OAAO,IAAI;AAAA,UACnB,MAAM,UAAU;AAAA,UAChB,SAAS;AAAA,QACX,CAAC;AACD;AAAA,MACF;AAEA,YAAM,WAAW,QAAQ,YAAY,IAAI,YAAY;AACrD,UAAI,UAAU;AACZ,oBAAY,iBAAiB,SAAS,QAAQ;AAAA,MAChD;AAEA,YAAM,gBAAgB;AAAA,QACpB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,YAAM,WACJ,cAAc,QAAQ,YAAY,OAAO,cAAc,MAAM;AAE/D,UAAI;AACJ,UAAI;AACF,gBAAQ,MAAM,QAAQ,EAAE,IAAI;AAAA,MAC9B,QAAQ;AACN,gBAAQ,MAAM,SAAS,EAAE,IAAI;AAAA,MAC/B;AAEA,YAAM,gBAA+B;AAAA,QACnC,IAAI;AAAA,QACJ,OAAO,YAAY,UAAU,QAAQ,YAAY,IAAI;AAAA,QACrD;AAAA,MACF;AAEA,YAAM,oBAAuC;AAAA,QAC3C;AAAA,QACA;AAAA,QACA,UAAU,OAAO;AAAA,QACjB;AAAA,MACF;AAEA,cAAQ,YAAY,IAAI,cAAc,iBAAiB;AACvD,kBAAY,cAAc,OAAO,IAAI,EAAE,QAAQ,aAAa,CAAC;AAE7D,aAAO,KAAK,MAAM;AAElB,YAAM,SAAiC;AAAA,QACrC;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAGA,SAAG,GAAG,YAAY,gBAAgB,OAAO,CAAC,EAAE;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AAEA,eAAS,EAAE,IAAI,MAAM,cAAc,OAAO,CAAC;AAE3C,YAAM,iBAAiB;AAAA,QACrB;AAAA,QACA;AAAA,QACA,QAAQ;AAAA,MACV;AAEA,aAAO,KAAK,kBAAkB,cAAc;AAG5C,YAAM,eAAe;AAAA,QACnB;AAAA,QACA,OAAO;AAAA,UACL,WAAW,QAAQ;AAAA,QACrB;AAAA,MACF;AACA,aAAO,KAAK,gBAAgB,YAAY;AAMxC,UAAI,QAAQ,qBAAqB;AAC/B,eAAO,KAAK,iBAAiB,EAAE,KAAK,QAAQ,oBAAoB,CAAC;AAAA,MACnE;AAEA,cAAQ;AAAA,QACN,mCAAmC,MAAM,KAAK,QAAQ,YAAY,IAAI,IAAI,QAAQ,UAAU;AAAA,MAC9F;AAAA,IACF,CAAC;AAED,WAAO,GAAG,oBAAoB,CAAC,YAAoC;AACjE,YAAM,SAAS,sBAAsB,UAAU,OAAO;AACtD,UAAI,CAAC,OAAO,SAAS;AACnB;AAAA,MACF;AACA,YAAM,EAAE,QAAQ,aAAa,IAAI,OAAO;AAGxC,UAAI,CAAC,8BAA8B,QAAQ,YAAY,GAAG;AACxD;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AACA,cAAQ,YAAY,OAAO,YAAY;AACvC,kBAAY,iBAAiB,OAAO,EAAE;AACtC,YAAM,SAA+B,EAAE,aAAa;AACpD,SAAG,GAAG,YAAY,gBAAgB,OAAO,CAAC,EAAE;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AACA,aAAO,MAAM,MAAM;AAAA,IACrB,CAAC;AAED,WAAO,GAAG,oBAAoB,CAAC,YAAkC;AAC/D,YAAM,MAAM,KAAK,IAAI;AAGrB,YAAM,QAAQ,SAAS;AACvB,YAAM,iBACJ,UACC,MAAM,WAAW,QACf,OAAO,MAAM,WAAW,YACvB,MAAM,WAAW,SAChB,KAAK,IAAK,MAAM,OAAsC,KAAK,CAAC,IAC3D,QACA,KAAK,IAAK,MAAM,OAAsC,KAAK,CAAC,IAC1D;AAGV,UACE,mBACC,CAAC,0BAA0B,MAAM,yBAAyB,MAC3D;AACA,iCAAyB;AAAA,MAC3B;AAGA,YAAM,SAAS,sBAAsB,UAAU,OAAO;AACtD,UAAI,CAAC,OAAO,SAAS;AACnB,YACE,CAAC,8BACD,MAAM,6BAA6B,KACnC;AACA,uCAA6B;AAAA,QAC/B;AACA;AAAA,MACF;AAEA,YAAM,EAAE,QAAQ,aAAa,IAAI,OAAO;AAGxC,UAAI,CAAC,8BAA8B,QAAQ,YAAY,GAAG;AACxD;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ,YACE,CAAC,8BACD,MAAM,6BAA6B,KACnC;AACA,uCAA6B;AAAA,QAC/B;AACA;AAAA,MACF;AAGA,YAAM,eAAe,YAAY,gBAAgB,OAAO;AACxD,UAAI,cAAc;AAEhB,YAAI,CAAC,0BAA0B,MAAM,yBAAyB,KAAM;AAClE,mCAAyB;AAAA,QAC3B;AACA,WAAG,GAAG,YAAY,EAAE,KAAK,gBAAgB,OAAO,IAAI;AAAA,MACtD,OAAO;AACL,YACE,CAAC,8BACD,MAAM,6BAA6B,KACnC;AACA,uCAA6B;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,GAAG,qBAAqB,CAAC,YAAY;AAC1C,YAAM,SAAS,uBAAuB,UAAU,OAAO;AACvD,UAAI,CAAC,OAAO,SAAS;AACnB;AAAA,MACF;AAEA,YAAM,EAAE,QAAQ,QAAQ,IAAI,OAAO;AAGnC,UAAI,CAAC,8BAA8B,MAAM,GAAG;AAC1C;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AAEA,UAAI,YAAY,QAAQ;AAEtB,gBAAQ,IAAI,4CAA4C,MAAM,EAAE;AAGhE,YAAI,QAAQ,mBAAmB;AAC7B,gBAAM,cAAc,GAAG,QAAQ,QAAQ,IAAI,QAAQ,iBAAiB;AACpE,cAAI,aAAa;AACf,wBAAY,WAAW,IAAI;AAAA,UAC7B;AAAA,QACF;AAGA,gBAAQ,QAAQ;AAChB,gBAAQ,oBAAoB;AAC5B,gBAAQ,YAAY;AACpB,gBAAQ,sBAAsB;AAC9B,gBAAQ,YAAY;AAGpB,WAAG,GAAG,MAAM,EAAE,KAAK,iBAAiB;AAGpC,YAAI,QAAQ,oBAAoB;AAC9B,aAAG,GAAG,QAAQ,kBAAkB,EAAE,KAAK,mBAAmB;AAAA,QAC5D;AAAA,MACF,WAAW,YAAY,gBAAgB;AAErC,gBAAQ,YACN,QAAQ,cAAc,YAAY,WAAW;AAG/C,cAAM,eAAe;AAAA,UACnB;AAAA,UACA,OAAO;AAAA,YACL,WAAW,QAAQ;AAAA,UACrB;AAAA,QACF;AAEA,WAAG,GAAG,MAAM,EAAE,KAAK,gBAAgB,YAAY;AAAA,MACjD;AAAA,IACF,CAAC;AAED,WAAO,GAAG,eAAe,CAAC,YAAY;AACpC,YAAM,SAAS,uBAAuB,UAAU,OAAO;AACvD,UAAI,CAAC,OAAO,SAAS;AACnB;AAAA,MACF;AAEA,YAAM,EAAE,QAAQ,QAAQ,IAAI,OAAO;AAGnC,UAAI,CAAC,wBAAwB,MAAM,GAAG;AACpC;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AAEA,UAAI,YAAY,gBAAgB;AAE9B,gBAAQ,YACN,QAAQ,cAAc,YAAY,WAAW;AAG/C,cAAM,eAAe;AAAA,UACnB;AAAA,UACA,OAAO;AAAA,YACL,WAAW,QAAQ;AAAA,UACrB;AAAA,QACF;AAEA,WAAG,GAAG,MAAM,EAAE,KAAK,gBAAgB,YAAY;AAAA,MACjD;AAAA,IACF,CAAC;AAED,WAAO,GAAG,cAAc,CAAC,YAAoC;AAC3D,YAAM,SAAS,sBAAsB,UAAU,OAAO;AACtD,UAAI,CAAC,OAAO,QAAS;AAErB,YAAM,EAAE,QAAQ,MAAM,IAAI,OAAO;AACjC,UAAI,CAAC,wBAAwB,MAAM,GAAG;AACpC;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,SAAS;AAEX,YAAI,MAAM,WAAW;AACnB,kBAAQ,YAAY,MAAM;AAAA,QAC5B;AAGA,gBAAQ,YAAY,QAAQ,CAAC,MAAM;AACjC,aAAG,GAAG,EAAE,QAAQ,EAAE,KAAK,gBAAgB,OAAO,IAAI;AAAA,QACpD,CAAC;AAGD,YAAI,QAAQ,oBAAoB;AAC9B,aAAG,GAAG,QAAQ,kBAAkB,EAAE,KAAK,gBAAgB,OAAO,IAAI;AAAA,QACpE;AACA,YAAI,QAAQ,mBAAmB;AAC7B,aAAG,GAAG,QAAQ,iBAAiB,EAAE,KAAK,gBAAgB,OAAO,IAAI;AAAA,QACnE;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,GAAG,eAAe,CAAC,YAA2B;AACnD,YAAM,SAAS,YAAY,gBAAgB,OAAO,EAAE;AACpD,UAAI,CAAC,OAAQ;AAEb,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,QAAS;AAEd,UAAI,QAAQ,UAAU;AACpB,cAAM,aAAa,QAAQ,YAAY,IAAI,QAAQ,QAAQ;AAC3D,YAAI,YAAY;AACd,aAAG,GAAG,WAAW,QAAQ,EAAE,KAAK,iBAAiB,OAAO;AAAA,QAC1D;AAAA,MACF,OAAO;AACL,eAAO,GAAG,MAAM,EAAE,KAAK,iBAAiB,OAAO;AAAA,MACjD;AAAA,IACF,CAAC;AAED,WAAO,GAAG,mBAAmB,CAAC,YAAmC;AAC/D,YAAM,EAAE,QAAQ,oBAAoB,SAAS,QAAQ,KAAK,IAAI;AAC9D,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,QAAS;AAEd,YAAM,UAAU,EAAE,IAAI,SAAS,QAAQ,KAAK;AAE5C,UAAI,oBAAoB;AACtB,cAAM,aAAa,QAAQ,YAAY,IAAI,kBAAkB;AAC7D,YAAI,YAAY;AACd,aAAG,GAAG,WAAW,QAAQ,EAAE,KAAK,oBAAoB,OAAO;AAAA,QAC7D;AAAA,MACF,OAAO;AACL,eAAO,GAAG,MAAM,EAAE,KAAK,oBAAoB,OAAO;AAAA,MACpD;AAAA,IACF,CAAC;AAED,WAAO,GAAG,yBAAyB,CAAC,YAAmC;AACrE,YAAM,EAAE,QAAQ,SAAS,QAAQ,KAAK,IAAI;AAE1C,UAAI,CAAC,8BAA8B,MAAM,GAAG;AAC1C;AAAA,MACF;AAEA,YAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,UAAI,CAAC,QAAS;AAEd,SAAG,GAAG,YAAY,gBAAgB,OAAO,CAAC,EAAE,KAAK,oBAAoB;AAAA,QACnE,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,WAAO,GAAG,mBAAmB,CAAC,YAAkC;AAC9D,YAAM,EAAE,QAAQ,KAAK,IAAI;AACzB,YAAM,UAAU,YAAY,QAAQ,MAAM;AAG1C,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AACA,UACE,QAAQ,uBAAuB,OAAO,MACtC,QAAQ,sBAAsB,OAAO,IACrC;AACA;AAAA,MACF;AAGA,YAAM,cAAsC;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AAEA,SAAG,GAAG,MAAM,EAAE,KAAK,qBAAqB,WAAW;AAAA,IACrD,CAAC;AAGD,WAAO;AAAA,MACL;AAAA,MACA,CAAC,YAAwC;AACvC,cAAM,EAAE,QAAQ,YAAY,MAAM,aAAa,IAAI;AAEnD,cAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,YAAI,CAAC,QAAS;AAGd,cAAM,oBAAoB,QAAQ,YAAY,IAAI,YAAY;AAC9D,YAAI,CAAC,mBAAmB;AAEtB;AAAA,QACF;AAYA,YAAI,kBAAkB,aAAa,OAAO,IAAI;AAE5C,iBAAO,KAAK,MAAM;AAAA,QACpB;AAGA,cAAM,SAAS,YAAY,gBAAgB,OAAO;AAClD,YAAI,QAAQ;AAEV,gBAAM,aAAqC;AAAA,YACzC;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA,aAAG,GAAG,MAAM,EAAE,KAAK,qBAAqB,UAAU;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AAEA,WAAO,GAAG,cAAc,MAAM;AAC5B,YAAM,SAAS,YAAY,gBAAgB,OAAO,EAAE;AACpD,UAAI,QAAQ;AACV,cAAM,UAAU,YAAY,QAAQ,MAAM;AAC1C,YAAI,CAAC,SAAS;AACZ,sBAAY,WAAW,OAAO,EAAE;AAChC;AAAA,QACF;AAEA,YAAI,OAAO,OAAO,QAAQ,mBAAmB;AAE3C,kBAAQ,IAAI,6CAA6C,MAAM,EAAE;AACjE,kBAAQ,oBAAoB;AAC5B,kBAAQ,QAAQ;AAChB,kBAAQ,YAAY;AACpB,kBAAQ,sBAAsB;AAG9B,aAAG,GAAG,MAAM,EAAE,KAAK,iBAAiB;AAIpC,cAAI,QAAQ,oBAAoB;AAC9B,kBAAM,eAAe,GAAG,QAAQ,QAAQ;AAAA,cACtC,QAAQ;AAAA,YACV;AACA,gBAAI,cAAc;AAChB,yBAAW,MAAM;AACf,wBAAQ,YAAY,QAAQ,CAAC,MAAM;AACjC,wBAAM,SAAiC;AAAA,oBACrC,cAAc,EAAE;AAAA,oBAChB,UAAU,EAAE;AAAA,oBACZ,QAAQ,EAAE;AAAA,kBACZ;AACA,+BAAa,KAAK,2BAA2B,MAAM;AAAA,gBACrD,CAAC;AAAA,cACH,GAAG,GAAG;AAAA,YACR;AAAA,UACF;AAAA,QACF,WAAW,OAAO,OAAO,QAAQ,oBAAoB;AAEnD,kBAAQ,IAAI,wCAAwC,MAAM,EAAE;AAE5D,qBAAW,MAAM;AACf,kBAAM,iBAAiB,YAAY,QAAQ,MAAM;AACjD,gBACE,kBACA,eAAe,uBAAuB,OAAO,IAC7C;AACA,sBAAQ,IAAI,0BAA0B,MAAM,EAAE;AAC9C,0BAAY,WAAW,QAAQ,IAAI,mBAAmB;AAAA,YACxD;AAAA,UACF,GAAG,GAAI;AAAA,QACT;AAEA,oBAAY,WAAW,OAAO,EAAE;AAChC;AAAA,MACF;AAEA,YAAM,aAAa,YAAY,kBAAkB,OAAO,EAAE;AAC1D,UAAI,YAAY;AACd,cAAM,UAAU,YAAY,QAAQ,WAAW,MAAM;AACrD,YAAI,SAAS;AACX,kBAAQ,YAAY,OAAO,WAAW,YAAY;AAClD,gBAAM,SAA+B;AAAA,YACnC,cAAc,WAAW;AAAA,UAC3B;AACA,aAAG,GAAG,YAAY,gBAAgB,OAAO,CAAC,EAAE;AAAA,YAC1C;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,oBAAY,iBAAiB,OAAO,EAAE;AAAA,MACxC;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,WAAW,OAAO,MAAM,MAAM;AAC5B,UAAQ,IAAI,kDAAkD,IAAI,EAAE;AACtE,CAAC;","names":["io"]}
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-T6BT7WE5.js";
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 { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\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\n"],"mappings":";;;;AAOA,OAAO,YAAY;AACnB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAG9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AACpC,OAAO,OAAO,EAAE,MAAM,KAAK,WAAW,MAAM,MAAM,EAAE,CAAC;","names":[]}
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-T6BT7WE5.js";
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",
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.2",
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
+ }