@mml-io/3d-web-experience-server 0.26.0 → 0.27.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.
@@ -2,6 +2,7 @@ import WebSocket from "ws";
2
2
  export declare class MMLDocumentsServer {
3
3
  private directory;
4
4
  private documents;
5
+ private disposed;
5
6
  private watcher;
6
7
  private watchPattern;
7
8
  constructor(directory: string, watchPattern: string);
@@ -1 +1 @@
1
- {"version":3,"file":"MMLDocumentsServer.d.ts","sourceRoot":"","sources":["../src/MMLDocumentsServer.ts"],"names":[],"mappings":"AAOA,OAAO,SAAS,MAAM,IAAI,CAAC;AAM3B,qBAAa,kBAAkB;IAY3B,OAAO,CAAC,SAAS;IAXnB,OAAO,CAAC,SAAS,CAMb;IACJ,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,YAAY,CAAS;gBAGnB,SAAS,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM;IAMf,OAAO;IAQP,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS;IAa7C,OAAO,CAAC,KAAK;CAyDd"}
1
+ {"version":3,"file":"MMLDocumentsServer.d.ts","sourceRoot":"","sources":["../src/MMLDocumentsServer.ts"],"names":[],"mappings":"AAOA,OAAO,SAAS,MAAM,IAAI,CAAC;AAM3B,qBAAa,kBAAkB;IAa3B,OAAO,CAAC,SAAS;IAZnB,OAAO,CAAC,SAAS,CAMb;IACJ,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,YAAY,CAAS;gBAGnB,SAAS,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM;IAMf,OAAO;IASP,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS;IAe7C,OAAO,CAAC,KAAK;CAyDd"}
@@ -1,20 +1,25 @@
1
- import { UserData, UserNetworkingServer, UserNetworkingServerError } from "@mml-io/3d-web-user-networking";
1
+ import { type WorldConfigPayload } from "@mml-io/3d-web-experience-protocol";
2
+ import { UserData, UserIdentityUpdate, UserNetworkingServer, UserNetworkingServerError } from "@mml-io/3d-web-user-networking";
2
3
  import express from "express";
3
4
  import enableWs from "express-ws";
4
5
  import { MMLDocumentsServer } from "./MMLDocumentsServer";
5
- type UserAuthenticator = {
6
- generateAuthorizedSessionToken(req: express.Request): Promise<string | null>;
6
+ export type UserAuthenticator = {
7
+ generateAuthorizedSessionToken(req: express.Request): Promise<string | {
8
+ redirect: string;
9
+ } | null>;
7
10
  getClientIdForSessionToken: (sessionToken: string) => {
8
11
  id: number;
9
12
  } | null;
10
- onClientConnect(clientId: number, sessionToken: string, userIdentityPresentedOnConnection?: UserData): Promise<UserData | true | Error> | UserData | true | Error;
11
- onClientUserIdentityUpdate(clientId: number, userIdentity: UserData): UserData | true | Error;
12
- onClientDisconnect(clientId: number): void;
13
+ getSessionAuthToken?(sessionToken: string): string | null | Promise<string | null>;
14
+ onClientConnect(connectionId: number, sessionToken: string, userIdentityPresentedOnConnection?: UserData): Promise<UserData | true | Error> | UserData | true | Error;
15
+ onClientUserIdentityUpdate(connectionId: number, userIdentity: UserIdentityUpdate): Promise<UserIdentityUpdate | null | false | true | Error> | UserIdentityUpdate | null | false | true | Error;
16
+ onClientDisconnect(connectionId: number): void;
17
+ dispose?(): void;
13
18
  };
14
19
  export declare const defaultSessionTokenPlaceholder = "SESSION.TOKEN.PLACEHOLDER";
15
20
  export type Networked3dWebExperienceServerConfig = {
16
21
  networkPath: string;
17
- webClientServing: {
22
+ webClientServing?: {
18
23
  indexUrl: string;
19
24
  indexContent: string;
20
25
  sessionTokenPlaceholder?: string;
@@ -22,7 +27,6 @@ export type Networked3dWebExperienceServerConfig = {
22
27
  clientUrl: string;
23
28
  clientWatchWebsocketPath?: string;
24
29
  };
25
- enableChat?: boolean;
26
30
  assetServing?: {
27
31
  assetsDir: string;
28
32
  assetsUrl: string;
@@ -33,15 +37,53 @@ export type Networked3dWebExperienceServerConfig = {
33
37
  documentsUrl: string;
34
38
  };
35
39
  userAuthenticator: UserAuthenticator;
40
+ /**
41
+ * Whether to relay chat messages between clients. Defaults to true.
42
+ */
43
+ enableChat?: boolean;
44
+ /**
45
+ * Initial world config sent to each client after authentication.
46
+ * See `UpdatableConfig` from `@mml-io/3d-web-experience-client` for the
47
+ * full typed version consumed by the client.
48
+ */
49
+ worldConfig?: WorldConfigPayload;
36
50
  };
37
51
  export declare class Networked3dWebExperienceServer {
38
52
  private config;
39
53
  userNetworkingServer: UserNetworkingServer;
40
54
  mmlDocumentsServer?: MMLDocumentsServer;
55
+ private worldConfig;
56
+ private connectionSessionTokens;
41
57
  constructor(config: Networked3dWebExperienceServerConfig);
42
- updateUserCharacter(clientId: number, userData: UserData): void;
58
+ /**
59
+ * Replace the index HTML content served to new web clients.
60
+ */
61
+ setIndexContent(indexContent: string): void;
62
+ /**
63
+ * Update whether chat is enabled at runtime.
64
+ */
65
+ setEnableChat(enabled: boolean): void;
66
+ /**
67
+ * Update the world config and optionally broadcast it to all connected clients.
68
+ * Newly connecting clients will receive the updated config after authentication.
69
+ *
70
+ * By default the update is broadcast to all clients. Pass `{ broadcast: false }`
71
+ * to update the stored config without notifying existing clients.
72
+ */
73
+ setWorldConfig(config: WorldConfigPayload, options?: {
74
+ broadcast?: boolean;
75
+ }): void;
76
+ updateUserCharacter(connectionId: number, userData: UserData): void;
43
77
  dispose(error?: UserNetworkingServerError): void;
44
- registerExpressRoutes(app: enableWs.Application): void;
78
+ /**
79
+ * Register all HTTP and WebSocket routes on the given Express application.
80
+ *
81
+ * Accepts either a plain `express.Application` or one that already has
82
+ * `express-ws` applied. If WebSocket support has not been applied yet, this
83
+ * method calls `enableWs()` internally with the required sub-protocol
84
+ * handling. If the application already has a `.ws()` method (i.e. the caller
85
+ * applied `express-ws` themselves), the existing setup is reused.
86
+ */
87
+ registerExpressRoutes(expressApp: express.Application | enableWs.Application): void;
45
88
  }
46
- export {};
47
89
  //# sourceMappingURL=Networked3dWebExperienceServer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Networked3dWebExperienceServer.d.ts","sourceRoot":"","sources":["../src/Networked3dWebExperienceServer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,oBAAoB,EACpB,yBAAyB,EAC1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,QAAQ,MAAM,YAAY,CAAC;AAGlC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG1D,KAAK,iBAAiB,GAAG;IACvB,8BAA8B,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7E,0BAA0B,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK;QACpD,EAAE,EAAE,MAAM,CAAC;KACZ,GAAG,IAAI,CAAC;IACT,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,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,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC;IAC9F,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5C,CAAC;AAEF,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E,MAAM,MAAM,oCAAoC,GAAG;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE;QAChB,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,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,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;CACtC,CAAC;AAEF,qBAAa,8BAA8B;IAK7B,OAAO,CAAC,MAAM;IAJnB,oBAAoB,EAAE,oBAAoB,CAAC;IAE3C,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;gBAE3B,MAAM,EAAE,oCAAoC;IAgCzD,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ;IAKxD,OAAO,CAAC,KAAK,CAAC,EAAE,yBAAyB;IAOhD,qBAAqB,CAAC,GAAG,EAAE,QAAQ,CAAC,WAAW;CAiDhD"}
1
+ {"version":3,"file":"Networked3dWebExperienceServer.d.ts","sourceRoot":"","sources":["../src/Networked3dWebExperienceServer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { CharacterDescription, UserData, UserIdentityUpdate } from "@mml-io/3d-web-user-networking";
2
+ import type { UserAuthenticator } from "../Networked3dWebExperienceServer";
3
+ export type AnonymousAuthenticatorOptions = {
4
+ /** Character descriptions to randomly assign to connecting users. */
5
+ defaultCharacterDescriptions?: CharacterDescription[];
6
+ };
7
+ /**
8
+ * Stateless anonymous authenticator. Every connection gets a random username
9
+ * and avatar. No database required.
10
+ */
11
+ export declare class AnonymousAuthenticator implements UserAuthenticator {
12
+ private usersByToken;
13
+ private connectionIdToToken;
14
+ private nextId;
15
+ private characterDescriptions;
16
+ private cleanupInterval;
17
+ constructor(options?: AnonymousAuthenticatorOptions);
18
+ private randomCharacterDescription;
19
+ generateAuthorizedSessionToken(): Promise<string>;
20
+ getClientIdForSessionToken(sessionToken: string): {
21
+ id: number;
22
+ } | null;
23
+ onClientConnect(connectionId: number, sessionToken: string, _userIdentityPresentedOnConnection?: UserData): UserData | true | Error;
24
+ onClientUserIdentityUpdate(_connectionId: number, userIdentity: UserIdentityUpdate): UserIdentityUpdate | true | Error;
25
+ onClientDisconnect(connectionId: number): void;
26
+ getSessionAuthToken(_sessionToken: string): string | null;
27
+ dispose(): void;
28
+ }
29
+ //# sourceMappingURL=AnonymousAuthenticator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnonymousAuthenticator.d.ts","sourceRoot":"","sources":["../../src/auth/AnonymousAuthenticator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,oBAAoB,EACpB,QAAQ,EACR,kBAAkB,EACnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAK3E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,qEAAqE;IACrE,4BAA4B,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACvD,CAAC;AAEF;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,iBAAiB;IAC9D,OAAO,CAAC,YAAY,CAGhB;IACJ,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,qBAAqB,CAAyB;IACtD,OAAO,CAAC,eAAe,CAAiC;gBAE5C,OAAO,CAAC,EAAE,6BAA6B;IAiBnD,OAAO,CAAC,0BAA0B;IAO5B,8BAA8B,IAAI,OAAO,CAAC,MAAM,CAAC;IAcvD,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAMvE,eAAe,CACb,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,kCAAkC,CAAC,EAAE,QAAQ,GAC5C,QAAQ,GAAG,IAAI,GAAG,KAAK;IAa1B,0BAA0B,CACxB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,kBAAkB,GAC/B,kBAAkB,GAAG,IAAI,GAAG,KAAK;IAIpC,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI;IAQ9C,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAIzD,OAAO,IAAI,IAAI;CAKhB"}
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./MMLDocumentsServer";
2
2
  export * from "./Networked3dWebExperienceServer";
3
3
  export * from "./websocketDirectoryChangeListener";
4
+ export { AnonymousAuthenticator, type AnonymousAuthenticatorOptions, } from "./auth/AnonymousAuthenticator";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,kCAAkC,CAAC;AACjD,cAAc,oCAAoC,CAAC;AACnD,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,GACnC,MAAM,+BAA+B,CAAC"}
package/build/index.js CHANGED
@@ -15,9 +15,11 @@ var MMLDocumentsServer = class {
15
15
  this.watch();
16
16
  }
17
17
  documents = /* @__PURE__ */ new Map();
18
+ disposed = false;
18
19
  watcher;
19
20
  watchPattern;
20
21
  dispose() {
22
+ this.disposed = true;
21
23
  for (const { document } of this.documents.values()) {
22
24
  document.dispose();
23
25
  }
@@ -33,7 +35,9 @@ var MMLDocumentsServer = class {
33
35
  }
34
36
  document.addWebSocket(ws);
35
37
  ws.on("close", () => {
36
- document.removeWebSocket(ws);
38
+ if (!this.disposed) {
39
+ document.removeWebSocket(ws);
40
+ }
37
41
  });
38
42
  }
39
43
  watch() {
@@ -91,11 +95,23 @@ var MMLDocumentsServer = class {
91
95
  };
92
96
 
93
97
  // src/Networked3dWebExperienceServer.ts
98
+ import {
99
+ experienceProtocolToDeltaNetSubProtocol,
100
+ FROM_CLIENT_CHAT_MESSAGE_TYPE,
101
+ FROM_SERVER_CHAT_MESSAGE_TYPE,
102
+ FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,
103
+ handleExperienceWebsocketSubprotocol,
104
+ MAX_CHAT_MESSAGE_LENGTH,
105
+ parseClientChatMessage,
106
+ FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE
107
+ } from "@mml-io/3d-web-experience-protocol";
94
108
  import {
95
109
  UserNetworkingServer
96
110
  } from "@mml-io/3d-web-user-networking";
111
+ import { NetworkedDOM } from "@mml-io/networked-dom-server";
97
112
  import cors from "cors";
98
113
  import express from "express";
114
+ import enableWs from "express-ws";
99
115
 
100
116
  // src/websocketDirectoryChangeListener.ts
101
117
  import { watch as watch2 } from "chokidar";
@@ -116,6 +132,9 @@ function websocketDirectoryChangeListener(app, options) {
116
132
 
117
133
  // src/Networked3dWebExperienceServer.ts
118
134
  var defaultSessionTokenPlaceholder = "SESSION.TOKEN.PLACEHOLDER";
135
+ function escapeForJsString(str) {
136
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
137
+ }
119
138
  var Networked3dWebExperienceServer = class {
120
139
  constructor(config) {
121
140
  this.config = config;
@@ -123,50 +142,216 @@ var Networked3dWebExperienceServer = class {
123
142
  const { documentsWatchPath, documentsDirectoryRoot } = this.config.mmlServing;
124
143
  this.mmlDocumentsServer = new MMLDocumentsServer(documentsDirectoryRoot, documentsWatchPath);
125
144
  }
145
+ this.worldConfig = this.config.worldConfig;
126
146
  this.userNetworkingServer = new UserNetworkingServer({
127
- legacyAdapterEnabled: true,
128
- onClientConnect: (clientId, sessionToken, userIdentityPresentedOnConnection) => {
129
- return this.config.userAuthenticator.onClientConnect(
130
- clientId,
147
+ onClientConnect: (connectionId, sessionToken, userIdentityPresentedOnConnection) => {
148
+ const result = this.config.userAuthenticator.onClientConnect(
149
+ connectionId,
131
150
  sessionToken,
132
151
  userIdentityPresentedOnConnection
133
152
  );
153
+ if (result !== null && typeof result === "object" && "then" in result) {
154
+ return result.then((resolved) => {
155
+ if (!(resolved instanceof Error)) {
156
+ this.connectionSessionTokens.set(connectionId, sessionToken);
157
+ }
158
+ return resolved;
159
+ });
160
+ }
161
+ if (!(result instanceof Error)) {
162
+ this.connectionSessionTokens.set(connectionId, sessionToken);
163
+ }
164
+ return result;
134
165
  },
135
- onClientUserIdentityUpdate: (clientId, userIdentity) => {
136
- return this.config.userAuthenticator.onClientUserIdentityUpdate(clientId, userIdentity);
166
+ onClientUserIdentityUpdate: (connectionId, userIdentity) => {
167
+ return this.config.userAuthenticator.onClientUserIdentityUpdate(connectionId, userIdentity);
137
168
  },
138
- onClientDisconnect: (clientId) => {
139
- this.config.userAuthenticator.onClientDisconnect(clientId);
140
- }
169
+ onClientDisconnect: (connectionId) => {
170
+ this.connectionSessionTokens.delete(connectionId);
171
+ this.config.userAuthenticator.onClientDisconnect(connectionId);
172
+ },
173
+ onClientAuthenticated: (connectionId) => {
174
+ if (this.worldConfig) {
175
+ this.userNetworkingServer.sendCustomMessageToClient(
176
+ connectionId,
177
+ FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,
178
+ JSON.stringify(this.worldConfig)
179
+ );
180
+ }
181
+ const sessionToken = this.connectionSessionTokens.get(connectionId);
182
+ if (sessionToken && this.config.userAuthenticator.getSessionAuthToken) {
183
+ const result = this.config.userAuthenticator.getSessionAuthToken(sessionToken);
184
+ if (result !== null && typeof result === "object" && "then" in result) {
185
+ const expectedToken = sessionToken;
186
+ result.then(
187
+ (token) => {
188
+ if (token !== null && this.connectionSessionTokens.get(connectionId) === expectedToken) {
189
+ const sessionConfig = { authToken: token };
190
+ this.userNetworkingServer.sendCustomMessageToClient(
191
+ connectionId,
192
+ FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,
193
+ JSON.stringify(sessionConfig)
194
+ );
195
+ }
196
+ },
197
+ () => {
198
+ }
199
+ );
200
+ } else {
201
+ const token = result;
202
+ if (token !== null) {
203
+ const sessionConfig = { authToken: token };
204
+ this.userNetworkingServer.sendCustomMessageToClient(
205
+ connectionId,
206
+ FROM_SERVER_SESSION_CONFIG_MESSAGE_TYPE,
207
+ JSON.stringify(sessionConfig)
208
+ );
209
+ }
210
+ }
211
+ }
212
+ },
213
+ onCustomMessage: (connectionId, customType, contents) => {
214
+ var _a;
215
+ if (customType === FROM_CLIENT_CHAT_MESSAGE_TYPE) {
216
+ const chatEnabled = this.config.enableChat ?? ((_a = this.worldConfig) == null ? void 0 : _a.enableChat) ?? true;
217
+ if (!chatEnabled) {
218
+ return;
219
+ }
220
+ const chatMessage = parseClientChatMessage(contents);
221
+ if (chatMessage instanceof Error) {
222
+ console.error(`Invalid chat message from connection ${connectionId}:`, chatMessage);
223
+ const errorPayload = {
224
+ fromConnectionId: 0,
225
+ userId: "",
226
+ message: "[Server] Your message could not be delivered (invalid format)."
227
+ };
228
+ this.userNetworkingServer.sendCustomMessageToClient(
229
+ connectionId,
230
+ FROM_SERVER_CHAT_MESSAGE_TYPE,
231
+ JSON.stringify(errorPayload)
232
+ );
233
+ } else {
234
+ const senderUser = this.userNetworkingServer.getAuthenticatedUser(connectionId);
235
+ const serverChatMessage = {
236
+ fromConnectionId: connectionId,
237
+ userId: (senderUser == null ? void 0 : senderUser.userId) ?? "",
238
+ message: chatMessage.message.substring(0, MAX_CHAT_MESSAGE_LENGTH)
239
+ };
240
+ this.userNetworkingServer.broadcastMessage(
241
+ FROM_SERVER_CHAT_MESSAGE_TYPE,
242
+ JSON.stringify(serverChatMessage)
243
+ );
244
+ }
245
+ }
246
+ },
247
+ resolveProtocol: experienceProtocolToDeltaNetSubProtocol
141
248
  });
142
249
  }
143
250
  userNetworkingServer;
144
251
  mmlDocumentsServer;
145
- updateUserCharacter(clientId, userData) {
146
- console.log(`Initiate server-side update of client ${clientId}`);
147
- this.userNetworkingServer.updateUserCharacter(clientId, userData);
252
+ worldConfig;
253
+ connectionSessionTokens = /* @__PURE__ */ new Map();
254
+ /**
255
+ * Replace the index HTML content served to new web clients.
256
+ */
257
+ setIndexContent(indexContent) {
258
+ if (this.config.webClientServing) {
259
+ this.config.webClientServing.indexContent = indexContent;
260
+ }
261
+ }
262
+ /**
263
+ * Update whether chat is enabled at runtime.
264
+ */
265
+ setEnableChat(enabled) {
266
+ this.config.enableChat = enabled;
267
+ }
268
+ /**
269
+ * Update the world config and optionally broadcast it to all connected clients.
270
+ * Newly connecting clients will receive the updated config after authentication.
271
+ *
272
+ * By default the update is broadcast to all clients. Pass `{ broadcast: false }`
273
+ * to update the stored config without notifying existing clients.
274
+ */
275
+ setWorldConfig(config, options) {
276
+ this.worldConfig = config;
277
+ if ((options == null ? void 0 : options.broadcast) !== false) {
278
+ this.userNetworkingServer.broadcastMessage(
279
+ FROM_SERVER_WORLD_CONFIG_MESSAGE_TYPE,
280
+ JSON.stringify(this.worldConfig)
281
+ );
282
+ }
283
+ }
284
+ updateUserCharacter(connectionId, userData) {
285
+ console.log(`Initiate server-side update of connection ${connectionId}`);
286
+ this.userNetworkingServer.updateUserCharacter(connectionId, userData);
148
287
  }
149
288
  dispose(error) {
289
+ var _a, _b;
150
290
  this.userNetworkingServer.dispose(error);
151
291
  if (this.mmlDocumentsServer) {
152
292
  this.mmlDocumentsServer.dispose();
153
293
  }
294
+ this.connectionSessionTokens.clear();
295
+ (_b = (_a = this.config.userAuthenticator).dispose) == null ? void 0 : _b.call(_a);
154
296
  }
155
- registerExpressRoutes(app) {
297
+ /**
298
+ * Register all HTTP and WebSocket routes on the given Express application.
299
+ *
300
+ * Accepts either a plain `express.Application` or one that already has
301
+ * `express-ws` applied. If WebSocket support has not been applied yet, this
302
+ * method calls `enableWs()` internally with the required sub-protocol
303
+ * handling. If the application already has a `.ws()` method (i.e. the caller
304
+ * applied `express-ws` themselves), the existing setup is reused.
305
+ */
306
+ registerExpressRoutes(expressApp) {
307
+ var _a;
308
+ const mmlDocumentsUrl = (_a = this.config.mmlServing) == null ? void 0 : _a.documentsUrl;
309
+ let app;
310
+ if (typeof expressApp.ws === "function") {
311
+ app = expressApp;
312
+ } else {
313
+ ({ app } = enableWs(expressApp, void 0, {
314
+ wsOptions: {
315
+ handleProtocols: (protocols, request) => {
316
+ var _a2;
317
+ if (mmlDocumentsUrl && ((_a2 = request.url) == null ? void 0 : _a2.startsWith(mmlDocumentsUrl))) {
318
+ return NetworkedDOM.handleWebsocketSubprotocol(protocols);
319
+ }
320
+ return handleExperienceWebsocketSubprotocol(protocols);
321
+ }
322
+ }
323
+ }));
324
+ }
156
325
  app.ws(this.config.networkPath, (ws) => {
157
326
  this.userNetworkingServer.connectClient(ws);
158
327
  });
159
328
  const webClientServing = this.config.webClientServing;
160
329
  if (webClientServing) {
161
330
  app.get(webClientServing.indexUrl, async (req, res) => {
162
- const token = await this.config.userAuthenticator.generateAuthorizedSessionToken(req);
163
- if (!token) {
164
- res.send("Error: Could not generate token");
331
+ const result = await this.config.userAuthenticator.generateAuthorizedSessionToken(req);
332
+ if (result === null) {
333
+ res.status(403).send("Access denied: authentication required");
334
+ return;
335
+ }
336
+ if (typeof result === "object" && "redirect" in result) {
337
+ try {
338
+ const redirectUrl = new URL(result.redirect);
339
+ if (redirectUrl.protocol !== "http:" && redirectUrl.protocol !== "https:") {
340
+ console.error("Redirect URL has disallowed scheme:", result.redirect);
341
+ res.send("Error: Invalid redirect URL");
342
+ return;
343
+ }
344
+ } catch {
345
+ console.error("Invalid redirect URL from authenticator:", result.redirect);
346
+ res.send("Error: Invalid redirect URL");
347
+ return;
348
+ }
349
+ res.redirect(result.redirect);
165
350
  return;
166
351
  }
167
352
  const authorizedDemoIndexContent = webClientServing.indexContent.replace(
168
353
  webClientServing.sessionTokenPlaceholder || defaultSessionTokenPlaceholder,
169
- token
354
+ escapeForJsString(result)
170
355
  );
171
356
  res.send(authorizedDemoIndexContent);
172
357
  });
@@ -196,7 +381,84 @@ var Networked3dWebExperienceServer = class {
196
381
  }
197
382
  }
198
383
  };
384
+
385
+ // src/auth/AnonymousAuthenticator.ts
386
+ import crypto from "crypto";
387
+ var TOKEN_EXPIRY_MS = 5 * 60 * 1e3;
388
+ var TOKEN_CLEANUP_INTERVAL_MS = 60 * 1e3;
389
+ var AnonymousAuthenticator = class {
390
+ usersByToken = /* @__PURE__ */ new Map();
391
+ connectionIdToToken = /* @__PURE__ */ new Map();
392
+ nextId = 1;
393
+ characterDescriptions;
394
+ cleanupInterval;
395
+ constructor(options) {
396
+ this.characterDescriptions = (options == null ? void 0 : options.defaultCharacterDescriptions) ?? [];
397
+ this.cleanupInterval = setInterval(() => {
398
+ const now = Date.now();
399
+ for (const [token, entry] of this.usersByToken) {
400
+ if (entry.connectionId === null && now - entry.createdAt > TOKEN_EXPIRY_MS) {
401
+ this.usersByToken.delete(token);
402
+ }
403
+ }
404
+ }, TOKEN_CLEANUP_INTERVAL_MS);
405
+ this.cleanupInterval.unref();
406
+ }
407
+ randomCharacterDescription() {
408
+ if (this.characterDescriptions.length === 0) return void 0;
409
+ return this.characterDescriptions[Math.floor(Math.random() * this.characterDescriptions.length)];
410
+ }
411
+ async generateAuthorizedSessionToken() {
412
+ const token = crypto.randomBytes(20).toString("hex");
413
+ const id = this.nextId++;
414
+ const characterDescription = this.randomCharacterDescription();
415
+ const userData = {
416
+ userId: crypto.randomUUID(),
417
+ username: `User ${id}`,
418
+ characterDescription: characterDescription ?? null,
419
+ colors: null
420
+ };
421
+ this.usersByToken.set(token, { id, userData, connectionId: null, createdAt: Date.now() });
422
+ return token;
423
+ }
424
+ getClientIdForSessionToken(sessionToken) {
425
+ const entry = this.usersByToken.get(sessionToken);
426
+ if (!entry) return null;
427
+ return { id: entry.id };
428
+ }
429
+ onClientConnect(connectionId, sessionToken, _userIdentityPresentedOnConnection) {
430
+ const entry = this.usersByToken.get(sessionToken);
431
+ if (!entry) {
432
+ return new Error("Invalid session token");
433
+ }
434
+ if (entry.connectionId !== null) {
435
+ return new Error("Session token already connected");
436
+ }
437
+ entry.connectionId = connectionId;
438
+ this.connectionIdToToken.set(connectionId, sessionToken);
439
+ return entry.userData;
440
+ }
441
+ onClientUserIdentityUpdate(_connectionId, userIdentity) {
442
+ return userIdentity;
443
+ }
444
+ onClientDisconnect(connectionId) {
445
+ const token = this.connectionIdToToken.get(connectionId);
446
+ if (token) {
447
+ this.usersByToken.delete(token);
448
+ this.connectionIdToToken.delete(connectionId);
449
+ }
450
+ }
451
+ getSessionAuthToken(_sessionToken) {
452
+ return null;
453
+ }
454
+ dispose() {
455
+ clearInterval(this.cleanupInterval);
456
+ this.usersByToken.clear();
457
+ this.connectionIdToToken.clear();
458
+ }
459
+ };
199
460
  export {
461
+ AnonymousAuthenticator,
200
462
  MMLDocumentsServer,
201
463
  Networked3dWebExperienceServer,
202
464
  defaultSessionTokenPlaceholder,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/MMLDocumentsServer.ts", "../src/Networked3dWebExperienceServer.ts", "../src/websocketDirectoryChangeListener.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 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 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 document.removeWebSocket(ws as any);\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 {\n UserData,\n UserNetworkingServer,\n UserNetworkingServerError,\n} from \"@mml-io/3d-web-user-networking\";\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\ntype UserAuthenticator = {\n generateAuthorizedSessionToken(req: express.Request): Promise<string | null>;\n getClientIdForSessionToken: (sessionToken: string) => {\n id: number;\n } | null;\n onClientConnect(\n clientId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error;\n onClientUserIdentityUpdate(clientId: number, userIdentity: UserData): UserData | true | Error;\n onClientDisconnect(clientId: number): 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 enableChat?: boolean;\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\nexport class Networked3dWebExperienceServer {\n public userNetworkingServer: UserNetworkingServer;\n\n public mmlDocumentsServer?: MMLDocumentsServer;\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.userNetworkingServer = new UserNetworkingServer({\n legacyAdapterEnabled: true,\n onClientConnect: (\n clientId: number,\n sessionToken: string,\n userIdentityPresentedOnConnection?: UserData,\n ): Promise<UserData | true | Error> | UserData | true | Error => {\n return this.config.userAuthenticator.onClientConnect(\n clientId,\n sessionToken,\n userIdentityPresentedOnConnection,\n );\n },\n onClientUserIdentityUpdate: (\n clientId: number,\n userIdentity: UserData,\n ): UserData | true | Error => {\n // Called whenever a user connects or updates their character/identity\n return this.config.userAuthenticator.onClientUserIdentityUpdate(clientId, userIdentity);\n },\n onClientDisconnect: (clientId: number): void => {\n this.config.userAuthenticator.onClientDisconnect(clientId);\n },\n });\n }\n\n public updateUserCharacter(clientId: number, userData: UserData) {\n console.log(`Initiate server-side update of client ${clientId}`);\n this.userNetworkingServer.updateUserCharacter(clientId, userData);\n }\n\n public dispose(error?: UserNetworkingServerError) {\n this.userNetworkingServer.dispose(error);\n if (this.mmlDocumentsServer) {\n this.mmlDocumentsServer.dispose();\n }\n }\n\n registerExpressRoutes(app: enableWs.Application) {\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 token = await this.config.userAuthenticator.generateAuthorizedSessionToken(req);\n if (!token) {\n res.send(\"Error: Could not generate token\");\n return;\n }\n const authorizedDemoIndexContent = webClientServing.indexContent.replace(\n webClientServing.sessionTokenPlaceholder || defaultSessionTokenPlaceholder,\n token,\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"],
5
- "mappings": "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",
6
- "names": ["watch", "path"]
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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",
6
+ "names": ["watch", "_a", "path"]
7
7
  }
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@mml-io/3d-web-experience-server",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mml-io/3d-web-experience.git",
10
+ "directory": "packages/3d-web-experience-server"
11
+ },
7
12
  "main": "./build/index.js",
8
13
  "types": "./build/index.d.ts",
9
14
  "type": "module",
@@ -16,11 +21,13 @@
16
21
  "iterate": "tsx ./build.ts --watch",
17
22
  "type-check": "tsc --noEmit",
18
23
  "lint": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --max-warnings 0",
19
- "lint-fix": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --fix"
24
+ "lint-fix": "eslint \"./{src,test}/**/*.{js,jsx,ts,tsx}\" --fix",
25
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
20
26
  },
21
27
  "dependencies": {
22
- "@mml-io/3d-web-user-networking": "^0.26.0",
23
- "@mml-io/networked-dom-server": "0.23.1",
28
+ "@mml-io/3d-web-experience-protocol": "^0.27.0",
29
+ "@mml-io/3d-web-user-networking": "^0.27.0",
30
+ "@mml-io/networked-dom-server": "0.26.1",
24
31
  "chokidar": "4.0.3",
25
32
  "cors": "^2.8.5",
26
33
  "express": "^4.21.2",
@@ -29,12 +36,17 @@
29
36
  "ws": "^8.18.0"
30
37
  },
31
38
  "devDependencies": {
39
+ "@jest/globals": "29.7.0",
32
40
  "@types/cors": "2.8.17",
33
41
  "@types/express": "^5.0.0",
34
42
  "@types/express-ws": "^3.0.5",
35
43
  "@types/jest": "^29.5.12",
36
44
  "@types/micromatch": "^4.0.9",
37
- "@types/node": "^22.13.1"
45
+ "@types/node": "^22.13.1",
46
+ "cross-env": "^7.0.3",
47
+ "jest": "^29.7.0",
48
+ "jest-junit": "16.0.0",
49
+ "ts-jest": "^29.1.2"
38
50
  },
39
- "gitHead": "2a3c26bb10a6db0452450b08a5d9d921a5084963"
51
+ "gitHead": "4038d512f99c0700d77337a0daa6f49b281dce87"
40
52
  }