@mml-io/3d-web-experience-server 0.27.0 → 0.28.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.
@@ -1 +1 @@
1
- {"version":3,"file":"Networked3dWebExperienceServer.d.ts","sourceRoot":"","sources":["../src/Networked3dWebExperienceServer.ts"],"names":[],"mappings":"AAEA,OAAO,EAWL,KAAK,kBAAkB,EACxB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,QAAQ,EACR,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EAC1B,MAAM,gCAAgC,CAAC;AAGxC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,QAAQ,MAAM,YAAY,CAAC;AAGlC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG1D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,8BAA8B,CAC5B,GAAG,EAAE,OAAO,CAAC,OAAO,GACnB,OAAO,CAAC,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACjD,0BAA0B,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK;QACpD,EAAE,EAAE,MAAM,CAAC;KACZ,GAAG,IAAI,CAAC;IACT,mBAAmB,CAAC,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnF,eAAe,CACb,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,iCAAiC,CAAC,EAAE,QAAQ,GAC3C,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC;IAC9D,0BAA0B,CACxB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,kBAAkB,GAE9B,OAAO,CAAC,kBAAkB,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC,GACzD,kBAAkB,GAClB,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,CAAC;IACV,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E,MAAM,MAAM,oCAAoC,GAAG;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;QAEjC,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,wBAAwB,CAAC,EAAE,MAAM,CAAC;KACnC,CAAC;IACF,YAAY,CAAC,EAAE;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,CAAC,EAAE;QACX,kBAAkB,EAAE,MAAM,CAAC;QAC3B,sBAAsB,EAAE,MAAM,CAAC;QAC/B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,iBAAiB,EAAE,iBAAiB,CAAC;IACrC;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC,CAAC;AAqBF,qBAAa,8BAA8B;IAQ7B,OAAO,CAAC,MAAM;IAPnB,oBAAoB,EAAE,oBAAoB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IAE/C,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,uBAAuB,CAA6B;gBAExC,MAAM,EAAE,oCAAoC;IA8HhE;;OAEG;IACI,eAAe,CAAC,YAAY,EAAE,MAAM;IAM3C;;OAEG;IACI,aAAa,CAAC,OAAO,EAAE,OAAO;IAIrC;;;;;;OAMG;IACI,cAAc,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE;IAU5E,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ;IAK5D,OAAO,CAAC,KAAK,CAAC,EAAE,yBAAyB;IAShD;;;;;;;;OAQG;IACH,qBAAqB,CAAC,UAAU,EAAE,OAAO,CAAC,WAAW,GAAG,QAAQ,CAAC,WAAW;CAqF7E"}
1
+ {"version":3,"file":"Networked3dWebExperienceServer.d.ts","sourceRoot":"","sources":["../src/Networked3dWebExperienceServer.ts"],"names":[],"mappings":"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"}
package/build/index.js CHANGED
@@ -349,6 +349,10 @@ var Networked3dWebExperienceServer = class {
349
349
  res.redirect(result.redirect);
350
350
  return;
351
351
  }
352
+ if (req.accepts("json") && !req.accepts("html")) {
353
+ res.json({ sessionToken: result });
354
+ return;
355
+ }
352
356
  const authorizedDemoIndexContent = webClientServing.indexContent.replace(
353
357
  webClientServing.sessionTokenPlaceholder || defaultSessionTokenPlaceholder,
354
358
  escapeForJsString(result)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/MMLDocumentsServer.ts", "../src/Networked3dWebExperienceServer.ts", "../src/websocketDirectoryChangeListener.ts", "../src/auth/AnonymousAuthenticator.ts"],
4
- "sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport url from \"node:url\";\n\nimport { EditableNetworkedDOM, LocalObservableDOMFactory } from \"@mml-io/networked-dom-server\";\nimport { watch, FSWatcher } from \"chokidar\";\nimport micromatch from \"micromatch\";\nimport WebSocket from \"ws\";\n\nconst getMmlDocumentContent = (documentPath: string) => {\n return fs.readFileSync(documentPath, { encoding: \"utf8\", flag: \"r\" });\n};\n\nexport class MMLDocumentsServer {\n private documents = new Map<\n string,\n {\n documentPath: string;\n document: EditableNetworkedDOM;\n }\n >();\n private disposed = false;\n private watcher: FSWatcher;\n private watchPattern: string;\n\n constructor(\n private directory: string,\n watchPattern: string,\n ) {\n this.watchPattern = watchPattern;\n this.watch();\n }\n\n public dispose() {\n this.disposed = true;\n for (const { document } of this.documents.values()) {\n document.dispose();\n }\n this.documents.clear();\n this.watcher.close();\n }\n\n public handle(filename: string, ws: WebSocket) {\n const document = this.documents.get(filename)?.document;\n if (!document) {\n ws.close();\n return;\n }\n\n document.addWebSocket(ws as any);\n ws.on(\"close\", () => {\n if (!this.disposed) {\n document.removeWebSocket(ws as any);\n }\n });\n }\n\n private watch() {\n this.watcher = watch(this.directory, {\n ignoreInitial: false,\n ignored: (checkPath, stats) => {\n if (!stats || !stats.isFile()) {\n return false;\n }\n return !micromatch.isMatch(checkPath, this.watchPattern);\n },\n persistent: true,\n });\n this.watcher\n .on(\"add\", (fullPath, stats) => {\n if (!stats || !stats.isFile()) {\n return;\n }\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been added`);\n const contents = getMmlDocumentContent(fullPath);\n const document = new EditableNetworkedDOM(\n url.pathToFileURL(fullPath).toString(),\n LocalObservableDOMFactory,\n );\n document.load(contents);\n\n const currentData = {\n documentPath: fullPath,\n document,\n };\n this.documents.set(relativePath, currentData);\n })\n .on(\"change\", (fullPath) => {\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been changed`);\n const contents = getMmlDocumentContent(fullPath);\n const documentState = this.documents.get(relativePath);\n if (!documentState) {\n console.error(`MML Document '${relativePath}' not found`);\n return;\n }\n documentState.document.load(contents);\n })\n .on(\"unlink\", (fullPath) => {\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been removed`);\n const documentState = this.documents.get(relativePath);\n if (!documentState) {\n console.error(`MML Document '${relativePath}' not found`);\n return;\n }\n documentState.document.dispose();\n this.documents.delete(relativePath);\n })\n .on(\"error\", (error) => {\n console.error(\"Error whilst watching directory\", error);\n });\n }\n}\n", "import http from \"http\";\n\nimport {\n experienceProtocolToDeltaNetSubProtocol,\n FROM_CLIENT_CHAT_MESSAGE_TYPE,\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n handleExperienceWebsocketSubprotocol,\n MAX_CHAT_MESSAGE_LENGTH,\n parseClientChatMessage,\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n type ServerChatMessage,\n type SessionConfigPayload,\n type WorldConfigPayload,\n} from \"@mml-io/3d-web-experience-protocol\";\nimport {\n UserData,\n UserIdentityUpdate,\n UserNetworkingServer,\n UserNetworkingServerError,\n} from \"@mml-io/3d-web-user-networking\";\nimport { NetworkedDOM } from \"@mml-io/networked-dom-server\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport enableWs from \"express-ws\";\nimport ws from \"ws\";\n\nimport { MMLDocumentsServer } from \"./MMLDocumentsServer\";\nimport { websocketDirectoryChangeListener } from \"./websocketDirectoryChangeListener\";\n\nexport type UserAuthenticator = {\n generateAuthorizedSessionToken(\n req: express.Request,\n ): Promise<string | { redirect: string } | null>;\n getClientIdForSessionToken: (sessionToken: string) => {\n id: number;\n } | null;\n getSessionAuthToken?(sessionToken: string): string | null | Promise<string | null>;\n onClientConnect(\n connectionId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error;\n onClientUserIdentityUpdate(\n connectionId: number,\n userIdentity: UserIdentityUpdate,\n ):\n | Promise<UserIdentityUpdate | null | false | true | Error>\n | UserIdentityUpdate\n | null\n | false\n | true\n | Error;\n onClientDisconnect(connectionId: number): void;\n dispose?(): void;\n};\n\nexport const defaultSessionTokenPlaceholder = \"SESSION.TOKEN.PLACEHOLDER\";\n\nexport type Networked3dWebExperienceServerConfig = {\n networkPath: string;\n webClientServing?: {\n indexUrl: string;\n indexContent: string;\n sessionTokenPlaceholder?: string;\n\n clientBuildDir: string;\n clientUrl: string;\n clientWatchWebsocketPath?: string;\n };\n assetServing?: {\n assetsDir: string;\n assetsUrl: string;\n };\n mmlServing?: {\n documentsWatchPath: string;\n documentsDirectoryRoot: string;\n documentsUrl: string;\n };\n userAuthenticator: UserAuthenticator;\n /**\n * Whether to relay chat messages between clients. Defaults to true.\n */\n enableChat?: boolean;\n /**\n * Initial world config sent to each client after authentication.\n * See `UpdatableConfig` from `@mml-io/3d-web-experience-client` for the\n * full typed version consumed by the client.\n */\n worldConfig?: WorldConfigPayload;\n};\n\n/**\n * Escape a string for safe injection into a JavaScript string literal\n * inside a `<script>` block. All `<` characters are escaped to prevent\n * `</script>` and `<!--` sequences from interfering with HTML parsing.\n * U+2028 and U+2029 are escaped because they are valid in JSON but act\n * as line terminators in JavaScript string literals.\n */\nfunction escapeForJsString(str: string): string {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/'/g, \"\\\\'\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport class Networked3dWebExperienceServer {\n public userNetworkingServer: UserNetworkingServer;\n\n public mmlDocumentsServer?: MMLDocumentsServer;\n\n private worldConfig: WorldConfigPayload | undefined;\n private connectionSessionTokens = new Map<number, string>();\n\n constructor(private config: Networked3dWebExperienceServerConfig) {\n if (this.config.mmlServing) {\n const { documentsWatchPath, documentsDirectoryRoot } = this.config.mmlServing;\n this.mmlDocumentsServer = new MMLDocumentsServer(documentsDirectoryRoot, documentsWatchPath);\n }\n\n this.worldConfig = this.config.worldConfig;\n\n this.userNetworkingServer = new UserNetworkingServer({\n onClientConnect: (\n connectionId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error => {\n const result = this.config.userAuthenticator.onClientConnect(\n connectionId,\n sessionToken,\n userIdentityPresentedOnConnection,\n );\n if (result !== null && typeof result === \"object\" && \"then\" in result) {\n return (result as Promise<UserData | true | Error>).then((resolved) => {\n if (!(resolved instanceof Error)) {\n this.connectionSessionTokens.set(connectionId, sessionToken);\n }\n return resolved;\n });\n }\n if (!(result instanceof Error)) {\n this.connectionSessionTokens.set(connectionId, sessionToken);\n }\n return result;\n },\n onClientUserIdentityUpdate: (connectionId: number, userIdentity: UserIdentityUpdate) => {\n return this.config.userAuthenticator.onClientUserIdentityUpdate(connectionId, userIdentity);\n },\n onClientDisconnect: (connectionId: number): void => {\n this.connectionSessionTokens.delete(connectionId);\n this.config.userAuthenticator.onClientDisconnect(connectionId);\n },\n onClientAuthenticated: (connectionId: number): void => {\n // Send world config immediately \u2014 it does not depend on the session auth token\n if (this.worldConfig) {\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n JSON.stringify(this.worldConfig),\n );\n }\n\n // Send session config when the auth token resolves\n const sessionToken = this.connectionSessionTokens.get(connectionId);\n if (sessionToken && this.config.userAuthenticator.getSessionAuthToken) {\n const result = this.config.userAuthenticator.getSessionAuthToken(sessionToken);\n if (result !== null && typeof result === \"object\" && \"then\" in result) {\n const expectedToken = sessionToken;\n (result as Promise<string | null>).then(\n (token) => {\n if (\n token !== null &&\n this.connectionSessionTokens.get(connectionId) === expectedToken\n ) {\n const sessionConfig: SessionConfigPayload = { authToken: token };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n JSON.stringify(sessionConfig),\n );\n }\n },\n () => {\n // Auth token fetch failed \u2014 session config is optional\n },\n );\n } else {\n const token = result as string | null;\n if (token !== null) {\n const sessionConfig: SessionConfigPayload = { authToken: token };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n JSON.stringify(sessionConfig),\n );\n }\n }\n }\n },\n onCustomMessage: (connectionId: number, customType: number, contents: string): void => {\n if (customType === FROM_CLIENT_CHAT_MESSAGE_TYPE) {\n // When enableChat is explicitly set use that, otherwise fall back to\n // the value from worldConfig so that server relay and client UI agree.\n const chatEnabled = this.config.enableChat ?? this.worldConfig?.enableChat ?? true;\n if (!chatEnabled) {\n return;\n }\n const chatMessage = parseClientChatMessage(contents);\n if (chatMessage instanceof Error) {\n console.error(`Invalid chat message from connection ${connectionId}:`, chatMessage);\n // Notify the client that their message was rejected\n const errorPayload: ServerChatMessage = {\n fromConnectionId: 0,\n userId: \"\",\n message: \"[Server] Your message could not be delivered (invalid format).\",\n };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n JSON.stringify(errorPayload),\n );\n } else {\n const senderUser = this.userNetworkingServer.getAuthenticatedUser(connectionId);\n const serverChatMessage: ServerChatMessage = {\n fromConnectionId: connectionId,\n userId: senderUser?.userId ?? \"\",\n message: chatMessage.message.substring(0, MAX_CHAT_MESSAGE_LENGTH),\n };\n this.userNetworkingServer.broadcastMessage(\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n JSON.stringify(serverChatMessage),\n );\n }\n }\n },\n resolveProtocol: experienceProtocolToDeltaNetSubProtocol,\n });\n }\n\n /**\n * Replace the index HTML content served to new web clients.\n */\n public setIndexContent(indexContent: string) {\n if (this.config.webClientServing) {\n this.config.webClientServing.indexContent = indexContent;\n }\n }\n\n /**\n * Update whether chat is enabled at runtime.\n */\n public setEnableChat(enabled: boolean) {\n this.config.enableChat = enabled;\n }\n\n /**\n * Update the world config and optionally broadcast it to all connected clients.\n * Newly connecting clients will receive the updated config after authentication.\n *\n * By default the update is broadcast to all clients. Pass `{ broadcast: false }`\n * to update the stored config without notifying existing clients.\n */\n public setWorldConfig(config: WorldConfigPayload, options?: { broadcast?: boolean }) {\n this.worldConfig = config;\n if (options?.broadcast !== false) {\n this.userNetworkingServer.broadcastMessage(\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n JSON.stringify(this.worldConfig),\n );\n }\n }\n\n public updateUserCharacter(connectionId: number, userData: UserData) {\n console.log(`Initiate server-side update of connection ${connectionId}`);\n this.userNetworkingServer.updateUserCharacter(connectionId, userData);\n }\n\n public dispose(error?: UserNetworkingServerError) {\n this.userNetworkingServer.dispose(error);\n if (this.mmlDocumentsServer) {\n this.mmlDocumentsServer.dispose();\n }\n this.connectionSessionTokens.clear();\n this.config.userAuthenticator.dispose?.();\n }\n\n /**\n * Register all HTTP and WebSocket routes on the given Express application.\n *\n * Accepts either a plain `express.Application` or one that already has\n * `express-ws` applied. If WebSocket support has not been applied yet, this\n * method calls `enableWs()` internally with the required sub-protocol\n * handling. If the application already has a `.ws()` method (i.e. the caller\n * applied `express-ws` themselves), the existing setup is reused.\n */\n registerExpressRoutes(expressApp: express.Application | enableWs.Application) {\n const mmlDocumentsUrl = this.config.mmlServing?.documentsUrl;\n\n // If the caller already applied express-ws, reuse it; otherwise apply it\n // ourselves with the required handleProtocols configuration.\n let app: enableWs.Application;\n if (typeof (expressApp as enableWs.Application).ws === \"function\") {\n app = expressApp as enableWs.Application;\n } else {\n ({ app } = enableWs(expressApp, undefined, {\n wsOptions: {\n handleProtocols: (protocols: Set<string>, request: http.IncomingMessage) => {\n if (mmlDocumentsUrl && request.url?.startsWith(mmlDocumentsUrl)) {\n return NetworkedDOM.handleWebsocketSubprotocol(protocols);\n }\n return handleExperienceWebsocketSubprotocol(protocols);\n },\n },\n }));\n }\n\n app.ws(this.config.networkPath, (ws) => {\n this.userNetworkingServer.connectClient(ws as unknown as WebSocket);\n });\n\n const webClientServing = this.config.webClientServing;\n if (webClientServing) {\n app.get(webClientServing.indexUrl, async (req: express.Request, res: express.Response) => {\n const result = await this.config.userAuthenticator.generateAuthorizedSessionToken(req);\n if (result === null) {\n res.status(403).send(\"Access denied: authentication required\");\n return;\n }\n if (typeof result === \"object\" && \"redirect\" in result) {\n try {\n const redirectUrl = new URL(result.redirect);\n if (redirectUrl.protocol !== \"http:\" && redirectUrl.protocol !== \"https:\") {\n console.error(\"Redirect URL has disallowed scheme:\", result.redirect);\n res.send(\"Error: Invalid redirect URL\");\n return;\n }\n } catch {\n console.error(\"Invalid redirect URL from authenticator:\", result.redirect);\n res.send(\"Error: Invalid redirect URL\");\n return;\n }\n res.redirect(result.redirect);\n return;\n }\n const authorizedDemoIndexContent = webClientServing.indexContent.replace(\n webClientServing.sessionTokenPlaceholder || defaultSessionTokenPlaceholder,\n escapeForJsString(result),\n );\n res.send(authorizedDemoIndexContent);\n });\n\n app.use(webClientServing.clientUrl, express.static(webClientServing.clientBuildDir));\n if (webClientServing.clientWatchWebsocketPath) {\n websocketDirectoryChangeListener(app, {\n directory: webClientServing.clientBuildDir,\n websocketPath: webClientServing.clientWatchWebsocketPath,\n });\n }\n }\n\n const mmlDocumentsServer = this.mmlDocumentsServer;\n const mmlServing = this.config.mmlServing;\n // Handle example document sockets\n if (mmlServing && mmlDocumentsServer) {\n app.ws(`${mmlServing.documentsUrl}*`, (ws: ws.WebSocket, req: express.Request) => {\n const path = req.params[0];\n console.log(\"document requested\", { path });\n mmlDocumentsServer.handle(path, ws);\n });\n }\n\n if (this.config.assetServing) {\n // Serve assets with CORS allowing all origins\n app.use(\n this.config.assetServing.assetsUrl,\n cors(),\n express.static(this.config.assetServing.assetsDir),\n );\n }\n }\n}\n", "import { watch } from \"chokidar\";\nimport enableWs from \"express-ws\";\nimport WebSocket from \"ws\";\n\nexport function websocketDirectoryChangeListener(\n app: enableWs.Application,\n options: {\n directory: string;\n websocketPath: string;\n },\n) {\n const listeningClients = new Set<WebSocket>();\n watch(options.directory).on(\"all\", () => {\n for (const client of listeningClients) {\n client.send(\"change\");\n }\n });\n // Create an event-source that updates whenever the build folder gets modified\n app.ws(options.websocketPath, (ws: WebSocket) => {\n listeningClients.add(ws);\n ws.on(\"close\", () => {\n listeningClients.delete(ws);\n });\n });\n}\n", "import crypto from \"crypto\";\n\nimport type {\n CharacterDescription,\n UserData,\n UserIdentityUpdate,\n} from \"@mml-io/3d-web-user-networking\";\n\nimport type { UserAuthenticator } from \"../Networked3dWebExperienceServer\";\n\nconst TOKEN_EXPIRY_MS = 5 * 60 * 1000; // Unused tokens expire after 5 minutes\nconst TOKEN_CLEANUP_INTERVAL_MS = 60 * 1000; // Clean up every minute\n\nexport type AnonymousAuthenticatorOptions = {\n /** Character descriptions to randomly assign to connecting users. */\n defaultCharacterDescriptions?: CharacterDescription[];\n};\n\n/**\n * Stateless anonymous authenticator. Every connection gets a random username\n * and avatar. No database required.\n */\nexport class AnonymousAuthenticator implements UserAuthenticator {\n private usersByToken = new Map<\n string,\n { id: number; userData: UserData; connectionId: number | null; createdAt: number }\n >();\n private connectionIdToToken = new Map<number, string>();\n private nextId = 1;\n private characterDescriptions: CharacterDescription[];\n private cleanupInterval: ReturnType<typeof setInterval>;\n\n constructor(options?: AnonymousAuthenticatorOptions) {\n this.characterDescriptions = options?.defaultCharacterDescriptions ?? [];\n\n // Periodically clean up tokens that were never used to connect.\n // Use unref() so this interval doesn't prevent the process from exiting\n // if dispose() is not called.\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [token, entry] of this.usersByToken) {\n if (entry.connectionId === null && now - entry.createdAt > TOKEN_EXPIRY_MS) {\n this.usersByToken.delete(token);\n }\n }\n }, TOKEN_CLEANUP_INTERVAL_MS);\n this.cleanupInterval.unref();\n }\n\n private randomCharacterDescription(): CharacterDescription | undefined {\n if (this.characterDescriptions.length === 0) return undefined;\n return this.characterDescriptions[\n Math.floor(Math.random() * this.characterDescriptions.length)\n ];\n }\n\n async generateAuthorizedSessionToken(): Promise<string> {\n const token = crypto.randomBytes(20).toString(\"hex\");\n const id = this.nextId++;\n const characterDescription = this.randomCharacterDescription();\n const userData: UserData = {\n userId: crypto.randomUUID(),\n username: `User ${id}`,\n characterDescription: characterDescription ?? null,\n colors: null,\n };\n this.usersByToken.set(token, { id, userData, connectionId: null, createdAt: Date.now() });\n return token;\n }\n\n getClientIdForSessionToken(sessionToken: string): { id: number } | null {\n const entry = this.usersByToken.get(sessionToken);\n if (!entry) return null;\n return { id: entry.id };\n }\n\n onClientConnect(\n connectionId: number,\n sessionToken: string,\n _userIdentityPresentedOnConnection?: UserData,\n ): UserData | true | Error {\n const entry = this.usersByToken.get(sessionToken);\n if (!entry) {\n return new Error(\"Invalid session token\");\n }\n if (entry.connectionId !== null) {\n return new Error(\"Session token already connected\");\n }\n entry.connectionId = connectionId;\n this.connectionIdToToken.set(connectionId, sessionToken);\n return entry.userData;\n }\n\n onClientUserIdentityUpdate(\n _connectionId: number,\n userIdentity: UserIdentityUpdate,\n ): UserIdentityUpdate | true | Error {\n return userIdentity;\n }\n\n onClientDisconnect(connectionId: number): void {\n const token = this.connectionIdToToken.get(connectionId);\n if (token) {\n this.usersByToken.delete(token);\n this.connectionIdToToken.delete(connectionId);\n }\n }\n\n getSessionAuthToken(_sessionToken: string): string | null {\n return null;\n }\n\n dispose(): void {\n clearInterval(this.cleanupInterval);\n this.usersByToken.clear();\n this.connectionIdToToken.clear();\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport url from \"node:url\";\n\nimport { EditableNetworkedDOM, LocalObservableDOMFactory } from \"@mml-io/networked-dom-server\";\nimport { watch, FSWatcher } from \"chokidar\";\nimport micromatch from \"micromatch\";\nimport WebSocket from \"ws\";\n\nconst getMmlDocumentContent = (documentPath: string) => {\n return fs.readFileSync(documentPath, { encoding: \"utf8\", flag: \"r\" });\n};\n\nexport class MMLDocumentsServer {\n private documents = new Map<\n string,\n {\n documentPath: string;\n document: EditableNetworkedDOM;\n }\n >();\n private disposed = false;\n private watcher: FSWatcher;\n private watchPattern: string;\n\n constructor(\n private directory: string,\n watchPattern: string,\n ) {\n this.watchPattern = watchPattern;\n this.watch();\n }\n\n public dispose() {\n this.disposed = true;\n for (const { document } of this.documents.values()) {\n document.dispose();\n }\n this.documents.clear();\n this.watcher.close();\n }\n\n public handle(filename: string, ws: WebSocket) {\n const document = this.documents.get(filename)?.document;\n if (!document) {\n ws.close();\n return;\n }\n\n document.addWebSocket(ws as any);\n ws.on(\"close\", () => {\n if (!this.disposed) {\n document.removeWebSocket(ws as any);\n }\n });\n }\n\n private watch() {\n this.watcher = watch(this.directory, {\n ignoreInitial: false,\n ignored: (checkPath, stats) => {\n if (!stats || !stats.isFile()) {\n return false;\n }\n return !micromatch.isMatch(checkPath, this.watchPattern);\n },\n persistent: true,\n });\n this.watcher\n .on(\"add\", (fullPath, stats) => {\n if (!stats || !stats.isFile()) {\n return;\n }\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been added`);\n const contents = getMmlDocumentContent(fullPath);\n const document = new EditableNetworkedDOM(\n url.pathToFileURL(fullPath).toString(),\n LocalObservableDOMFactory,\n );\n document.load(contents);\n\n const currentData = {\n documentPath: fullPath,\n document,\n };\n this.documents.set(relativePath, currentData);\n })\n .on(\"change\", (fullPath) => {\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been changed`);\n const contents = getMmlDocumentContent(fullPath);\n const documentState = this.documents.get(relativePath);\n if (!documentState) {\n console.error(`MML Document '${relativePath}' not found`);\n return;\n }\n documentState.document.load(contents);\n })\n .on(\"unlink\", (fullPath) => {\n const relativePath = path.relative(this.directory, fullPath);\n console.log(`MML Document '${relativePath}' has been removed`);\n const documentState = this.documents.get(relativePath);\n if (!documentState) {\n console.error(`MML Document '${relativePath}' not found`);\n return;\n }\n documentState.document.dispose();\n this.documents.delete(relativePath);\n })\n .on(\"error\", (error) => {\n console.error(\"Error whilst watching directory\", error);\n });\n }\n}\n", "import http from \"http\";\n\nimport {\n experienceProtocolToDeltaNetSubProtocol,\n FROM_CLIENT_CHAT_MESSAGE_TYPE,\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n handleExperienceWebsocketSubprotocol,\n MAX_CHAT_MESSAGE_LENGTH,\n parseClientChatMessage,\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n type ServerChatMessage,\n type SessionConfigPayload,\n type WorldConfigPayload,\n} from \"@mml-io/3d-web-experience-protocol\";\nimport {\n UserData,\n UserIdentityUpdate,\n UserNetworkingServer,\n UserNetworkingServerError,\n} from \"@mml-io/3d-web-user-networking\";\nimport { NetworkedDOM } from \"@mml-io/networked-dom-server\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport enableWs from \"express-ws\";\nimport ws from \"ws\";\n\nimport { MMLDocumentsServer } from \"./MMLDocumentsServer\";\nimport { websocketDirectoryChangeListener } from \"./websocketDirectoryChangeListener\";\n\nexport type UserAuthenticator = {\n generateAuthorizedSessionToken(\n req: express.Request,\n ): Promise<string | { redirect: string } | null>;\n getClientIdForSessionToken: (sessionToken: string) => {\n id: number;\n } | null;\n getSessionAuthToken?(sessionToken: string): string | null | Promise<string | null>;\n onClientConnect(\n connectionId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error;\n onClientUserIdentityUpdate(\n connectionId: number,\n userIdentity: UserIdentityUpdate,\n ):\n | Promise<UserIdentityUpdate | null | false | true | Error>\n | UserIdentityUpdate\n | null\n | false\n | true\n | Error;\n onClientDisconnect(connectionId: number): void;\n dispose?(): void;\n};\n\nexport const defaultSessionTokenPlaceholder = \"SESSION.TOKEN.PLACEHOLDER\";\n\nexport type Networked3dWebExperienceServerConfig = {\n networkPath: string;\n webClientServing?: {\n indexUrl: string;\n indexContent: string;\n sessionTokenPlaceholder?: string;\n\n clientBuildDir: string;\n clientUrl: string;\n clientWatchWebsocketPath?: string;\n };\n assetServing?: {\n assetsDir: string;\n assetsUrl: string;\n };\n mmlServing?: {\n documentsWatchPath: string;\n documentsDirectoryRoot: string;\n documentsUrl: string;\n };\n userAuthenticator: UserAuthenticator;\n /**\n * Whether to relay chat messages between clients. Defaults to true.\n */\n enableChat?: boolean;\n /**\n * Initial world config sent to each client after authentication.\n * See `UpdatableConfig` from `@mml-io/3d-web-experience-client` for the\n * full typed version consumed by the client.\n */\n worldConfig?: WorldConfigPayload;\n};\n\n/**\n * Escape a string for safe injection into a JavaScript string literal\n * inside a `<script>` block. All `<` characters are escaped to prevent\n * `</script>` and `<!--` sequences from interfering with HTML parsing.\n * U+2028 and U+2029 are escaped because they are valid in JSON but act\n * as line terminators in JavaScript string literals.\n */\nfunction escapeForJsString(str: string): string {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/'/g, \"\\\\'\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport class Networked3dWebExperienceServer {\n public userNetworkingServer: UserNetworkingServer;\n\n public mmlDocumentsServer?: MMLDocumentsServer;\n\n private worldConfig: WorldConfigPayload | undefined;\n private connectionSessionTokens = new Map<number, string>();\n\n constructor(private config: Networked3dWebExperienceServerConfig) {\n if (this.config.mmlServing) {\n const { documentsWatchPath, documentsDirectoryRoot } = this.config.mmlServing;\n this.mmlDocumentsServer = new MMLDocumentsServer(documentsDirectoryRoot, documentsWatchPath);\n }\n\n this.worldConfig = this.config.worldConfig;\n\n this.userNetworkingServer = new UserNetworkingServer({\n onClientConnect: (\n connectionId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error => {\n const result = this.config.userAuthenticator.onClientConnect(\n connectionId,\n sessionToken,\n userIdentityPresentedOnConnection,\n );\n if (result !== null && typeof result === \"object\" && \"then\" in result) {\n return (result as Promise<UserData | true | Error>).then((resolved) => {\n if (!(resolved instanceof Error)) {\n this.connectionSessionTokens.set(connectionId, sessionToken);\n }\n return resolved;\n });\n }\n if (!(result instanceof Error)) {\n this.connectionSessionTokens.set(connectionId, sessionToken);\n }\n return result;\n },\n onClientUserIdentityUpdate: (connectionId: number, userIdentity: UserIdentityUpdate) => {\n return this.config.userAuthenticator.onClientUserIdentityUpdate(connectionId, userIdentity);\n },\n onClientDisconnect: (connectionId: number): void => {\n this.connectionSessionTokens.delete(connectionId);\n this.config.userAuthenticator.onClientDisconnect(connectionId);\n },\n onClientAuthenticated: (connectionId: number): void => {\n // Send world config immediately \u2014 it does not depend on the session auth token\n if (this.worldConfig) {\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n JSON.stringify(this.worldConfig),\n );\n }\n\n // Send session config when the auth token resolves\n const sessionToken = this.connectionSessionTokens.get(connectionId);\n if (sessionToken && this.config.userAuthenticator.getSessionAuthToken) {\n const result = this.config.userAuthenticator.getSessionAuthToken(sessionToken);\n if (result !== null && typeof result === \"object\" && \"then\" in result) {\n const expectedToken = sessionToken;\n (result as Promise<string | null>).then(\n (token) => {\n if (\n token !== null &&\n this.connectionSessionTokens.get(connectionId) === expectedToken\n ) {\n const sessionConfig: SessionConfigPayload = { authToken: token };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n JSON.stringify(sessionConfig),\n );\n }\n },\n () => {\n // Auth token fetch failed \u2014 session config is optional\n },\n );\n } else {\n const token = result as string | null;\n if (token !== null) {\n const sessionConfig: SessionConfigPayload = { authToken: token };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,\n JSON.stringify(sessionConfig),\n );\n }\n }\n }\n },\n onCustomMessage: (connectionId: number, customType: number, contents: string): void => {\n if (customType === FROM_CLIENT_CHAT_MESSAGE_TYPE) {\n // When enableChat is explicitly set use that, otherwise fall back to\n // the value from worldConfig so that server relay and client UI agree.\n const chatEnabled = this.config.enableChat ?? this.worldConfig?.enableChat ?? true;\n if (!chatEnabled) {\n return;\n }\n const chatMessage = parseClientChatMessage(contents);\n if (chatMessage instanceof Error) {\n console.error(`Invalid chat message from connection ${connectionId}:`, chatMessage);\n // Notify the client that their message was rejected\n const errorPayload: ServerChatMessage = {\n fromConnectionId: 0,\n userId: \"\",\n message: \"[Server] Your message could not be delivered (invalid format).\",\n };\n this.userNetworkingServer.sendCustomMessageToClient(\n connectionId,\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n JSON.stringify(errorPayload),\n );\n } else {\n const senderUser = this.userNetworkingServer.getAuthenticatedUser(connectionId);\n const serverChatMessage: ServerChatMessage = {\n fromConnectionId: connectionId,\n userId: senderUser?.userId ?? \"\",\n message: chatMessage.message.substring(0, MAX_CHAT_MESSAGE_LENGTH),\n };\n this.userNetworkingServer.broadcastMessage(\n FROM_SERVER_CHAT_MESSAGE_TYPE,\n JSON.stringify(serverChatMessage),\n );\n }\n }\n },\n resolveProtocol: experienceProtocolToDeltaNetSubProtocol,\n });\n }\n\n /**\n * Replace the index HTML content served to new web clients.\n */\n public setIndexContent(indexContent: string) {\n if (this.config.webClientServing) {\n this.config.webClientServing.indexContent = indexContent;\n }\n }\n\n /**\n * Update whether chat is enabled at runtime.\n */\n public setEnableChat(enabled: boolean) {\n this.config.enableChat = enabled;\n }\n\n /**\n * Update the world config and optionally broadcast it to all connected clients.\n * Newly connecting clients will receive the updated config after authentication.\n *\n * By default the update is broadcast to all clients. Pass `{ broadcast: false }`\n * to update the stored config without notifying existing clients.\n */\n public setWorldConfig(config: WorldConfigPayload, options?: { broadcast?: boolean }) {\n this.worldConfig = config;\n if (options?.broadcast !== false) {\n this.userNetworkingServer.broadcastMessage(\n FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,\n JSON.stringify(this.worldConfig),\n );\n }\n }\n\n public updateUserCharacter(connectionId: number, userData: UserData) {\n console.log(`Initiate server-side update of connection ${connectionId}`);\n this.userNetworkingServer.updateUserCharacter(connectionId, userData);\n }\n\n public dispose(error?: UserNetworkingServerError) {\n this.userNetworkingServer.dispose(error);\n if (this.mmlDocumentsServer) {\n this.mmlDocumentsServer.dispose();\n }\n this.connectionSessionTokens.clear();\n this.config.userAuthenticator.dispose?.();\n }\n\n /**\n * Register all HTTP and WebSocket routes on the given Express application.\n *\n * Accepts either a plain `express.Application` or one that already has\n * `express-ws` applied. If WebSocket support has not been applied yet, this\n * method calls `enableWs()` internally with the required sub-protocol\n * handling. If the application already has a `.ws()` method (i.e. the caller\n * applied `express-ws` themselves), the existing setup is reused.\n */\n registerExpressRoutes(expressApp: express.Application | enableWs.Application) {\n const mmlDocumentsUrl = this.config.mmlServing?.documentsUrl;\n\n // If the caller already applied express-ws, reuse it; otherwise apply it\n // ourselves with the required handleProtocols configuration.\n let app: enableWs.Application;\n if (typeof (expressApp as enableWs.Application).ws === \"function\") {\n app = expressApp as enableWs.Application;\n } else {\n ({ app } = enableWs(expressApp, undefined, {\n wsOptions: {\n handleProtocols: (protocols: Set<string>, request: http.IncomingMessage) => {\n if (mmlDocumentsUrl && request.url?.startsWith(mmlDocumentsUrl)) {\n return NetworkedDOM.handleWebsocketSubprotocol(protocols);\n }\n return handleExperienceWebsocketSubprotocol(protocols);\n },\n },\n }));\n }\n\n app.ws(this.config.networkPath, (ws) => {\n this.userNetworkingServer.connectClient(ws as unknown as WebSocket);\n });\n\n const webClientServing = this.config.webClientServing;\n if (webClientServing) {\n app.get(webClientServing.indexUrl, async (req: express.Request, res: express.Response) => {\n const result = await this.config.userAuthenticator.generateAuthorizedSessionToken(req);\n if (result === null) {\n res.status(403).send(\"Access denied: authentication required\");\n return;\n }\n if (typeof result === \"object\" && \"redirect\" in result) {\n try {\n const redirectUrl = new URL(result.redirect);\n if (redirectUrl.protocol !== \"http:\" && redirectUrl.protocol !== \"https:\") {\n console.error(\"Redirect URL has disallowed scheme:\", result.redirect);\n res.send(\"Error: Invalid redirect URL\");\n return;\n }\n } catch {\n console.error(\"Invalid redirect URL from authenticator:\", result.redirect);\n res.send(\"Error: Invalid redirect URL\");\n return;\n }\n res.redirect(result.redirect);\n return;\n }\n // Content negotiation: return JSON for programmatic clients (bridges, bots),\n // HTML for browsers. Same auth path either way.\n if (req.accepts(\"json\") && !req.accepts(\"html\")) {\n res.json({ sessionToken: result });\n return;\n }\n const authorizedDemoIndexContent = webClientServing.indexContent.replace(\n webClientServing.sessionTokenPlaceholder || defaultSessionTokenPlaceholder,\n escapeForJsString(result),\n );\n res.send(authorizedDemoIndexContent);\n });\n\n app.use(webClientServing.clientUrl, express.static(webClientServing.clientBuildDir));\n if (webClientServing.clientWatchWebsocketPath) {\n websocketDirectoryChangeListener(app, {\n directory: webClientServing.clientBuildDir,\n websocketPath: webClientServing.clientWatchWebsocketPath,\n });\n }\n }\n\n const mmlDocumentsServer = this.mmlDocumentsServer;\n const mmlServing = this.config.mmlServing;\n // Handle example document sockets\n if (mmlServing && mmlDocumentsServer) {\n app.ws(`${mmlServing.documentsUrl}*`, (ws: ws.WebSocket, req: express.Request) => {\n const path = req.params[0];\n console.log(\"document requested\", { path });\n mmlDocumentsServer.handle(path, ws);\n });\n }\n\n if (this.config.assetServing) {\n // Serve assets with CORS allowing all origins\n app.use(\n this.config.assetServing.assetsUrl,\n cors(),\n express.static(this.config.assetServing.assetsDir),\n );\n }\n }\n}\n", "import { watch } from \"chokidar\";\nimport enableWs from \"express-ws\";\nimport WebSocket from \"ws\";\n\nexport function websocketDirectoryChangeListener(\n app: enableWs.Application,\n options: {\n directory: string;\n websocketPath: string;\n },\n) {\n const listeningClients = new Set<WebSocket>();\n watch(options.directory).on(\"all\", () => {\n for (const client of listeningClients) {\n client.send(\"change\");\n }\n });\n // Create an event-source that updates whenever the build folder gets modified\n app.ws(options.websocketPath, (ws: WebSocket) => {\n listeningClients.add(ws);\n ws.on(\"close\", () => {\n listeningClients.delete(ws);\n });\n });\n}\n", "import crypto from \"crypto\";\n\nimport type {\n CharacterDescription,\n UserData,\n UserIdentityUpdate,\n} from \"@mml-io/3d-web-user-networking\";\n\nimport type { UserAuthenticator } from \"../Networked3dWebExperienceServer\";\n\nconst TOKEN_EXPIRY_MS = 5 * 60 * 1000; // Unused tokens expire after 5 minutes\nconst TOKEN_CLEANUP_INTERVAL_MS = 60 * 1000; // Clean up every minute\n\nexport type AnonymousAuthenticatorOptions = {\n /** Character descriptions to randomly assign to connecting users. */\n defaultCharacterDescriptions?: CharacterDescription[];\n};\n\n/**\n * Stateless anonymous authenticator. Every connection gets a random username\n * and avatar. No database required.\n */\nexport class AnonymousAuthenticator implements UserAuthenticator {\n private usersByToken = new Map<\n string,\n { id: number; userData: UserData; connectionId: number | null; createdAt: number }\n >();\n private connectionIdToToken = new Map<number, string>();\n private nextId = 1;\n private characterDescriptions: CharacterDescription[];\n private cleanupInterval: ReturnType<typeof setInterval>;\n\n constructor(options?: AnonymousAuthenticatorOptions) {\n this.characterDescriptions = options?.defaultCharacterDescriptions ?? [];\n\n // Periodically clean up tokens that were never used to connect.\n // Use unref() so this interval doesn't prevent the process from exiting\n // if dispose() is not called.\n this.cleanupInterval = setInterval(() => {\n const now = Date.now();\n for (const [token, entry] of this.usersByToken) {\n if (entry.connectionId === null && now - entry.createdAt > TOKEN_EXPIRY_MS) {\n this.usersByToken.delete(token);\n }\n }\n }, TOKEN_CLEANUP_INTERVAL_MS);\n this.cleanupInterval.unref();\n }\n\n private randomCharacterDescription(): CharacterDescription | undefined {\n if (this.characterDescriptions.length === 0) return undefined;\n return this.characterDescriptions[\n Math.floor(Math.random() * this.characterDescriptions.length)\n ];\n }\n\n async generateAuthorizedSessionToken(): Promise<string> {\n const token = crypto.randomBytes(20).toString(\"hex\");\n const id = this.nextId++;\n const characterDescription = this.randomCharacterDescription();\n const userData: UserData = {\n userId: crypto.randomUUID(),\n username: `User ${id}`,\n characterDescription: characterDescription ?? null,\n colors: null,\n };\n this.usersByToken.set(token, { id, userData, connectionId: null, createdAt: Date.now() });\n return token;\n }\n\n getClientIdForSessionToken(sessionToken: string): { id: number } | null {\n const entry = this.usersByToken.get(sessionToken);\n if (!entry) return null;\n return { id: entry.id };\n }\n\n onClientConnect(\n connectionId: number,\n sessionToken: string,\n _userIdentityPresentedOnConnection?: UserData,\n ): UserData | true | Error {\n const entry = this.usersByToken.get(sessionToken);\n if (!entry) {\n return new Error(\"Invalid session token\");\n }\n if (entry.connectionId !== null) {\n return new Error(\"Session token already connected\");\n }\n entry.connectionId = connectionId;\n this.connectionIdToToken.set(connectionId, sessionToken);\n return entry.userData;\n }\n\n onClientUserIdentityUpdate(\n _connectionId: number,\n userIdentity: UserIdentityUpdate,\n ): UserIdentityUpdate | true | Error {\n return userIdentity;\n }\n\n onClientDisconnect(connectionId: number): void {\n const token = this.connectionIdToToken.get(connectionId);\n if (token) {\n this.usersByToken.delete(token);\n this.connectionIdToToken.delete(connectionId);\n }\n }\n\n getSessionAuthToken(_sessionToken: string): string | null {\n return null;\n }\n\n dispose(): void {\n clearInterval(this.cleanupInterval);\n this.usersByToken.clear();\n this.connectionIdToToken.clear();\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["watch", "_a", "path"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mml-io/3d-web-experience-server",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -25,8 +25,8 @@
25
25
  "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
26
26
  },
27
27
  "dependencies": {
28
- "@mml-io/3d-web-experience-protocol": "^0.27.0",
29
- "@mml-io/3d-web-user-networking": "^0.27.0",
28
+ "@mml-io/3d-web-experience-protocol": "^0.28.0",
29
+ "@mml-io/3d-web-user-networking": "^0.28.0",
30
30
  "@mml-io/networked-dom-server": "0.26.1",
31
31
  "chokidar": "4.0.3",
32
32
  "cors": "^2.8.5",
@@ -48,5 +48,5 @@
48
48
  "jest-junit": "16.0.0",
49
49
  "ts-jest": "^29.1.2"
50
50
  },
51
- "gitHead": "4038d512f99c0700d77337a0daa6f49b281dce87"
51
+ "gitHead": "9c1c1d2601468e8ecc4d9b4d2907ba55151248a3"
52
52
  }