@colyseus/core 0.18.5 → 0.18.7

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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,0BAcO;AAGP,oBAA8J;AAC9J,kBAA2R;AAC3R,yBAAkD;AAClD,mBAAgL;AAChL,oBAAwE;AACxE,wBAGO;AACP,sBAAgC;AAChC,+BAAkC;AAClC,yBAA4B;AAE5B,4BAYO;AAGP,iBAA4B;AAE5B,mBAA4C;AAC5C,kBAAgD;AAGhD,0BAAc,qDAtDd;AAuDA,oBAAoC;AAGpC,uBAAiO;AAGjO,sBAA8B;AAC9B,2BAA8B;AAG9B,wBAAgC;AAChC,8BAAiC;AAIjC,mBAA+E;AAC/E,qBAAsC;AAGtC,iBAA6C;AAG7C,mBASO;AAGP,uBAA0B;AAC1B,uBAA0B;AAC1B,uBAA8G;AAG9G,oBAmBO;AAGP,oBAAuB;",
4
+ "sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA,4BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAG7C,0BAcO;AAGP,oBAAsM;AACtM,kBAA2R;AAC3R,yBAAkD;AAClD,mBAAgL;AAChL,oBAAwE;AACxE,wBAGO;AACP,sBAAgC;AAChC,+BAAkC;AAClC,yBAA4B;AAE5B,4BAYO;AAGP,iBAA4B;AAE5B,mBAA4C;AAC5C,kBAAgD;AAGhD,0BAAc,qDAtDd;AAuDA,oBAAoC;AAGpC,uBAAiO;AAGjO,sBAA8B;AAC9B,2BAA8B;AAG9B,wBAAgC;AAChC,8BAAiC;AAIjC,mBAA+E;AAC/E,qBAAsC;AAGtC,iBAA6C;AAG7C,mBASO;AAGP,uBAA0B;AAC1B,uBAA0B;AAC1B,uBAA8G;AAG9G,oBAmBO;AAGP,oBAAuB;",
6
6
  "names": ["Clock"]
7
7
  }
package/build/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ClockTimer as Clock, Delayed } from '@colyseus/timer';
2
2
  export { Protocol, ProtocolModifier, ErrorCode, CloseCode, ResponseStatus, type InferState, type ExtractRoomMessages, type ExtractRoomClientMessages, type MessageContext, type Rejection, type Resolution, type ExtractRejectReason, type ExtractResponseType, } from '@colyseus/shared-types';
3
- export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
3
+ export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';
4
4
  export { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';
5
5
  export { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';
6
6
  export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';
package/build/index.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  CloseCode,
8
8
  ResponseStatus
9
9
  } from "@colyseus/shared-types";
10
- import { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions } from "./Server.mjs";
10
+ import { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv } from "./Server.mjs";
11
11
  import { Room, RoomInternalState, validate } from "./Room.mjs";
12
12
  import { InputBufferImpl, compileSanitizer } from "./input/InputBuffer.mjs";
13
13
  import {} from "./input/types.mjs";
@@ -102,6 +102,7 @@ export {
102
102
  TimedEventException,
103
103
  TimestepException,
104
104
  Transport,
105
+ applySimulatedLatency,
105
106
  attachToTestRoom,
106
107
  basicAuth,
107
108
  compileSanitizer,
@@ -133,6 +134,7 @@ export {
133
134
  isDevMode,
134
135
  logger,
135
136
  matchMaker,
137
+ parseLatencyEnv,
136
138
  registerRoomDefinitions,
137
139
  requestFromIPC,
138
140
  runBeforeUpgrade,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
5
- "mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP,SAAS,QAAQ,YAAY,cAAc,yBAAyB,iCAA0F;AAC9J,SAAS,MAAM,mBAAmB,gBAAyP;AAC3R,SAAS,iBAAiB,wBAAwB;AAClD,eAAgL;AAChL,SAAS,QAAQ,kBAAuD;AACxE;AAAA,EACE;AAAA,EAAY;AAAA,EAAe;AAAA,OAEtB;AACP,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAE5B;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,YAAY,gBAAgB;AAE5B,SAAS,aAAa,sBAAsB;AAC5C,SAAS,uCAAuC;AAGhD,cAAc;AACd,SAAS,2BAA2B;AAGpC,SAA4D,aAAa,aAAa,WAAyD,kBAAkB,mBAAmB,qBAAqB,wBAAwB;AAGjO,eAA8B;AAC9B,SAAS,qBAAqB;AAG9B,eAAgC;AAChC,SAAS,wBAAwB;AAIjC,SAAS,YAAY,UAAU,WAAW,gBAAgB,qBAAqB;AAC/E,SAAS,WAAW,kBAAkB;AAGtC,SAAS,cAAc,sBAAsB;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,iBAAqG;AAG9G;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,cAAc;",
4
+ "sourcesContent": ["import { ClockTimer as Clock, Delayed } from '@colyseus/timer';\n\n// Shared types - re-export from @colyseus/shared-types for convenience\nexport {\n Protocol,\n ProtocolModifier,\n ErrorCode,\n CloseCode,\n ResponseStatus,\n type InferState,\n type ExtractRoomMessages,\n type ExtractRoomClientMessages,\n type MessageContext,\n type Rejection,\n type Resolution,\n type ExtractRejectReason,\n type ExtractResponseType,\n} from '@colyseus/shared-types';\n\n// Core classes\nexport { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions, applySimulatedLatency, parseLatencyEnv, type RoomDefinitions, type ServerOptions, type SDKTypes } from './Server.ts';\nexport { Room, RoomInternalState, validate, type RoomOptions, type DefineInputOptions, type SimulationCallback, type FixedTimestepCallback, type StepContext, type MessageHandlerWithFormat, type Messages, type ExtractRoomState, type ExtractRoomMetadata, type ExtractRoomClient } from './Room.ts';\nexport { InputBufferImpl, compileSanitizer } from './input/InputBuffer.ts';\nexport { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf } from './input/types.ts';\nexport { Rewind, RewindView, type RewindOptions, type RewindMode } from './Rewind.ts';\nexport {\n RoomPlugin, definePlugins, attachToTestRoom,\n type RoomPluginOrder, type PluginDependencies, type RoomPluginClass,\n} from './RoomPlugin.ts';\nexport { getMessageBytes } from './Protocol.ts';\nexport { RegisteredHandler } from './matchmaker/RegisteredHandler.ts';\nexport { ServerError } from './errors/ServerError.ts';\n\nexport {\n type RoomException,\n type RoomMethodName,\n OnCreateException,\n OnAuthException,\n OnJoinException,\n OnLeaveException,\n OnDisposeException,\n OnMessageException,\n TimestepException,\n SimulationIntervalException,\n TimedEventException,\n} from './errors/RoomExceptions.ts';\n\n// MatchMaker\nimport * as matchMaker from './MatchMaker.ts';\nexport { matchMaker };\nexport { updateLobby, subscribeLobby } from './matchmaker/Lobby.ts';\nexport { createNodeMatchmakingMiddleware } from './router/node.ts';\n\n// Driver\nexport * from './matchmaker/LocalDriver/LocalDriver.ts';\nexport { initializeRoomCache } from './matchmaker/driver.ts';\n\n// Transport\nexport { type Client, type ClientPrivate, type AuthContext, ClientState, ClientArray, Transport, type ISendOptions, type BeforeUpgradeHandler, runBeforeUpgrade, createAuthContext, connectClientToRoom, enqueueClientRaw } from './Transport.ts';\n\n// Presence\nexport { type Presence } from './presence/Presence.ts';\nexport { LocalPresence } from './presence/LocalPresence.ts';\n\n// Serializers\nexport { type Serializer } from './serializer/Serializer.ts';\nexport { SchemaSerializer } from './serializer/SchemaSerializer.ts';\n\n// Utilities\nexport { Clock, Delayed };\nexport { generateId, Deferred, spliceOne, getBearerToken, dynamicImport } from './utils/Utils.ts';\nexport { isDevMode, setDevMode } from './utils/DevMode.ts';\n\n// IPC\nexport { subscribeIPC, requestFromIPC } from './IPC.ts';\n\n// Debug\nexport {\n debugMatchMaking,\n debugMessage,\n debugPatch,\n debugError,\n debugConnection,\n debugDriver,\n debugPresence,\n debugAndPrintError,\n} from './Debug.ts';\n\n// Default rooms\nexport { LobbyRoom } from './rooms/LobbyRoom.ts';\nexport { RelayRoom } from './rooms/RelayRoom.ts';\nexport { QueueRoom, type QueueOptions, type QueueMatchGroup, type QueueMatchTeam, type QueueClientData } from './rooms/QueueRoom.ts';\n\n// Router / Endpoints\nexport {\n createEndpoint,\n createInternalContext,\n createMiddleware,\n createRouter,\n basicAuth,\n type BasicAuthOptions,\n toNodeHandler,\n dualModeEndpoints,\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type ExpressMiddleware,\n type NodeHandler,\n type DualModeHelpers,\n} from './router/index.ts';\n\n// Abstract logging support\nexport { logger } from './Logger.ts';\n"],
5
+ "mappings": ";AAAA,SAAS,cAAc,OAAO,eAAe;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;AAGP,SAAS,QAAQ,YAAY,cAAc,yBAAyB,2BAA2B,uBAAuB,uBAAgF;AACtM,SAAS,MAAM,mBAAmB,gBAAyP;AAC3R,SAAS,iBAAiB,wBAAwB;AAClD,eAAgL;AAChL,SAAS,QAAQ,kBAAuD;AACxE;AAAA,EACE;AAAA,EAAY;AAAA,EAAe;AAAA,OAEtB;AACP,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,mBAAmB;AAE5B;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,YAAY,gBAAgB;AAE5B,SAAS,aAAa,sBAAsB;AAC5C,SAAS,uCAAuC;AAGhD,cAAc;AACd,SAAS,2BAA2B;AAGpC,SAA4D,aAAa,aAAa,WAAyD,kBAAkB,mBAAmB,qBAAqB,wBAAwB;AAGjO,eAA8B;AAC9B,SAAS,qBAAqB;AAG9B,eAAgC;AAChC,SAAS,wBAAwB;AAIjC,SAAS,YAAY,UAAU,WAAW,gBAAgB,qBAAqB;AAC/E,SAAS,WAAW,kBAAkB;AAGtC,SAAS,cAAc,sBAAsB;AAG7C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,iBAAqG;AAG9G;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,OAWK;AAGP,SAAS,cAAc;",
6
6
  "names": []
7
7
  }
@@ -41,6 +41,8 @@ __export(router_exports, {
41
41
  toNodeHandler: () => import_node.toNodeHandler
42
42
  });
43
43
  module.exports = __toCommonJS(router_exports);
44
+ var import_promises = __toESM(require("node:fs/promises"), 1);
45
+ var import_node_path = __toESM(require("node:path"), 1);
44
46
  var import_node_crypto = require("node:crypto");
45
47
  var import_better_call = require("@colyseus/better-call");
46
48
  var import_node = require("@colyseus/better-call/node");
@@ -179,7 +181,8 @@ function dualModeEndpoints(endpoints, opts) {
179
181
  ) : endpoints;
180
182
  const specificRouter = createRouter(specificEndpoints);
181
183
  const specificHandler = (0, import_node.toNodeHandler)(specificRouter.handler);
182
- const middleware = opts.buildMiddleware({
184
+ const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? "", opts.staticDir);
185
+ const middleware = buildMiddleware({
183
186
  specificRouter,
184
187
  specificHandler,
185
188
  fullRouter,
@@ -187,6 +190,49 @@ function dualModeEndpoints(endpoints, opts) {
187
190
  });
188
191
  return Object.assign(middleware, endpoints);
189
192
  }
193
+ function panelMiddleware(prefix, staticDir) {
194
+ const staticRoot = staticDir && import_node_path.default.resolve(staticDir);
195
+ return ({ specificRouter, specificHandler, fullHandler }) => (req, res, next) => {
196
+ const r = req;
197
+ const raw = r.url ?? "";
198
+ const q = raw.indexOf("?");
199
+ const url = q < 0 ? raw : raw.slice(0, q);
200
+ const query = q < 0 ? "" : raw.slice(q);
201
+ const dispatchPath = r.baseUrl ? prefix + url : url;
202
+ if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {
203
+ const original = (r.originalUrl ?? raw).split("?")[0];
204
+ if (r.method === "GET" && !original.endsWith("/")) {
205
+ res.writeHead(302, { location: `${original}/${query}` });
206
+ res.end();
207
+ return;
208
+ }
209
+ }
210
+ const dispatch = (handler) => {
211
+ const wrapped = Object.create(req, {
212
+ url: { value: dispatchPath + query, enumerable: true, configurable: true },
213
+ baseUrl: { value: "", enumerable: true, configurable: true },
214
+ originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true }
215
+ });
216
+ handler(wrapped, res).catch(next);
217
+ };
218
+ const route = specificRouter.findRoute(r.method ?? "GET", dispatchPath);
219
+ if (route && (!route.data?.path?.endsWith("/") || dispatchPath.endsWith("/"))) {
220
+ return dispatch(specificHandler);
221
+ }
222
+ if (r.method !== "GET" || !staticRoot || !dispatchPath.startsWith(prefix)) {
223
+ return next();
224
+ }
225
+ const rel = dispatchPath.slice(prefix.length).replace(/^\/+/, "");
226
+ if (!rel || rel.includes("..")) {
227
+ return next();
228
+ }
229
+ const filePath = import_node_path.default.resolve(staticRoot, rel);
230
+ if (!filePath.startsWith(staticRoot + import_node_path.default.sep)) {
231
+ return next();
232
+ }
233
+ import_promises.default.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());
234
+ };
235
+ }
190
236
  // Annotate the CommonJS export names for ESM import in node:
191
237
  0 && (module.exports = {
192
238
  basicAuth,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
- "mappings": "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",
6
- "names": ["import_better_call", "pkg", "createBetterCallRouter"]
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_better_call", "pkg", "createBetterCallRouter", "path", "fs"]
7
7
  }
@@ -183,6 +183,10 @@ export interface DualModeHelpers {
183
183
  export declare function dualModeEndpoints<E extends Record<string, Endpoint>>(endpoints: E, opts: {
184
184
  /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */
185
185
  catchAllKey?: keyof E;
186
- /** Build the express middleware given the pre-built routers + node handlers. */
187
- buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;
186
+ /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */
187
+ prefix?: string;
188
+ /** SPA dist dir — the default middleware dispatches catch-all requests only for files that exist here. */
189
+ staticDir?: string;
190
+ /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */
191
+ buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;
188
192
  }): ExpressMiddleware & E;
@@ -1,4 +1,6 @@
1
1
  // packages/core/src/router/index.ts
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
2
4
  import { createHash, timingSafeEqual } from "node:crypto";
3
5
  import { createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from "@colyseus/better-call";
4
6
  import { toNodeHandler, getRequest, setResponse } from "@colyseus/better-call/node";
@@ -141,7 +143,8 @@ function dualModeEndpoints(endpoints, opts) {
141
143
  ) : endpoints;
142
144
  const specificRouter = createRouter(specificEndpoints);
143
145
  const specificHandler = toNodeHandler(specificRouter.handler);
144
- const middleware = opts.buildMiddleware({
146
+ const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? "", opts.staticDir);
147
+ const middleware = buildMiddleware({
145
148
  specificRouter,
146
149
  specificHandler,
147
150
  fullRouter,
@@ -149,6 +152,49 @@ function dualModeEndpoints(endpoints, opts) {
149
152
  });
150
153
  return Object.assign(middleware, endpoints);
151
154
  }
155
+ function panelMiddleware(prefix, staticDir) {
156
+ const staticRoot = staticDir && path.resolve(staticDir);
157
+ return ({ specificRouter, specificHandler, fullHandler }) => (req, res, next) => {
158
+ const r = req;
159
+ const raw = r.url ?? "";
160
+ const q = raw.indexOf("?");
161
+ const url = q < 0 ? raw : raw.slice(0, q);
162
+ const query = q < 0 ? "" : raw.slice(q);
163
+ const dispatchPath = r.baseUrl ? prefix + url : url;
164
+ if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {
165
+ const original = (r.originalUrl ?? raw).split("?")[0];
166
+ if (r.method === "GET" && !original.endsWith("/")) {
167
+ res.writeHead(302, { location: `${original}/${query}` });
168
+ res.end();
169
+ return;
170
+ }
171
+ }
172
+ const dispatch = (handler) => {
173
+ const wrapped = Object.create(req, {
174
+ url: { value: dispatchPath + query, enumerable: true, configurable: true },
175
+ baseUrl: { value: "", enumerable: true, configurable: true },
176
+ originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true }
177
+ });
178
+ handler(wrapped, res).catch(next);
179
+ };
180
+ const route = specificRouter.findRoute(r.method ?? "GET", dispatchPath);
181
+ if (route && (!route.data?.path?.endsWith("/") || dispatchPath.endsWith("/"))) {
182
+ return dispatch(specificHandler);
183
+ }
184
+ if (r.method !== "GET" || !staticRoot || !dispatchPath.startsWith(prefix)) {
185
+ return next();
186
+ }
187
+ const rel = dispatchPath.slice(prefix.length).replace(/^\/+/, "");
188
+ if (!rel || rel.includes("..")) {
189
+ return next();
190
+ }
191
+ const filePath = path.resolve(staticRoot, rel);
192
+ if (!filePath.startsWith(staticRoot + path.sep)) {
193
+ return next();
194
+ }
195
+ fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());
196
+ };
197
+ }
152
198
  export {
153
199
  basicAuth,
154
200
  bindRouterToTransport,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/router/index.ts"],
4
- "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Build the express middleware given the pre-built routers + node handlers. */\n buildMiddleware: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const middleware = opts.buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type express from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"http\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createHash, timingSafeEqual } from \"node:crypto\";\nimport { type Endpoint, type Router, type RouterConfig, createRouter as createBetterCallRouter, createEndpoint, createMiddleware, APIError } from \"@colyseus/better-call\";\nimport { toNodeHandler, getRequest, setResponse } from \"@colyseus/better-call/node\";\nimport { Transport } from \"../Transport.ts\";\nimport { controller } from \"../matchmaker/controller.ts\";\nimport pkg from \"../../package.json\" with { type: \"json\" };\n\nexport {\n createEndpoint,\n createMiddleware,\n createInternalContext,\n\n // Re-export every type reachable from an inferred type below \u2014 consumers\n // depend on @colyseus/core, not @colyseus/better-call, and cannot name it\n // under pnpm's isolated node_modules (TS2742/TS2883).\n type Router,\n type RouterConfig,\n type Endpoint,\n type EndpointHandler,\n type EndpointOptions,\n type EndpointContext,\n type StrictEndpoint,\n type StandardSchemaV1,\n type MiddlewareOptions,\n type MiddlewareInputContext,\n type CookieOptions,\n type CookiePrefixOptions,\n type Status,\n} from \"@colyseus/better-call\";\n\nexport { toNodeHandler };\n\nexport function bindRouterToTransport(transport: Transport, router: Router, useExpress: boolean) {\n // add default \"/__healthcheck\" endpoint\n router.addEndpoint(createEndpoint(\"/__healthcheck\", { method: \"GET\" }, async (ctx) => {\n return new Response(\"OK\", { status: 200 });\n }));\n\n const server = transport.server;\n\n // check if the server is bound to an express app\n const expressApp: express.Application = (useExpress)\n ? transport.getExpressApp() as express.Application\n // fallback searching for express app in server listeners\n : server?.listeners('request').find((listener: Function) => listener.name === \"app\" && listener['mountpath'] === '/') as express.Application;\n\n // add default \"/\" route, if not provided.\n const hasRootRoute = (\n // check if express app has a root route\n (expressApp && hasExpressRootRoute(expressApp)) ||\n\n // check if router has a root route\n Object.values(router.endpoints).some(endpoint => endpoint.path === \"/\")\n );\n\n if (!hasRootRoute) {\n router.addEndpoint(createEndpoint(\"/\", { method: \"GET\" }, async (ctx) => {\n return new Response(`Colyseus ${pkg.version}`, { status: 200 });\n }));\n }\n\n // use custom bindRouter method if provided\n if (!server && transport.bindRouter) {\n transport.bindRouter(router);\n return;\n }\n\n // which route handler to use\n // (router + fallback to express, or just router)\n let next: any;\n\n if (expressApp) {\n server.removeListener('request', expressApp);\n\n next = async (req: IncomingMessage, res: ServerResponse) => {\n // check if the route is defined in the router\n // if so, use the router handler, otherwise fallback to express\n if (router.findRoute(req.method, req.url.split('?')[0]) !== undefined) {\n const protocol = req.headers[\"x-forwarded-proto\"] || ((req.socket as any).encrypted ? \"https\" : \"http\");\n const base = `${protocol}://${req.headers[\":authority\"] || req.headers.host}`;\n const response = await router.handler(getRequest({ base, request: req }));\n return setResponse(res, response);\n\n } else {\n return expressApp['handle'](req, res);\n }\n };\n\n } else {\n next = toNodeHandler(router.handler);\n }\n\n // handle cors headers for all requests by default\n server.prependListener('request', (req: IncomingMessage, res: ServerResponse) => {\n const corsHeaders = {\n ...controller.DEFAULT_CORS_HEADERS,\n ...controller.getCorsHeaders(new Headers(req.headers as any)),\n };\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204, corsHeaders);\n res.end();\n return;\n }\n\n Object.entries(corsHeaders).forEach(([key, value]) => {\n res.setHeader(key, value);\n });\n\n next(req, res);\n });\n}\n\n/**\n * Whether the express app already handles the root route, in which case\n * Colyseus must not register its own default \"/\" endpoint over it.\n */\nfunction hasExpressRootRoute(expressApp: express.Application) {\n return expressRouterStack(expressApp).some((layer: any) =>\n layer.match('/') && !['query', 'expressInit'].includes(layer.name));\n}\n\n/**\n * The app's router stack, or an empty stack if it has no routes yet.\n *\n * express v5 exposes the router as `app.router`; v4 exposes it as `app._router`\n * and only creates it once the first route/middleware is registered.\n */\nfunction expressRouterStack(expressApp: express.Application): any[] {\n const app = expressApp as any;\n\n if (app?._router?.stack) {\n return app._router.stack;\n }\n\n try {\n return app?.router?.stack ?? [];\n } catch (e) {\n return []; // express v4 throws on `app.router` \u2014 no routes registered\n }\n}\n\nexport function createRouter<\n E extends Record<string, Endpoint>,\n Config extends RouterConfig\n>(endpoints: E, config: Config = {} as Config) {\n const onError = config?.onError;\n return createBetterCallRouter({ ...endpoints }, {\n // better-call's /api/reference page dumps the full API surface\n // unauthenticated \u2014 opt back in by passing `openapi` explicitly.\n openapi: { disabled: true },\n ...config,\n // Otherwise a malformed body is a 500 plus a stack trace on stderr: log\n // noise any anonymous client can trigger at will. Matched on the message\n // because `onError` receives no request context to test against.\n onError: async (error: unknown) => (error instanceof SyntaxError && error.message.includes('JSON'))\n ? Response.json({ error: 'malformed request body' }, { status: 400 })\n : await onError?.(error),\n });\n}\n\nexport interface BasicAuthOptions {\n /** username \u2192 password. The common static case. */\n users?: Record<string, string>;\n /** Custom validator (e.g. DB-backed). Takes precedence over `users`. */\n validate?: (username: string, password: string) => boolean | Promise<boolean>;\n /** Realm shown in the browser prompt. Default 'Restricted'. */\n realm?: string;\n}\n\n/**\n * HTTP Basic Auth middleware. Drop into any endpoint's `use:` slot to gate\n * it behind a browser credentials prompt:\n *\n * playground({ use: [basicAuth({ users: { admin: 's3cret' } })] })\n */\nexport function basicAuth(opts: BasicAuthOptions) {\n const { users, validate } = opts;\n if (!users && !validate) {\n throw new Error('[basicAuth] provide `users` or `validate`');\n }\n // Realm is interpolated into a header \u2014 strip `\"` so it can't break out.\n const challenge = `Basic realm=\"${(opts.realm ?? 'Restricted').replace(/\"/g, '')}\", charset=\"UTF-8\"`;\n\n return createMiddleware(async (ctx) => {\n const creds = parseBasicHeader(ctx.getHeader('authorization'));\n const ok = !!creds && (validate\n ? await validate(creds.username, creds.password)\n : staticCheck(users!, creds.username, creds.password));\n if (!ok) {\n throw new APIError(401, { message: 'authentication required' }, { 'WWW-Authenticate': challenge });\n }\n });\n}\n\nfunction parseBasicHeader(header: string | null | undefined) {\n if (!header) { return null; }\n const sep = header.indexOf(' ');\n if (sep < 0 || header.slice(0, sep).toLowerCase() !== 'basic') { return null; }\n let decoded: string;\n try { decoded = Buffer.from(header.slice(sep + 1), 'base64').toString('utf8'); } catch { return null; }\n const colon = decoded.indexOf(':');\n if (colon < 0) { return null; }\n return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) };\n}\n\nfunction staticCheck(users: Record<string, string>, username: string, password: string): boolean {\n const expected = Object.prototype.hasOwnProperty.call(users, username) ? users[username] : undefined;\n // Compare even for an unknown user so reject timing doesn't reveal which\n // usernames exist.\n return safeEqual(password, expected ?? '\\0') && expected !== undefined;\n}\n\n// Hash both sides first: equalizes length (timingSafeEqual throws on a\n// length mismatch, which would itself leak the secret's length).\nfunction safeEqual(a: string, b: string): boolean {\n return timingSafeEqual(\n createHash('sha256').update(a).digest(),\n createHash('sha256').update(b).digest(),\n );\n}\n\n// ---------------------------------------------------------------------------\n// dualModeEndpoints \u2014 shared express-compat layer for @colyseus/admin,\n// @colyseus/monitor, @colyseus/playground. Builds the two local routers\n// (specific = no catch-all, full = everything) and the matching node\n// handlers, then packages the express middleware so the return value works\n// both as `{...spread}` into createRouter AND as `app.use(\"/\", x)` middleware.\n// ---------------------------------------------------------------------------\n\nexport type ExpressMiddleware = (\n req: IncomingMessage,\n res: ServerResponse,\n next: (err?: any) => void,\n) => void;\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;\n\nexport interface DualModeHelpers {\n specificRouter: Router;\n specificHandler: NodeHandler;\n fullRouter: Router;\n fullHandler: NodeHandler;\n}\n\nexport function dualModeEndpoints<E extends Record<string, Endpoint>>(\n endpoints: E,\n opts: {\n /** Key in `endpoints` whose path is a catch-all. Excluded from `specificRouter` so it doesn't eat fall-through decisions. */\n catchAllKey?: keyof E;\n /** Endpoint-path prefix baked into `endpoints` (`''`, `'/monitor'`). Used by the default middleware to rebase path mounts. */\n prefix?: string;\n /** SPA dist dir \u2014 the default middleware dispatches catch-all requests only for files that exist here. */\n staticDir?: string;\n /** Build a custom express middleware given the pre-built routers + node handlers. Omit for the default SPA-panel middleware. */\n buildMiddleware?: (helpers: DualModeHelpers) => ExpressMiddleware;\n },\n): ExpressMiddleware & E {\n const fullRouter = createRouter(endpoints);\n const fullHandler = toNodeHandler(fullRouter.handler) as NodeHandler;\n\n const specificEndpoints = opts.catchAllKey\n ? Object.fromEntries(\n Object.entries(endpoints).filter(([k]) => k !== opts.catchAllKey),\n ) as Partial<E>\n : endpoints;\n const specificRouter = createRouter(specificEndpoints as E);\n const specificHandler = toNodeHandler(specificRouter.handler) as NodeHandler;\n\n const buildMiddleware = opts.buildMiddleware ?? panelMiddleware(opts.prefix ?? '', opts.staticDir);\n const middleware = buildMiddleware({\n specificRouter, specificHandler, fullRouter, fullHandler,\n });\n return Object.assign(middleware, endpoints) as ExpressMiddleware & E;\n}\n\n/**\n * Default express middleware for a single-prefix SPA panel (monitor,\n * playground). Express and the endpoint map use different coordinates: a path\n * mount strips its prefix into `req.baseUrl`, while the endpoints keep the\n * configured `prefix` baked into their paths. This rebases every request onto\n * the endpoint namespace, so the panel works at any mount path \u2014 `prefix` only\n * matters in router mode and at root mounts.\n */\nfunction panelMiddleware(prefix: string, staticDir?: string) {\n const staticRoot = staticDir && path.resolve(staticDir);\n return ({ specificRouter, specificHandler, fullHandler }: DualModeHelpers): ExpressMiddleware =>\n (req, res, next) => {\n const r = req as IncomingMessage & { baseUrl?: string; originalUrl?: string };\n const raw = r.url ?? '';\n const q = raw.indexOf('?');\n const url = q < 0 ? raw : raw.slice(0, q);\n const query = q < 0 ? '' : raw.slice(q);\n\n // Path mounts arrive stripped (`req.url` is mount-relative); root and\n // pathless mounts arrive unstripped and already match the endpoints.\n const dispatchPath = r.baseUrl ? prefix + url : url;\n\n // Canonicalize the index: the SPA references its assets relatively, so\n // they only resolve from the trailing-slash URL. The browser's address\n // is originalUrl \u2014 302 (not 301: browsers cache 301s across remounts).\n if (dispatchPath === prefix || dispatchPath === `${prefix}/`) {\n const original = (r.originalUrl ?? raw).split('?')[0]!;\n if (r.method === 'GET' && !original.endsWith('/')) {\n res.writeHead(302, { location: `${original}/${query}` });\n res.end();\n return;\n }\n }\n\n const dispatch = (handler: NodeHandler) => {\n // better-call's getRequest resolves the path as baseUrl + url\n const wrapped = Object.create(req, {\n url: { value: dispatchPath + query, enumerable: true, configurable: true },\n baseUrl: { value: '', enumerable: true, configurable: true },\n originalUrl: { value: dispatchPath + query, enumerable: true, configurable: true },\n });\n handler(wrapped as any, res).catch(next);\n };\n\n const route = specificRouter.findRoute(r.method ?? 'GET', dispatchPath);\n // rou3 normalizes trailing slashes in findRoute but processRequest\n // exact-matches \u2014 only trust the hit when the slashes agree.\n if (route && (!route.data?.path?.endsWith('/') || dispatchPath.endsWith('/'))) {\n return dispatch(specificHandler);\n }\n\n // Asset request \u2014 only delegate when the file exists on disk, so the\n // catch-all's SPA fallback can't mask sibling express routes.\n if (r.method !== 'GET' || !staticRoot || !dispatchPath.startsWith(prefix)) { return next(); }\n const rel = dispatchPath.slice(prefix.length).replace(/^\\/+/, '');\n if (!rel || rel.includes('..')) { return next(); }\n const filePath = path.resolve(staticRoot, rel);\n if (!filePath.startsWith(staticRoot + path.sep)) { return next(); }\n fs.stat(filePath).then((stat) => stat.isFile() ? dispatch(fullHandler) : next()).catch(() => next());\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["createEndpoint", "createMiddleware"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/core",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
4
4
  "description": "Multiplayer Framework for Node.js.",
5
5
  "type": "module",
6
6
  "input": "./src/index.ts",
@@ -52,8 +52,8 @@
52
52
  "msgpackr": "^2.0.1",
53
53
  "nanoid": "^3.3.11",
54
54
  "@colyseus/better-call": "^1.3.1",
55
- "@colyseus/shared-types": "^0.18.1",
56
- "@colyseus/greeting-banner": "^4.0.1"
55
+ "@colyseus/greeting-banner": "^4.0.1",
56
+ "@colyseus/shared-types": "^0.18.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@colyseus/schema": "^5.0.8",
@@ -68,9 +68,9 @@
68
68
  "@pm2/io": "^6.1.0",
69
69
  "express": "^4.16.0 || ^5.0.0",
70
70
  "zod": "^4.1.12",
71
- "@colyseus/ws-transport": "^0.18.1",
72
71
  "@colyseus/auth": "^0.18.1",
73
- "@colyseus/better-call": "^1.3.1"
72
+ "@colyseus/better-call": "^1.3.1",
73
+ "@colyseus/ws-transport": "^0.18.1"
74
74
  },
75
75
  "peerDependenciesMeta": {
76
76
  "@colyseus/auth": {
package/src/Server.ts CHANGED
@@ -11,7 +11,7 @@ import { Deferred, registerGracefulShutdown, dynamicImport, type Type } from './
11
11
 
12
12
  import type { Presence } from "./presence/Presence.ts";
13
13
 
14
- import { setTransport, Transport } from './Transport.ts';
14
+ import { getTransport, setTransport, Transport } from './Transport.ts';
15
15
  import { logger, setLogger } from './Logger.ts';
16
16
  import { setDevMode, isDevMode } from './utils/DevMode.ts';
17
17
  import { type Router, bindRouterToTransport, createRouter } from './router/index.ts';
@@ -125,8 +125,6 @@ export class Server<
125
125
 
126
126
  protected _onTransportReady = new Deferred<Transport>();
127
127
 
128
- private _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
129
-
130
128
  // Implicit default for callers that omit explicit Server reference — e.g.
131
129
  // `playground()` reads `Server.current.router.endpoints` at request time.
132
130
  // Last-construction wins; multi-server setups should reference instances
@@ -384,7 +382,7 @@ export class Server<
384
382
  // this is going to lock all rooms and wait for them to be disposed
385
383
  await matchMaker.gracefullyShutdown();
386
384
 
387
- this.transport.shutdown();
385
+ this.transport?.shutdown();
388
386
  this.presence?.shutdown();
389
387
  await this.driver?.shutdown();
390
388
 
@@ -403,29 +401,19 @@ export class Server<
403
401
 
404
402
  /**
405
403
  * Add simulated latency between client and server.
404
+ *
405
+ * May be called at any time — before or after `listen()`. Also available
406
+ * via the `COLYSEUS_LATENCY` environment variable (round-trip ms), which
407
+ * overrides calls made before the server boots.
408
+ *
406
409
  * @param milliseconds round trip latency in milliseconds.
407
410
  */
408
411
  public simulateLatency(milliseconds: number) {
409
- if (milliseconds > 0) {
410
- logger.warn(`📶️❗ Colyseus latency simulation enabled → ${milliseconds}ms latency for round trip.`);
411
- } else {
412
- logger.warn(`📶️❗ Colyseus latency simulation disabled.`);
413
- }
414
-
415
- const halfwayMS = (milliseconds / 2);
416
- this.transport.simulateLatency(halfwayMS);
417
-
418
- if (this._originalRoomOnMessage == null) {
419
- this._originalRoomOnMessage = Room.prototype['_onMessage'];
420
- }
421
-
422
- const originalOnMessage = this._originalRoomOnMessage;
423
-
424
- Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
425
- // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
426
- const cachedBuffer = Buffer.from(buffer);
427
- setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
428
- };
412
+ // transport may still be resolving (constructor's attach() imports the default one)
413
+ this._onTransportReady.then(
414
+ (transport) => applySimulatedLatency(transport, milliseconds),
415
+ () => {/* missing-transport error already surfaces via attach()/listen() */},
416
+ );
429
417
  }
430
418
 
431
419
  /**
@@ -447,6 +435,11 @@ export class Server<
447
435
  private async _bootServices(): Promise<void> {
448
436
  const { beforeListen, database, express } = this.options;
449
437
 
438
+ // boot runs after module evaluation, so the env var wins over
439
+ // simulateLatency() calls made at the top level of user code
440
+ const envLatency = parseLatencyEnv();
441
+ if (envLatency !== undefined) { this.simulateLatency(envLatency); }
442
+
450
443
  if (beforeListen) { await beforeListen(); }
451
444
  if (database) { await database.boot(); }
452
445
 
@@ -505,6 +498,55 @@ export class Server<
505
498
  () => Promise.resolve()
506
499
  }
507
500
 
501
+ let _originalRoomOnMessage: typeof Room.prototype['_onMessage'] | null = null;
502
+
503
+ /**
504
+ * Applies both halves of the round-trip latency simulation: delays the
505
+ * transport's outgoing messages and the Room's incoming message handling.
506
+ *
507
+ * Prefer `server.simulateLatency()` — this is the shared core behind it, the
508
+ * dev-mode `defineServer()` object and the `COLYSEUS_LATENCY` env var, where
509
+ * no `Server` instance (or transport, yet) may exist.
510
+ */
511
+ export function applySimulatedLatency(transport: Transport | undefined, milliseconds: number) {
512
+ if (milliseconds > 0) {
513
+ logger.warn(`📶️❗ Colyseus latency simulation enabled → ${milliseconds}ms latency for round trip.`);
514
+ } else {
515
+ logger.warn(`📶️❗ Colyseus latency simulation disabled.`);
516
+ }
517
+
518
+ const halfwayMS = (milliseconds / 2);
519
+ transport?.simulateLatency(halfwayMS);
520
+
521
+ if (_originalRoomOnMessage == null) {
522
+ _originalRoomOnMessage = Room.prototype['_onMessage'];
523
+ }
524
+
525
+ const originalOnMessage = _originalRoomOnMessage;
526
+
527
+ Room.prototype['_onMessage'] = milliseconds <= Number.EPSILON ? originalOnMessage : function (this: Room, client, buffer) {
528
+ // uWebSockets.js: duplicate buffer because it is cleared at native layer before the timeout.
529
+ const cachedBuffer = Buffer.from(buffer);
530
+ setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS);
531
+ };
532
+ }
533
+
534
+ /**
535
+ * Reads the `COLYSEUS_LATENCY` environment variable (round-trip milliseconds).
536
+ * Returns `undefined` when absent or empty; a non-numeric value warns and is ignored.
537
+ */
538
+ export function parseLatencyEnv(): number | undefined {
539
+ const value = process.env.COLYSEUS_LATENCY;
540
+ if (value === undefined || value === '') { return undefined; }
541
+
542
+ const milliseconds = Number(value);
543
+ if (isNaN(milliseconds)) {
544
+ logger.warn(`📶️❗ Ignoring COLYSEUS_LATENCY: expected a number of milliseconds, got ${JSON.stringify(value)}.`);
545
+ return undefined;
546
+ }
547
+ return milliseconds;
548
+ }
549
+
508
550
  export type RoomDefinitions = Record<string, RegisteredHandler | Type<Room>>;
509
551
 
510
552
  function isRegisteredHandler(value: RegisteredHandler | Type<Room>): value is RegisteredHandler {
@@ -562,6 +604,8 @@ export function defineServer<
562
604
  options: serverOptions,
563
605
  router: routes,
564
606
  '~rooms': rooms,
607
+ // the vite plugin registers the transport before importing user code
608
+ simulateLatency: (milliseconds: number) => applySimulatedLatency(getTransport(), milliseconds),
565
609
  } as unknown as Server<T, R>;
566
610
  }
567
611