@mantiq/realtime 0.5.23 → 0.6.1
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
|
@@ -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' }))
|
package/src/sse/SSEManager.ts
CHANGED
|
@@ -137,12 +137,26 @@ export class SSEManager {
|
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
139
|
* Subscribe an SSE connection to a channel.
|
|
140
|
-
*
|
|
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)) {
|