@mantiq/realtime 0.5.22 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantiq/realtime",
3
- "version": "0.5.22",
3
+ "version": "0.6.0",
4
4
  "description": "WebSocket, SSE, channels, broadcasting",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,19 @@ export interface RealtimeConfig {
20
20
  heartbeatInterval: number
21
21
  /** Close connection if no pong after this many ms. */
22
22
  heartbeatTimeout: number
23
+ /**
24
+ * Allow unauthenticated WebSocket connections.
25
+ * When false (default), connections are rejected unless an authenticator
26
+ * is registered via WebSocketServer.authenticate(). Must be explicitly
27
+ * set to true to allow anonymous connections.
28
+ */
29
+ allowAnonymous?: boolean
30
+ /**
31
+ * Maximum WebSocket message payload size in bytes.
32
+ * Messages exceeding this limit will cause the connection to be closed.
33
+ * Default: 65536 (64 KB). Passed to Bun.serve's WebSocket config.
34
+ */
35
+ maxPayloadLength?: number
23
36
  }
24
37
 
25
38
  /** SSE fallback settings. */
@@ -55,6 +68,8 @@ export const DEFAULT_CONFIG: RealtimeConfig = {
55
68
  maxConnections: 0,
56
69
  heartbeatInterval: 25_000,
57
70
  heartbeatTimeout: 10_000,
71
+ allowAnonymous: false,
72
+ maxPayloadLength: 65_536, // 64 KB
58
73
  },
59
74
  sse: {
60
75
  enabled: true,
@@ -25,6 +25,9 @@ export class WebSocketServer implements WebSocketHandler {
25
25
  constructor(private config: RealtimeConfig) {
26
26
  this.connections = new ConnectionManager(config)
27
27
  this.channels = new ChannelManager(config)
28
+
29
+ // Security: if allowAnonymous is not explicitly true and no authenticator
30
+ // is registered, connections will be rejected by default (see onUpgrade).
28
31
  }
29
32
 
30
33
  // ── Configuration ──────────────────────────────────────────────────────
@@ -70,6 +73,11 @@ export class WebSocketServer implements WebSocketHandler {
70
73
  }
71
74
  userId = result.userId
72
75
  metadata = result.metadata ?? {}
76
+ } else if (!this.config.websocket.allowAnonymous) {
77
+ // Security: reject connections by default when no authenticator is
78
+ // configured. The developer must either register an authenticator via
79
+ // authenticate() or explicitly set allowAnonymous: true in config.
80
+ return null
73
81
  }
74
82
 
75
83
  return {
@@ -96,10 +104,29 @@ export class WebSocketServer implements WebSocketHandler {
96
104
  }
97
105
  }
98
106
 
107
+ /**
108
+ * Returns the configured maxPayloadLength for Bun.serve's WebSocket config.
109
+ * This enforces a server-side limit on incoming message sizes.
110
+ */
111
+ getMaxPayloadLength(): number {
112
+ return this.config.websocket.maxPayloadLength ?? 65_536
113
+ }
114
+
99
115
  /**
100
116
  * Called when a message is received from a client.
101
117
  */
102
118
  async message(ws: RealtimeSocket, raw: string | Buffer): Promise<void> {
119
+ // Security: enforce message size limit. Bun enforces maxPayloadLength at
120
+ // the transport level, but we double-check here as a defense-in-depth
121
+ // measure in case the transport config is misconfigured.
122
+ const maxPayload = this.config.websocket.maxPayloadLength ?? 65_536
123
+ const messageSize = typeof raw === 'string' ? raw.length : raw.byteLength
124
+ if (messageSize > maxPayload) {
125
+ ws.send(serialize({ event: 'error', message: 'Message exceeds maximum allowed size' }))
126
+ ws.close(1009, 'Message too large')
127
+ return
128
+ }
129
+
103
130
  const msg = parseClientMessage(raw)
104
131
  if (!msg) {
105
132
  ws.send(serialize({ event: 'error', message: 'Invalid message format' }))
@@ -137,12 +137,26 @@ export class SSEManager {
137
137
 
138
138
  /**
139
139
  * Subscribe an SSE connection to a channel.
140
- * Note: SSE only supports public channels (no auth handshake).
140
+ *
141
+ * Security: private and presence channels (prefixed with `private-` or
142
+ * `presence-`) require authorization and are rejected via SSE, which has
143
+ * no auth handshake. Only public channels are allowed.
141
144
  */
142
145
  subscribe(connId: string, channel: string): boolean {
143
146
  const conn = this.connections.get(connId)
144
147
  if (!conn) return false
145
148
 
149
+ // Security: reject subscriptions to private/presence channels.
150
+ // SSE is a unidirectional transport with no auth handshake, so there
151
+ // is no way to authorize the client for private channels.
152
+ if (channel.startsWith('private-') || channel.startsWith('presence-')) {
153
+ this.sendEvent(conn, 'subscription_error', {
154
+ channel,
155
+ error: 'Private and presence channels are not available over SSE. Use WebSockets instead.',
156
+ })
157
+ return false
158
+ }
159
+
146
160
  conn.channels.add(channel)
147
161
 
148
162
  if (!this.channelSubscriptions.has(channel)) {