@colyseus/core 0.17.43 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/build/MatchMaker.cjs +19 -6
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +18 -6
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Room.cjs +296 -19
  12. package/build/Room.cjs.map +3 -3
  13. package/build/Room.d.ts +186 -3
  14. package/build/Room.mjs +303 -21
  15. package/build/Room.mjs.map +3 -3
  16. package/build/RoomPlugin.cjs +252 -0
  17. package/build/RoomPlugin.cjs.map +7 -0
  18. package/build/RoomPlugin.d.ts +271 -0
  19. package/build/RoomPlugin.mjs +220 -0
  20. package/build/RoomPlugin.mjs.map +7 -0
  21. package/build/Server.cjs +49 -15
  22. package/build/Server.cjs.map +2 -2
  23. package/build/Server.d.ts +25 -0
  24. package/build/Server.mjs +50 -16
  25. package/build/Server.mjs.map +2 -2
  26. package/build/Transport.cjs +38 -2
  27. package/build/Transport.cjs.map +2 -2
  28. package/build/Transport.d.ts +40 -4
  29. package/build/Transport.mjs +38 -2
  30. package/build/Transport.mjs.map +2 -2
  31. package/build/index.cjs +11 -2
  32. package/build/index.cjs.map +2 -2
  33. package/build/index.d.ts +2 -1
  34. package/build/index.mjs +12 -2
  35. package/build/index.mjs.map +2 -2
  36. package/build/input/InputBuffer.cjs +113 -0
  37. package/build/input/InputBuffer.cjs.map +7 -0
  38. package/build/input/InputBuffer.d.ts +136 -0
  39. package/build/input/InputBuffer.mjs +86 -0
  40. package/build/input/InputBuffer.mjs.map +7 -0
  41. package/build/internal.cjs +61 -0
  42. package/build/internal.cjs.map +7 -0
  43. package/build/internal.d.ts +9 -0
  44. package/build/internal.mjs +29 -0
  45. package/build/internal.mjs.map +7 -0
  46. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  47. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  48. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  49. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  50. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  51. package/build/matchmaker/driver.cjs.map +1 -1
  52. package/build/matchmaker/driver.d.ts +12 -0
  53. package/build/matchmaker/driver.mjs.map +1 -1
  54. package/build/presence/LocalPresence.d.ts +1 -1
  55. package/build/rooms/LobbyRoom.cjs +8 -10
  56. package/build/rooms/LobbyRoom.cjs.map +2 -2
  57. package/build/rooms/LobbyRoom.d.ts +4 -3
  58. package/build/rooms/LobbyRoom.mjs +8 -10
  59. package/build/rooms/LobbyRoom.mjs.map +2 -2
  60. package/build/rooms/RelayRoom.cjs +12 -16
  61. package/build/rooms/RelayRoom.cjs.map +2 -2
  62. package/build/rooms/RelayRoom.d.ts +32 -11
  63. package/build/rooms/RelayRoom.mjs +10 -16
  64. package/build/rooms/RelayRoom.mjs.map +2 -2
  65. package/build/router/index.cjs +65 -4
  66. package/build/router/index.cjs.map +2 -2
  67. package/build/router/index.d.ts +30 -6
  68. package/build/router/index.mjs +66 -6
  69. package/build/router/index.mjs.map +3 -3
  70. package/build/utils/Env.cjs +4 -8
  71. package/build/utils/Env.cjs.map +3 -3
  72. package/build/utils/Env.mjs +4 -8
  73. package/build/utils/Env.mjs.map +2 -2
  74. package/build/utils/UserSessionIndex.cjs +162 -0
  75. package/build/utils/UserSessionIndex.cjs.map +7 -0
  76. package/build/utils/UserSessionIndex.d.ts +166 -0
  77. package/build/utils/UserSessionIndex.mjs +130 -0
  78. package/build/utils/UserSessionIndex.mjs.map +7 -0
  79. package/package.json +20 -15
  80. package/src/MatchMaker.ts +40 -6
  81. package/src/Protocol.ts +130 -59
  82. package/src/Room.ts +475 -22
  83. package/src/RoomPlugin.ts +563 -0
  84. package/src/Server.ts +81 -22
  85. package/src/Transport.ts +76 -8
  86. package/src/index.ts +10 -1
  87. package/src/input/InputBuffer.ts +192 -0
  88. package/src/internal.ts +46 -0
  89. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  90. package/src/matchmaker/driver.ts +13 -0
  91. package/src/rooms/LobbyRoom.ts +12 -8
  92. package/src/rooms/RelayRoom.ts +9 -15
  93. package/src/router/index.ts +112 -11
  94. package/src/utils/Env.ts +4 -12
  95. package/src/utils/UserSessionIndex.ts +311 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && expressRootRoute(expressApp) !== undefined) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\nfunction expressRootRoute(expressApp: express.Application) {\n //\n // express v5 uses `app.router`, express v4 uses `app._router`\n // check for `app._router` first, then `app.router`\n //\n // (express v4 will show a warning if `app.router` is used)\n //\n const stack = (expressApp as any)?._router?.stack ?? (expressApp as any)?.router?.stack;\n\n if (!stack) {\n return false;\n }\n\n return stack.find((layer: any) => layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * Do not use this directly. This is used internally by `@colyseus/playground`.\n * TODO: refactor. Avoid using globals.\n * @internal\n */\nexport let __globalEndpoints: Record<string, Endpoint> = {};\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n // TODO: refactor. Avoid using globals.\n __globalEndpoints = endpoints;\n\n return createBetterCallRouter({ ...endpoints }, config);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && expressRootRoute(expressApp) !== undefined) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\nfunction expressRootRoute(expressApp: express.Application) {\n //\n // express v5 uses `app.router`, express v4 uses `app._router`\n // check for `app._router` first, then `app.router`\n //\n // (express v4 will show a warning if `app.router` is used)\n //\n const stack = (expressApp as any)?._router?.stack ?? (expressApp as any)?.router?.stack;\n\n if (!stack) {\n return false;\n }\n\n return stack.find((layer: any) => layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n return createBetterCallRouter({ ...endpoints }, config);\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_better_call", "pkg", "createBetterCallRouter"]
7
7
  }
@@ -1,15 +1,10 @@
1
+ import type { IncomingMessage, ServerResponse } from "http";
1
2
  import { type Endpoint, type Router, type RouterConfig } from "@colyseus/better-call";
2
3
  import { toNodeHandler } from "@colyseus/better-call/node";
3
4
  import { Transport } from "../Transport.ts";
4
5
  export { createEndpoint, createMiddleware, createInternalContext, type Router, type RouterConfig, type Endpoint, type EndpointHandler, type EndpointOptions, type EndpointContext, type StrictEndpoint, } from "@colyseus/better-call";
5
6
  export { toNodeHandler };
6
7
  export declare function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean): void;
7
- /**
8
- * Do not use this directly. This is used internally by `@colyseus/playground`.
9
- * TODO: refactor. Avoid using globals.
10
- * @internal
11
- */
12
- export declare let __globalEndpoints: Record<string, Endpoint>;
13
8
  export declare function createRouter<E extends Record<string, Endpoint>, Config extends RouterConfig>(endpoints: E, config?: Config): {
14
9
  handler: (request: Request) => Promise<Response>;
15
10
  endpoints: E;
@@ -162,3 +157,32 @@ export declare function createRouter<E extends Record<string, Endpoint>, Config
162
157
  };
163
158
  };
164
159
  };
160
+ export interface BasicAuthOptions {
161
+ /** username → password. The common static case. */
162
+ users?: Record<string, string>;
163
+ /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */
164
+ validate?: (username: string, password: string) => boolean | Promise<boolean>;
165
+ /** Realm shown in the browser prompt. Default 'Restricted'. */
166
+ realm?: string;
167
+ }
168
+ /**
169
+ * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate
170
+ * it behind a browser credentials prompt:
171
+ *
172
+ * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })
173
+ */
174
+ export declare function basicAuth(opts: BasicAuthOptions): <InputCtx extends import("@colyseus/better-call").MiddlewareInputContext<import("@colyseus/better-call").MiddlewareOptions>>(inputContext: InputCtx) => Promise<void>;
175
+ export type ExpressMiddleware = (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => void;
176
+ export type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
177
+ export interface DualModeHelpers {
178
+ specificRouter: Router;
179
+ specificHandler: NodeHandler;
180
+ fullRouter: Router;
181
+ fullHandler: NodeHandler;
182
+ }
183
+ export declare function dualModeEndpoints<E extends Record<string, Endpoint>>(endpoints: E, opts: {
184
+ /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */
185
+ catchAllKey?: keyof E;
186
+ /** Build the express middleware given the pre-built routers + node handlers. */
187
+ buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;
188
+ }): ExpressMiddleware & E;
@@ -1,12 +1,13 @@
1
1
  // packages/core/src/router/index.ts
2
- import { createRouter as createBetterCallRouter, createEndpoint } from "@colyseus/better-call";
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from "@colyseus/better-call";
3
4
  import { toNodeHandler, getRequest, setResponse } from "@colyseus/better-call/node";
4
5
  import "../Transport.mjs";
5
6
  import { controller } from "../matchmaker/controller.mjs";
6
7
  import pkg from "../../package.json" with { type: "json" };
7
8
  import {
8
9
  createEndpoint as createEndpoint2,
9
- createMiddleware,
10
+ createMiddleware as createMiddleware2,
10
11
  createInternalContext
11
12
  } from "@colyseus/better-call";
12
13
  function bindRouterToTransport(transport, router, useExpress) {
@@ -68,17 +69,76 @@ function expressRootRoute(expressApp) {
68
69
  }
69
70
  return stack.find((layer) => layer.match("/") && !["query", "expressInit"].includes(layer.name));
70
71
  }
71
- var __globalEndpoints = {};
72
72
  function createRouter(endpoints, config = {}) {
73
- __globalEndpoints = endpoints;
74
73
  return createBetterCallRouter({ ...endpoints }, config);
75
74
  }
75
+ function basicAuth(opts) {
76
+ const { users, validate } = opts;
77
+ if (!users && !validate) {
78
+ throw new Error("[basicAuth] provide `users` or `validate`");
79
+ }
80
+ const challenge = `Basic realm="${(opts.realm ?? "Restricted").replace(/"/g, "")}", charset="UTF-8"`;
81
+ return createMiddleware(async (ctx) => {
82
+ const creds = parseBasicHeader(ctx.getHeader("authorization"));
83
+ const ok = !!creds && (validate ? await validate(creds.username, creds.password) : staticCheck(users, creds.username, creds.password));
84
+ if (!ok) {
85
+ throw new APIError(401, { message: "authentication required" }, { "WWW-Authenticate": challenge });
86
+ }
87
+ });
88
+ }
89
+ function parseBasicHeader(header) {
90
+ if (!header) {
91
+ return null;
92
+ }
93
+ const sep = header.indexOf(" ");
94
+ if (sep < 0 || header.slice(0, sep).toLowerCase() !== "basic") {
95
+ return null;
96
+ }
97
+ let decoded;
98
+ try {
99
+ decoded = Buffer.from(header.slice(sep + 1), "base64").toString("utf8");
100
+ } catch {
101
+ return null;
102
+ }
103
+ const colon = decoded.indexOf(":");
104
+ if (colon < 0) {
105
+ return null;
106
+ }
107
+ return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };
108
+ }
109
+ function staticCheck(users, username, password) {
110
+ const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : void 0;
111
+ return safeEqual(password, expected ?? "\0") && expected !== void 0;
112
+ }
113
+ function safeEqual(a, b) {
114
+ return timingSafeEqual(
115
+ createHash("sha256").update(a).digest(),
116
+ createHash("sha256").update(b).digest()
117
+ );
118
+ }
119
+ function dualModeEndpoints(endpoints, opts) {
120
+ const fullRouter = createRouter(endpoints);
121
+ const fullHandler = toNodeHandler(fullRouter.handler);
122
+ const specificEndpoints = opts.catchAllKey ? Object.fromEntries(
123
+ Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey)
124
+ ) : endpoints;
125
+ const specificRouter = createRouter(specificEndpoints);
126
+ const specificHandler = toNodeHandler(specificRouter.handler);
127
+ const middleware = opts.buildMiddleware({
128
+ specificRouter,
129
+ specificHandler,
130
+ fullRouter,
131
+ fullHandler
132
+ });
133
+ return Object.assign(middleware, endpoints);
134
+ }
76
135
  export {
77
- __globalEndpoints,
136
+ basicAuth,
78
137
  bindRouterToTransport,
79
138
  createEndpoint2 as createEndpoint,
80
139
  createInternalContext,
81
- createMiddleware,
140
+ createMiddleware2 as createMiddleware,
82
141
  createRouter,
142
+ dualModeEndpoints,
83
143
  toNodeHandler
84
144
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && expressRootRoute(expressApp) !== undefined) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\nfunction expressRootRoute(expressApp: express.Application) {\n //\n // express v5 uses `app.router`, express v4 uses `app._router`\n // check for `app._router` first, then `app.router`\n //\n // (express v4 will show a warning if `app.router` is used)\n //\n const stack = (expressApp as any)?._router?.stack ?? (expressApp as any)?.router?.stack;\n\n if (!stack) {\n return false;\n }\n\n return stack.find((layer: any) => layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * Do not use this directly. This is used internally by `@colyseus/playground`.\n * TODO: refactor. Avoid using globals.\n * @internal\n */\nexport let __globalEndpoints: Record<string, Endpoint> = {};\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n // TODO: refactor. Avoid using globals.\n __globalEndpoints = endpoints;\n\n return createBetterCallRouter({ ...endpoints }, config);\n}\n"],
5
- "mappings": "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",
6
- "names": ["createEndpoint"]
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export types needed for declaration emit\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && expressRootRoute(expressApp) !== undefined) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\nfunction expressRootRoute(expressApp: express.Application) {\n //\n // express v5 uses `app.router`, express v4 uses `app._router`\n // check for `app._router` first, then `app.router`\n //\n // (express v4 will show a warning if `app.router` is used)\n //\n const stack = (expressApp as any)?._router?.stack ?? (expressApp as any)?.router?.stack;\n\n if (!stack) {\n return false;\n }\n\n return stack.find((layer: any) => layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n return createBetterCallRouter({ ...endpoints }, config);\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createEndpoint", "createMiddleware"]
7
7
  }
@@ -36,33 +36,29 @@ __export(Env_exports, {
36
36
  isColyseusCloud: () => isColyseusCloud
37
37
  });
38
38
  module.exports = __toCommonJS(Env_exports);
39
- var import_os = __toESM(require("os"), 1);
40
39
  var import_LocalPresence = require("../presence/LocalPresence.cjs");
41
40
  var import_LocalDriver = require("../matchmaker/LocalDriver/LocalDriver.cjs");
42
41
  var import_Logger = require("../Logger.cjs");
43
42
  function isColyseusCloud() {
44
43
  return process.env.COLYSEUS_CLOUD !== void 0;
45
44
  }
46
- function shouldUseRedisOnCloud() {
47
- return import_os.default.cpus().length > 1 || process.env.REDIS_URI !== void 0;
48
- }
49
45
  async function getDefaultPresence() {
50
- if (isColyseusCloud() && shouldUseRedisOnCloud()) {
46
+ if (isColyseusCloud()) {
51
47
  try {
52
48
  const RedisPresence = await import("@colyseus/redis-presence");
53
49
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
54
50
  } catch (e) {
55
51
  console.error(e);
56
52
  import_Logger.logger.warn("");
57
- import_Logger.logger.warn("\u274C could not initialize RedisPresence.");
58
- import_Logger.logger.warn("\u{1F449} npm install --save @colyseus/redis-presence");
53
+ import_Logger.logger.warn("\u274C could not initialize RedisDriver.");
54
+ import_Logger.logger.warn("\u{1F449} npm install --save @colyseus/redis-driver");
59
55
  import_Logger.logger.warn("");
60
56
  }
61
57
  }
62
58
  return new import_LocalPresence.LocalPresence();
63
59
  }
64
60
  async function getDefaultDriver() {
65
- if (isColyseusCloud() && shouldUseRedisOnCloud()) {
61
+ if (isColyseusCloud()) {
66
62
  try {
67
63
  const RedisDriver = await import("@colyseus/redis-driver");
68
64
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Env.ts"],
4
- "sourcesContent": ["import os from 'os';\nimport { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\n// On Colyseus Cloud, only opt into Redis when there's more than one CPU\n// (multi-process deploys) or when REDIS_URI is explicitly provided.\n// Single-CPU instances without REDIS_URI keep using Local presence/driver.\nfunction shouldUseRedisOnCloud(): boolean {\n return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAe;AACf,2BAA8B;AAC9B,yBAAmD;AACnD,oBAAuB;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAKA,SAAS,wBAAiC;AACxC,SAAO,UAAAA,QAAG,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,cAAc;AAC3D;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,4CAAuC;AACnD,2BAAO,KAAK,uDAAgD;AAC5D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,mCAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,+BAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
- "names": ["os"]
4
+ "sourcesContent": ["import { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA8B;AAC9B,yBAAmD;AACnD,oBAAuB;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,mCAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,2BAAO,KAAK,EAAE;AACd,2BAAO,KAAK,0CAAqC;AACjD,2BAAO,KAAK,qDAA8C;AAC1D,2BAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,+BAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
7
  }
@@ -1,31 +1,27 @@
1
1
  // packages/core/src/utils/Env.ts
2
- import os from "os";
3
2
  import { LocalPresence } from "../presence/LocalPresence.mjs";
4
3
  import { LocalDriver } from "../matchmaker/LocalDriver/LocalDriver.mjs";
5
4
  import { logger } from "../Logger.mjs";
6
5
  function isColyseusCloud() {
7
6
  return process.env.COLYSEUS_CLOUD !== void 0;
8
7
  }
9
- function shouldUseRedisOnCloud() {
10
- return os.cpus().length > 1 || process.env.REDIS_URI !== void 0;
11
- }
12
8
  async function getDefaultPresence() {
13
- if (isColyseusCloud() && shouldUseRedisOnCloud()) {
9
+ if (isColyseusCloud()) {
14
10
  try {
15
11
  const RedisPresence = await import("@colyseus/redis-presence");
16
12
  return new RedisPresence.RedisPresence(process.env.REDIS_URI);
17
13
  } catch (e) {
18
14
  console.error(e);
19
15
  logger.warn("");
20
- logger.warn("\u274C could not initialize RedisPresence.");
21
- logger.warn("\u{1F449} npm install --save @colyseus/redis-presence");
16
+ logger.warn("\u274C could not initialize RedisDriver.");
17
+ logger.warn("\u{1F449} npm install --save @colyseus/redis-driver");
22
18
  logger.warn("");
23
19
  }
24
20
  }
25
21
  return new LocalPresence();
26
22
  }
27
23
  async function getDefaultDriver() {
28
- if (isColyseusCloud() && shouldUseRedisOnCloud()) {
24
+ if (isColyseusCloud()) {
29
25
  try {
30
26
  const RedisDriver = await import("@colyseus/redis-driver");
31
27
  return new RedisDriver.RedisDriver(process.env.REDIS_URI);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/utils/Env.ts"],
4
- "sourcesContent": ["import os from 'os';\nimport { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\n// On Colyseus Cloud, only opt into Redis when there's more than one CPU\n// (multi-process deploys) or when REDIS_URI is explicitly provided.\n// Single-CPU instances without REDIS_URI keep using Local presence/driver.\nfunction shouldUseRedisOnCloud(): boolean {\n return os.cpus().length > 1 || process.env.REDIS_URI !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisPresence.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-presence\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud() && shouldUseRedisOnCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
- "mappings": ";AAAA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,mBAA0C;AACnD,SAAS,cAAc;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAKA,SAAS,wBAAiC;AACxC,SAAO,GAAG,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,cAAc;AAC3D;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,4CAAuC;AACnD,aAAO,KAAK,uDAAgD;AAC5D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,cAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,KAAK,sBAAsB,GAAG;AAChD,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,YAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import { LocalPresence } from '../presence/LocalPresence.ts';\nimport { LocalDriver, type MatchMakerDriver } from '../matchmaker/LocalDriver/LocalDriver.ts';\nimport { logger } from '../Logger.ts';\nimport type { Presence } from '../presence/Presence.ts';\n\nexport function isColyseusCloud(): boolean {\n return process.env.COLYSEUS_CLOUD !== undefined;\n}\n\nexport async function getDefaultPresence(): Promise<Presence> {\n if (isColyseusCloud()) {\n try {\n const RedisPresence = await import('@colyseus/redis-presence');\n return new RedisPresence.RedisPresence(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalPresence();\n}\n\nexport async function getDefaultDriver(): Promise<MatchMakerDriver> {\n if (isColyseusCloud()) {\n try {\n const RedisDriver = await import('@colyseus/redis-driver');\n return new RedisDriver.RedisDriver(process.env.REDIS_URI);\n } catch (e) {\n console.error(e);\n logger.warn(\"\");\n logger.warn(\"\u274C could not initialize RedisDriver.\");\n logger.warn(\"\uD83D\uDC49 npm install --save @colyseus/redis-driver\");\n logger.warn(\"\");\n }\n }\n return new LocalDriver();\n}\n\nexport function getDefaultPublicAddress(): string | undefined {\n if (isColyseusCloud()) {\n let port = 2567;\n\n //\n // Multiple processes: Use NODE_APP_INSTANCE to play nicely with pm2\n //\n port += Number(process.env.NODE_APP_INSTANCE || \"0\");\n\n return process.env.SUBDOMAIN + \".\" + process.env.SERVER_NAME + \"/\" + port;\n\n } else {\n return undefined;\n }\n}"],
5
+ "mappings": ";AAAA,SAAS,qBAAqB;AAC9B,SAAS,mBAA0C;AACnD,SAAS,cAAc;AAGhB,SAAS,kBAA2B;AACzC,SAAO,QAAQ,IAAI,mBAAmB;AACxC;AAEA,eAAsB,qBAAwC;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAC7D,aAAO,IAAI,cAAc,cAAc,QAAQ,IAAI,SAAS;AAAA,IAC9D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,cAAc;AAC3B;AAEA,eAAsB,mBAA8C;AAClE,MAAI,gBAAgB,GAAG;AACrB,QAAI;AACF,YAAM,cAAc,MAAM,OAAO,wBAAwB;AACzD,aAAO,IAAI,YAAY,YAAY,QAAQ,IAAI,SAAS;AAAA,IAC1D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,aAAO,KAAK,EAAE;AACd,aAAO,KAAK,0CAAqC;AACjD,aAAO,KAAK,qDAA8C;AAC1D,aAAO,KAAK,EAAE;AAAA,IAChB;AAAA,EACF;AACA,SAAO,IAAI,YAAY;AACzB;AAEO,SAAS,0BAA8C;AAC5D,MAAI,gBAAgB,GAAG;AACrB,QAAI,OAAO;AAKX,YAAQ,OAAO,QAAQ,IAAI,qBAAqB,GAAG;AAEnD,WAAO,QAAQ,IAAI,YAAY,MAAM,QAAQ,IAAI,cAAc,MAAM;AAAA,EAEvE,OAAO;AACL,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/core/src/utils/UserSessionIndex.ts
21
+ var UserSessionIndex_exports = {};
22
+ __export(UserSessionIndex_exports, {
23
+ USER_ROOMS_KEY_PREFIX: () => USER_ROOMS_KEY_PREFIX,
24
+ listUserSessions: () => listUserSessions,
25
+ releaseRoomLeave: () => releaseRoomLeave,
26
+ releaseUserSession: () => releaseUserSession,
27
+ sweepRoomDispose: () => sweepRoomDispose,
28
+ trackRoomJoin: () => trackRoomJoin,
29
+ trackUserSession: () => trackUserSession,
30
+ userRoomsKey: () => userRoomsKey
31
+ });
32
+ module.exports = __toCommonJS(UserSessionIndex_exports);
33
+ var USER_ROOMS_KEY_PREFIX = "colyseus:user-rooms:";
34
+ function userRoomsKey(userId) {
35
+ return USER_ROOMS_KEY_PREFIX + userId;
36
+ }
37
+ var tracked = /* @__PURE__ */ new WeakMap();
38
+ function getTrackingMap(room) {
39
+ let map = tracked.get(room);
40
+ if (!map) {
41
+ map = /* @__PURE__ */ new Map();
42
+ tracked.set(room, map);
43
+ }
44
+ return map;
45
+ }
46
+ function resolveUserId(client) {
47
+ return client.userId ?? client.auth?.id;
48
+ }
49
+ async function trackUserSession(presence, userId, sessionId, entry) {
50
+ try {
51
+ await presence.hset(userRoomsKey(userId), sessionId, JSON.stringify(entry));
52
+ } catch {
53
+ }
54
+ }
55
+ async function releaseUserSession(presence, userId, sessionId) {
56
+ try {
57
+ await presence.hdel(userRoomsKey(userId), sessionId);
58
+ } catch {
59
+ }
60
+ }
61
+ function trackRoomJoin(room, client) {
62
+ if (!room.presence) {
63
+ return;
64
+ }
65
+ const userId = resolveUserId(client);
66
+ if (!userId) {
67
+ return;
68
+ }
69
+ getTrackingMap(room).set(client.sessionId, userId);
70
+ const entry = {
71
+ roomId: room.roomId,
72
+ roomName: room.roomName,
73
+ joinedAt: Date.now()
74
+ };
75
+ void trackUserSession(room.presence, userId, client.sessionId, entry);
76
+ }
77
+ function releaseRoomLeave(room, client) {
78
+ if (!room.presence) {
79
+ return;
80
+ }
81
+ const map = tracked.get(room);
82
+ const userId = map?.get(client.sessionId);
83
+ if (!userId || !map) {
84
+ return;
85
+ }
86
+ map.delete(client.sessionId);
87
+ void releaseUserSession(room.presence, userId, client.sessionId);
88
+ }
89
+ async function sweepRoomDispose(room) {
90
+ if (!room.presence) {
91
+ return;
92
+ }
93
+ const map = tracked.get(room);
94
+ if (!map || map.size === 0) {
95
+ return;
96
+ }
97
+ const pending = [];
98
+ for (const [sessionId, userId] of map) {
99
+ pending.push(releaseUserSession(room.presence, userId, sessionId));
100
+ }
101
+ map.clear();
102
+ await Promise.all(pending);
103
+ }
104
+ async function listUserSessions(presence, findRooms, userId, options = {}) {
105
+ const reconcile = options.reconcile === true;
106
+ const removeStale = reconcile && options.removeStale === true;
107
+ let raw;
108
+ try {
109
+ raw = await presence.hgetall(userRoomsKey(userId));
110
+ } catch {
111
+ return [];
112
+ }
113
+ const fields = Object.keys(raw);
114
+ if (fields.length === 0) {
115
+ return [];
116
+ }
117
+ const staleSessions = [];
118
+ const parsed = [];
119
+ for (const sessionId of fields) {
120
+ try {
121
+ parsed.push({ sessionId, entry: JSON.parse(raw[sessionId]) });
122
+ } catch {
123
+ staleSessions.push(sessionId);
124
+ }
125
+ }
126
+ if (!reconcile) {
127
+ return parsed.map(({ sessionId, entry }) => ({ sessionId, ...entry }));
128
+ }
129
+ const live = parsed.length > 0 ? await findRooms(parsed.map((p) => p.entry.roomId)) : /* @__PURE__ */ new Map();
130
+ const result = [];
131
+ for (const { sessionId, entry } of parsed) {
132
+ const room = live.get(entry.roomId);
133
+ if (!room) {
134
+ staleSessions.push(sessionId);
135
+ continue;
136
+ }
137
+ const info = { sessionId, ...entry };
138
+ if (room.processId !== void 0) {
139
+ info.processId = room.processId;
140
+ }
141
+ result.push(info);
142
+ }
143
+ if (removeStale && staleSessions.length > 0) {
144
+ const key = userRoomsKey(userId);
145
+ void Promise.all(
146
+ staleSessions.map((s) => presence.hdel(key, s))
147
+ ).catch(() => {
148
+ });
149
+ }
150
+ return result;
151
+ }
152
+ // Annotate the CommonJS export names for ESM import in node:
153
+ 0 && (module.exports = {
154
+ USER_ROOMS_KEY_PREFIX,
155
+ listUserSessions,
156
+ releaseRoomLeave,
157
+ releaseUserSession,
158
+ sweepRoomDispose,
159
+ trackRoomJoin,
160
+ trackUserSession,
161
+ userRoomsKey
162
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/utils/UserSessionIndex.ts"],
4
+ "sourcesContent": ["/**\n * User \u2192 active sessions index: \"which active rooms is user X in?\"\n *\n * Without this, answering that question would require fanning out\n * `getInspectorView()` across every running room \u2014 O(rooms) per lookup.\n * Instead, each Room writes a small Presence hash entry on `_onJoin` and\n * removes it on `_onAfterLeave` / dispose. The admin endpoint reads the\n * hash with a single `hgetall`, then reconciles against the matchmaker's\n * live room listing to drop stale entries left behind by hard crashes\n * (no `onLeave` ran).\n *\n * Hash schema:\n * key: colyseus:user-rooms:{userId}\n * field: sessionId\n * value: JSON `{ roomId, roomName, joinedAt }` (joinedAt is unix ms)\n *\n * Anonymous clients (no userId resolvable) are skipped \u2014 the index is\n * for forensic / support workflows, not anonymous traffic. A Room can\n * also opt out wholesale by setting `trackUserSessions = false` (e.g.\n * a high-volume relay room that doesn't want to pay the Presence\n * write per join).\n *\n * Per-Room state (which sessionIds have an entry, and under which userId)\n * lives in a module-level WeakMap rather than as a field on Room itself.\n * Two reasons:\n *\n * 1. Room.ts stays thin \u2014 it only knows about three call points\n * (`trackRoomJoin`, `releaseRoomLeave`, `sweepRoomDispose`) and\n * doesn't have to carry an extra Map field or two private helper\n * methods solely for this concern.\n * 2. The WeakMap is GC-tied to the Room \u2014 when a Room instance is\n * collected, its entries vanish automatically. No explicit teardown\n * hook needed beyond the dispose sweep that drains Presence.\n *\n * @internal\n */\nimport type { Presence } from '../presence/Presence.ts';\n\nexport const USER_ROOMS_KEY_PREFIX = 'colyseus:user-rooms:';\n\nexport function userRoomsKey(userId: string): string {\n return USER_ROOMS_KEY_PREFIX + userId;\n}\n\n/**\n * What's serialized into the Presence hash value (sessionId is the\n * hash field key, not part of the body). Internal write-side shape.\n */\nexport interface UserRoomEntry {\n roomId: string;\n roomName: string;\n joinedAt: number;\n}\n\n/**\n * Public read-side shape returned by `listUserSessions`: a parsed\n * `UserRoomEntry` plus its sessionId, optionally enriched with\n * `processId` when reconcile against the matchmaker was on.\n */\nexport interface UserSessionInfo extends UserRoomEntry {\n sessionId: string;\n /**\n * Process hosting the room, per the matchmaker. Populated only\n * when `listUserSessions` was called with `reconcile: true` AND\n * the room is still in the matchmaker roster.\n */\n processId?: string;\n}\n\n/**\n * Structural subset of `Room` needed by the index. Lets this module\n * avoid importing `Room` (which would create a cycle) while still\n * staying typed at the call site.\n */\ninterface InspectorRoomShape {\n roomId: string;\n roomName: string;\n presence: Presence;\n}\n\n/**\n * Structural subset of a Client we read at join/leave. `userId` and\n * `auth` are both optional \u2014 the index simply skips clients without\n * either, which is the correct \"anonymous traffic doesn't show up\n * in support tooling\" behavior.\n */\ninterface InspectorClientShape {\n sessionId: string;\n userId?: string;\n auth?: { id?: string } | null;\n}\n\n/**\n * sessionId \u2192 userId for clients currently registered in the index,\n * scoped per Room. WeakMap-keyed so a forgotten Room takes its tracking\n * map with it.\n */\nconst tracked = new WeakMap<InspectorRoomShape, Map<string, string>>();\n\nfunction getTrackingMap(room: InspectorRoomShape): Map<string, string> {\n let map = tracked.get(room);\n if (!map) {\n map = new Map();\n tracked.set(room, map);\n }\n return map;\n}\n\nfunction resolveUserId(client: InspectorClientShape): string | undefined {\n return client.userId ?? client.auth?.id;\n}\n\n/**\n * Best-effort: write the join entry. Errors are swallowed because the\n * index is observability metadata \u2014 a Presence outage shouldn't reject\n * a player's join. Exposed as a pure helper for tests + the admin\n * endpoint; `trackRoomJoin` is the Room-flavored entrypoint.\n */\nexport async function trackUserSession(\n presence: Presence,\n userId: string,\n sessionId: string,\n entry: UserRoomEntry,\n): Promise<void> {\n try {\n await presence.hset(userRoomsKey(userId), sessionId, JSON.stringify(entry));\n } catch {\n // intentional: see fn-doc\n }\n}\n\n/**\n * Best-effort: remove the join entry. Errors are swallowed so a Presence\n * blip doesn't bubble into `_onAfterLeave` / `_dispose`.\n */\nexport async function releaseUserSession(\n presence: Presence,\n userId: string,\n sessionId: string,\n): Promise<void> {\n try {\n await presence.hdel(userRoomsKey(userId), sessionId);\n } catch {\n // intentional: see fn-doc\n }\n}\n\n/**\n * Record `client`'s join under `room` in the reverse index. No-op for\n * clients with no resolvable userId (anonymous), and when the room\n * carries no presence (shouldn't happen \u2014 defensive for unit tests).\n *\n * Fire-and-forget against Presence; the in-memory tracking map updates\n * synchronously so a follow-up `releaseRoomLeave` always finds the\n * right userId even if the Presence write is still in flight.\n */\nexport function trackRoomJoin(room: InspectorRoomShape, client: InspectorClientShape): void {\n if (!room.presence) { return; }\n const userId = resolveUserId(client);\n if (!userId) { return; }\n getTrackingMap(room).set(client.sessionId, userId);\n const entry: UserRoomEntry = {\n roomId: room.roomId,\n roomName: room.roomName,\n joinedAt: Date.now(),\n };\n void trackUserSession(room.presence, userId, client.sessionId, entry);\n}\n\n/**\n * Drop `client`'s entry from the reverse index. Idempotent \u2014 no-op\n * when the client wasn't tracked (anonymous, or tracking failed at\n * join time).\n */\nexport function releaseRoomLeave(room: InspectorRoomShape, client: InspectorClientShape): void {\n if (!room.presence) { return; }\n const map = tracked.get(room);\n const userId = map?.get(client.sessionId);\n if (!userId || !map) { return; }\n map.delete(client.sessionId);\n void releaseUserSession(room.presence, userId, client.sessionId);\n}\n\n/**\n * Sweep any still-tracked sessions for `room`. Called during dispose to\n * cover the case where `disconnect()` races the per-client `_onAfterLeave`\n * path \u2014 the read-side reconcile handles cross-process crash recovery,\n * but this is the cheap deterministic cleanup for a clean local dispose.\n *\n * Awaits the pending `hdel`s so the caller can sequence against \"the\n * index is now coherent\" \u2014 the dispose path uses that ordering.\n */\nexport async function sweepRoomDispose(room: InspectorRoomShape): Promise<void> {\n if (!room.presence) { return; }\n const map = tracked.get(room);\n if (!map || map.size === 0) { return; }\n const pending: Promise<void>[] = [];\n for (const [sessionId, userId] of map) {\n pending.push(releaseUserSession(room.presence, userId, sessionId));\n }\n map.clear();\n await Promise.all(pending);\n}\n\n/**\n * Minimal shape of a matchmaker room record needed for reconcile \u2014\n * keeps this module decoupled from the matchmaker / driver types.\n * `matchMaker.query()`'s actual return (`IRoomCache[]`) is structurally\n * a supertype of this, so callers can pass `matchMaker.query` directly.\n */\ninterface MatchmakerRoomLike {\n roomId: string;\n processId?: string;\n}\n\nexport interface ListUserSessionsOptions {\n /**\n * Drop entries whose `roomId` is no longer in the matchmaker roster\n * (the index can lag a crashed process). When `true`, the returned\n * entries also carry `processId` from the live room record.\n *\n * Off by default \u2014 most callers (kick everyone, count) don't need it\n * and the extra `matchMaker.query` round-trip isn't free.\n */\n reconcile?: boolean;\n\n /**\n * Fire-and-forget `hdel` for stale entries \u2014 those dropped by\n * reconcile (matchmaker doesn't know the room anymore) plus any\n * with corrupt JSON. Lets read endpoints self-heal the index on\n * each call. No-op when `reconcile` is `false`.\n */\n removeStale?: boolean;\n}\n\n/**\n * Read the user \u2192 active sessions index. Pure helper \u2014 the\n * `Presence` + matchmaker batch lookup are injected so this module\n * stays free of matchmaker imports (and so unit tests can drive it\n * with fake deps).\n *\n * Wire-op count per call:\n * - 1 HGETALL on the user's hash (always).\n * - 1 batch room lookup when `reconcile: true` AND there are\n * entries to verify; skipped otherwise.\n *\n * Bounded at 2 wire ops regardless of the user's session count.\n */\nexport async function listUserSessions(\n presence: Presence,\n findRooms: (roomIds: string[]) => Promise<Map<string, MatchmakerRoomLike>>,\n userId: string,\n options: ListUserSessionsOptions = {},\n): Promise<UserSessionInfo[]> {\n const reconcile = options.reconcile === true;\n const removeStale = reconcile && options.removeStale === true;\n\n let raw: Record<string, string>;\n try {\n raw = await presence.hgetall(userRoomsKey(userId));\n } catch {\n // Presence outage \u2014 observability shouldn't bring down the caller.\n return [];\n }\n const fields = Object.keys(raw);\n if (fields.length === 0) { return []; }\n\n const staleSessions: string[] = [];\n const parsed: Array<{ sessionId: string; entry: UserRoomEntry }> = [];\n for (const sessionId of fields) {\n try {\n parsed.push({ sessionId, entry: JSON.parse(raw[sessionId]) as UserRoomEntry });\n } catch {\n // Corrupt JSON \u2014 index drift. Removable even without reconcile.\n staleSessions.push(sessionId);\n }\n }\n\n if (!reconcile) {\n return parsed.map(({ sessionId, entry }) => ({ sessionId, ...entry }));\n }\n\n // One batch lookup for the K roomIds we care about. K = entries\n // surviving the JSON.parse stage, not cluster size.\n const live = parsed.length > 0\n ? await findRooms(parsed.map((p) => p.entry.roomId))\n : new Map<string, MatchmakerRoomLike>();\n\n const result: UserSessionInfo[] = [];\n for (const { sessionId, entry } of parsed) {\n const room = live.get(entry.roomId);\n if (!room) {\n // Matchmaker doesn't know this roomId anymore \u2014 stale entry\n // from a crashed process. Drop it (and remove if requested).\n staleSessions.push(sessionId);\n continue;\n }\n const info: UserSessionInfo = { sessionId, ...entry };\n if (room.processId !== undefined) { info.processId = room.processId; }\n result.push(info);\n }\n\n if (removeStale && staleSessions.length > 0) {\n const key = userRoomsKey(userId);\n void Promise.all(\n staleSessions.map((s) => presence.hdel(key, s)),\n ).catch(() => { /* presence outage, swallow */ });\n }\n\n return result;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsCO,IAAM,wBAAwB;AAE9B,SAAS,aAAa,QAAwB;AACnD,SAAO,wBAAwB;AACjC;AAuDA,IAAM,UAAU,oBAAI,QAAiD;AAErE,SAAS,eAAe,MAA+C;AACrE,MAAI,MAAM,QAAQ,IAAI,IAAI;AAC1B,MAAI,CAAC,KAAK;AACR,UAAM,oBAAI,IAAI;AACd,YAAQ,IAAI,MAAM,GAAG;AAAA,EACvB;AACA,SAAO;AACT;AAEA,SAAS,cAAc,QAAkD;AACvE,SAAO,OAAO,UAAU,OAAO,MAAM;AACvC;AAQA,eAAsB,iBACpB,UACA,QACA,WACA,OACe;AACf,MAAI;AACF,UAAM,SAAS,KAAK,aAAa,MAAM,GAAG,WAAW,KAAK,UAAU,KAAK,CAAC;AAAA,EAC5E,QAAQ;AAAA,EAER;AACF;AAMA,eAAsB,mBACpB,UACA,QACA,WACe;AACf,MAAI;AACF,UAAM,SAAS,KAAK,aAAa,MAAM,GAAG,SAAS;AAAA,EACrD,QAAQ;AAAA,EAER;AACF;AAWO,SAAS,cAAc,MAA0B,QAAoC;AAC1F,MAAI,CAAC,KAAK,UAAU;AAAE;AAAA,EAAQ;AAC9B,QAAM,SAAS,cAAc,MAAM;AACnC,MAAI,CAAC,QAAQ;AAAE;AAAA,EAAQ;AACvB,iBAAe,IAAI,EAAE,IAAI,OAAO,WAAW,MAAM;AACjD,QAAM,QAAuB;AAAA,IAC3B,QAAQ,KAAK;AAAA,IACb,UAAU,KAAK;AAAA,IACf,UAAU,KAAK,IAAI;AAAA,EACrB;AACA,OAAK,iBAAiB,KAAK,UAAU,QAAQ,OAAO,WAAW,KAAK;AACtE;AAOO,SAAS,iBAAiB,MAA0B,QAAoC;AAC7F,MAAI,CAAC,KAAK,UAAU;AAAE;AAAA,EAAQ;AAC9B,QAAM,MAAM,QAAQ,IAAI,IAAI;AAC5B,QAAM,SAAS,KAAK,IAAI,OAAO,SAAS;AACxC,MAAI,CAAC,UAAU,CAAC,KAAK;AAAE;AAAA,EAAQ;AAC/B,MAAI,OAAO,OAAO,SAAS;AAC3B,OAAK,mBAAmB,KAAK,UAAU,QAAQ,OAAO,SAAS;AACjE;AAWA,eAAsB,iBAAiB,MAAyC;AAC9E,MAAI,CAAC,KAAK,UAAU;AAAE;AAAA,EAAQ;AAC9B,QAAM,MAAM,QAAQ,IAAI,IAAI;AAC5B,MAAI,CAAC,OAAO,IAAI,SAAS,GAAG;AAAE;AAAA,EAAQ;AACtC,QAAM,UAA2B,CAAC;AAClC,aAAW,CAAC,WAAW,MAAM,KAAK,KAAK;AACrC,YAAQ,KAAK,mBAAmB,KAAK,UAAU,QAAQ,SAAS,CAAC;AAAA,EACnE;AACA,MAAI,MAAM;AACV,QAAM,QAAQ,IAAI,OAAO;AAC3B;AA8CA,eAAsB,iBACpB,UACA,WACA,QACA,UAAmC,CAAC,GACR;AAC5B,QAAM,YAAY,QAAQ,cAAc;AACxC,QAAM,cAAc,aAAa,QAAQ,gBAAgB;AAEzD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,QAAQ,aAAa,MAAM,CAAC;AAAA,EACnD,QAAQ;AAEN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,OAAO,KAAK,GAAG;AAC9B,MAAI,OAAO,WAAW,GAAG;AAAE,WAAO,CAAC;AAAA,EAAG;AAEtC,QAAM,gBAA0B,CAAC;AACjC,QAAM,SAA6D,CAAC;AACpE,aAAW,aAAa,QAAQ;AAC9B,QAAI;AACF,aAAO,KAAK,EAAE,WAAW,OAAO,KAAK,MAAM,IAAI,SAAS,CAAC,EAAmB,CAAC;AAAA,IAC/E,QAAQ;AAEN,oBAAc,KAAK,SAAS;AAAA,IAC9B;AAAA,EACF;AAEA,MAAI,CAAC,WAAW;AACd,WAAO,OAAO,IAAI,CAAC,EAAE,WAAW,MAAM,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE;AAAA,EACvE;AAIA,QAAM,OAAO,OAAO,SAAS,IACzB,MAAM,UAAU,OAAO,IAAI,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,IACjD,oBAAI,IAAgC;AAExC,QAAM,SAA4B,CAAC;AACnC,aAAW,EAAE,WAAW,MAAM,KAAK,QAAQ;AACzC,UAAM,OAAO,KAAK,IAAI,MAAM,MAAM;AAClC,QAAI,CAAC,MAAM;AAGT,oBAAc,KAAK,SAAS;AAC5B;AAAA,IACF;AACA,UAAM,OAAwB,EAAE,WAAW,GAAG,MAAM;AACpD,QAAI,KAAK,cAAc,QAAW;AAAE,WAAK,YAAY,KAAK;AAAA,IAAW;AACrE,WAAO,KAAK,IAAI;AAAA,EAClB;AAEA,MAAI,eAAe,cAAc,SAAS,GAAG;AAC3C,UAAM,MAAM,aAAa,MAAM;AAC/B,SAAK,QAAQ;AAAA,MACX,cAAc,IAAI,CAAC,MAAM,SAAS,KAAK,KAAK,CAAC,CAAC;AAAA,IAChD,EAAE,MAAM,MAAM;AAAA,IAAiC,CAAC;AAAA,EAClD;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }