@couch-kit/host 1.7.12 → 1.7.13

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,4 +1,19 @@
1
1
  import { MessageTypes, type ClientMessage } from "@couch-kit/core";
2
+ /**
3
+ * Default maximum size (in bytes) of an inbound client message. Frames larger
4
+ * than this are discarded before `JSON.parse`, bounding the memory a single
5
+ * malicious LAN client can force the host to allocate. Generous enough for
6
+ * data-URI avatars in JOIN payloads while blocking multi-megabyte abuse.
7
+ */
8
+ export declare const DEFAULT_MAX_MESSAGE_BYTES: number;
9
+ /**
10
+ * Compute the UTF-8 byte length of a raw WebSocket frame payload.
11
+ *
12
+ * For binary frames the `ArrayBuffer.byteLength` is exact. For text frames the
13
+ * UTF-8 size is counted directly from the JS string without allocating an
14
+ * encoder buffer, so an oversized frame can be rejected cheaply.
15
+ */
16
+ export declare function frameByteLength(data: string | ArrayBuffer): number;
2
17
  type ClientMessageOf<TType extends ClientMessage["type"]> = Extract<ClientMessage, {
3
18
  type: TType;
4
19
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"message-validation.d.ts","sourceRoot":"","sources":["../src/message-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEnE,KAAK,eAAe,CAAC,KAAK,SAAS,aAAa,CAAC,MAAM,CAAC,IAAI,OAAO,CACjE,aAAa,EACb;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IACE,IAAI,EAAE,OAAO,YAAY,CAAC,IAAI,CAAC;IAC/B,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH,GACD,eAAe,CAAC,QAAQ,CAAC,GACzB,eAAe,CAAC,MAAM,CAAC,GACvB,eAAe,CAAC,eAAe,CAAC,CAAC;AAErC;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,GACX,GAAG,IAAI,sBAAsB,CA8B/B"}
1
+ {"version":3,"file":"message-validation.d.ts","sourceRoot":"","sources":["../src/message-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,QAAa,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAmBlE;AAED,KAAK,eAAe,CAAC,KAAK,SAAS,aAAa,CAAC,MAAM,CAAC,IAAI,OAAO,CACjE,aAAa,EACb;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IACE,IAAI,EAAE,OAAO,YAAY,CAAC,IAAI,CAAC;IAC/B,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH,GACD,eAAe,CAAC,QAAQ,CAAC,GACzB,eAAe,CAAC,MAAM,CAAC,GACvB,eAAe,CAAC,eAAe,CAAC,CAAC;AAErC;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,GACX,GAAG,IAAI,sBAAsB,CA8B/B"}
package/lib/provider.d.ts CHANGED
@@ -13,6 +13,11 @@ export interface GameHostConfig<S extends IGameState, A extends IAction> {
13
13
  disconnectTimeout?: number;
14
14
  /** State broadcast throttle interval in milliseconds (default: 33ms, ~30fps). */
15
15
  stateThrottleMs?: number;
16
+ /**
17
+ * Maximum size (in bytes) of an inbound client message. Frames larger than
18
+ * this are discarded before parsing (default: 256 KiB).
19
+ */
20
+ maxMessageBytes?: number;
16
21
  /** Called when a player successfully joins. */
17
22
  onPlayerJoined?: (playerId: string, name: string) => void;
18
23
  /** Called when a player disconnects. */
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,OAAO,EAEb,MAAM,iBAAiB,CAAC;AAczB,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACrE,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,wCAAwC;IACxC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,yCAAyC;IACzC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,UAAU,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACpE,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AAQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAAE,EACxE,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC9B,qBAqWA;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,KAK/C,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAC7C"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,OAAO,EAEb,MAAM,iBAAiB,CAAC;AAczB,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACrE,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,wCAAwC;IACxC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,yCAAyC;IACzC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,UAAU,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACpE,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AAQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAAE,EACxE,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC9B,qBAyWA;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,KAK/C,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAC7C"}
@@ -8,6 +8,13 @@ import { EventEmitter } from "./event-emitter";
8
8
  export interface WebSocketConfig {
9
9
  port: number;
10
10
  debug?: boolean;
11
+ /**
12
+ * Maximum size (in bytes) of an inbound client message. Frames larger than
13
+ * this are discarded before parsing, bounding the memory a single client can
14
+ * force the host to allocate. Defaults to {@link DEFAULT_MAX_MESSAGE_BYTES}
15
+ * (256 KiB).
16
+ */
17
+ maxMessageBytes?: number;
11
18
  /**
12
19
  * @deprecated No effect. The underlying nitro-http WebSocket transport
13
20
  * manages frame sizing internally and this value is never read. This field
@@ -41,6 +48,7 @@ export declare class GameWebSocketServer extends EventEmitter<WebSocketServerEve
41
48
  private clients;
42
49
  private port;
43
50
  private debug;
51
+ private maxMessageBytes;
44
52
  constructor(config: WebSocketConfig);
45
53
  private log;
46
54
  start(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,8CAA8C;AAC9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CACvB,CAAC;AAOF,qBAAa,mBAAoB,SAAQ,YAAY,CAAC,qBAAqB,CAAC;IAC1E,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,KAAK,CAAU;gBAEX,MAAM,EAAE,eAAe;IAUnC,OAAO,CAAC,GAAG;IAME,KAAK;IA6FL,IAAI;IAoBjB;;;OAGG;IACI,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;IAsB3C;;;OAGG;IACI,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM;IAoBlD,yDAAyD;IACzD,IAAW,WAAW,IAAI,MAAM,CAE/B;CACF"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../src/websocket.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAO/C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,8CAA8C;AAC9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC/B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CACvB,CAAC;AAOF,qBAAa,mBAAoB,SAAQ,YAAY,CAAC,qBAAqB,CAAC;IAC1E,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,eAAe,CAAS;gBAEpB,MAAM,EAAE,eAAe;IAYnC,OAAO,CAAC,GAAG;IAME,KAAK;IAuGL,IAAI;IAoBjB;;;OAGG;IACI,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;IAsB3C;;;OAGG;IACI,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM;IAoBlD,yDAAyD;IACzD,IAAW,WAAW,IAAI,MAAM,CAE/B;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couch-kit/host",
3
- "version": "1.7.12",
3
+ "version": "1.7.13",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -1,5 +1,41 @@
1
1
  import { MessageTypes, type ClientMessage } from "@couch-kit/core";
2
2
 
3
+ /**
4
+ * Default maximum size (in bytes) of an inbound client message. Frames larger
5
+ * than this are discarded before `JSON.parse`, bounding the memory a single
6
+ * malicious LAN client can force the host to allocate. Generous enough for
7
+ * data-URI avatars in JOIN payloads while blocking multi-megabyte abuse.
8
+ */
9
+ export const DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
10
+
11
+ /**
12
+ * Compute the UTF-8 byte length of a raw WebSocket frame payload.
13
+ *
14
+ * For binary frames the `ArrayBuffer.byteLength` is exact. For text frames the
15
+ * UTF-8 size is counted directly from the JS string without allocating an
16
+ * encoder buffer, so an oversized frame can be rejected cheaply.
17
+ */
18
+ export function frameByteLength(data: string | ArrayBuffer): number {
19
+ if (typeof data !== "string") return data.byteLength;
20
+
21
+ let bytes = 0;
22
+ for (let i = 0; i < data.length; i++) {
23
+ const code = data.charCodeAt(i);
24
+ if (code < 0x80) {
25
+ bytes += 1;
26
+ } else if (code < 0x800) {
27
+ bytes += 2;
28
+ } else if (code >= 0xd800 && code <= 0xdbff) {
29
+ // High surrogate: a full pair encodes to 4 UTF-8 bytes.
30
+ bytes += 4;
31
+ i++; // Skip the paired low surrogate.
32
+ } else {
33
+ bytes += 3;
34
+ }
35
+ }
36
+ return bytes;
37
+ }
38
+
3
39
  type ClientMessageOf<TType extends ClientMessage["type"]> = Extract<
4
40
  ClientMessage,
5
41
  { type: TType }
package/src/provider.tsx CHANGED
@@ -47,6 +47,11 @@ export interface GameHostConfig<S extends IGameState, A extends IAction> {
47
47
  disconnectTimeout?: number;
48
48
  /** State broadcast throttle interval in milliseconds (default: 33ms, ~30fps). */
49
49
  stateThrottleMs?: number;
50
+ /**
51
+ * Maximum size (in bytes) of an inbound client message. Frames larger than
52
+ * this are discarded before parsing (default: 256 KiB).
53
+ */
54
+ maxMessageBytes?: number;
50
55
  /** Called when a player successfully joins. */
51
56
  onPlayerJoined?: (playerId: string, name: string) => void;
52
57
  /** Called when a player disconnects. */
@@ -187,7 +192,11 @@ export function GameHostProvider<S extends IGameState, A extends IAction>({
187
192
 
188
193
  useEffect(() => {
189
194
  const port = config.wsPort || httpPort + DEFAULT_WS_PORT_OFFSET;
190
- const server = new GameWebSocketServer({ port, debug: config.debug });
195
+ const server = new GameWebSocketServer({
196
+ port,
197
+ debug: config.debug,
198
+ maxMessageBytes: config.maxMessageBytes,
199
+ });
191
200
 
192
201
  // Start the WebSocket server asynchronously
193
202
  server.start().catch((error) => {
package/src/websocket.ts CHANGED
@@ -12,10 +12,21 @@ import type {
12
12
  } from "react-native-nitro-http-server";
13
13
  import { EventEmitter } from "./event-emitter";
14
14
  import { generateId, DEFAULT_WS_PATH } from "@couch-kit/core";
15
+ import {
16
+ DEFAULT_MAX_MESSAGE_BYTES,
17
+ frameByteLength,
18
+ } from "./message-validation";
15
19
 
16
20
  export interface WebSocketConfig {
17
21
  port: number;
18
22
  debug?: boolean;
23
+ /**
24
+ * Maximum size (in bytes) of an inbound client message. Frames larger than
25
+ * this are discarded before parsing, bounding the memory a single client can
26
+ * force the host to allocate. Defaults to {@link DEFAULT_MAX_MESSAGE_BYTES}
27
+ * (256 KiB).
28
+ */
29
+ maxMessageBytes?: number;
19
30
  /**
20
31
  * @deprecated No effect. The underlying nitro-http WebSocket transport
21
32
  * manages frame sizing internally and this value is never read. This field
@@ -56,15 +67,18 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
56
67
  private clients: Map<string, WebSocketClient> = new Map();
57
68
  private port: number;
58
69
  private debug: boolean;
70
+ private maxMessageBytes: number;
59
71
 
60
72
  constructor(config: WebSocketConfig) {
61
73
  super();
62
74
  this.port = config.port;
63
75
  this.debug = !!config.debug;
64
- // Only `port` and `debug` are honored. `maxFrameSize`, `keepaliveInterval`,
65
- // and `keepaliveTimeout` are deprecated no-ops: the nitro-http WebSocket
66
- // transport does not expose these knobs, so they are intentionally ignored
67
- // (see the @deprecated tags on WebSocketConfig).
76
+ this.maxMessageBytes =
77
+ config.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
78
+ // Only `port`, `debug`, and `maxMessageBytes` are honored. `maxFrameSize`,
79
+ // `keepaliveInterval`, and `keepaliveTimeout` are deprecated no-ops: the
80
+ // nitro-http WebSocket transport does not expose these knobs, so they are
81
+ // intentionally ignored (see the @deprecated tags on WebSocketConfig).
68
82
  }
69
83
 
70
84
  private log(...args: unknown[]) {
@@ -93,6 +107,16 @@ export class GameWebSocketServer extends EventEmitter<WebSocketServerEvents> {
93
107
 
94
108
  // Handle incoming messages
95
109
  ws.onmessage = (event: { data: string | ArrayBuffer }) => {
110
+ // Reject oversized frames before parsing to bound memory usage.
111
+ const size = frameByteLength(event.data);
112
+ if (size > this.maxMessageBytes) {
113
+ this.log(
114
+ `[WebSocket] Message from ${socketId} exceeds limit ` +
115
+ `(${size} > ${this.maxMessageBytes} bytes), discarding`,
116
+ );
117
+ return;
118
+ }
119
+
96
120
  try {
97
121
  const data =
98
122
  typeof event.data === "string"