@digo-labs/ws 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -42,7 +42,7 @@ hub.getState('scores');
42
42
  hub.close();
43
43
  ```
44
44
 
45
- It also answers `GET /health` with `{ ok, clients }`. `tokenSecret` lets it verify the tokens clients send through `getToken`.
45
+ It also answers `GET /health` with `{ ok, clients }`. `tokenSecret` lets it verify the tokens clients send through `getToken`. With `requireToken: true` it closes with code `4401` every client that doesn't identify with a valid token within 5 seconds, or sends anything else first; it won't start without a `tokenSecret`. A tokenless `WebsocketClient` keeps reconnecting at its slowest interval, so after turning it on, clients that reconnect every few seconds and close with `4401` are tokenless clients, not an outage.
46
46
 
47
47
  ## Running the hub locally
48
48
 
@@ -51,4 +51,4 @@ npx digo-ws-hub # port 9900
51
51
  npx digo-ws-hub 9901 # another port
52
52
  ```
53
53
 
54
- The CLI reads `WS_TOKEN_SECRET` from the environment and shuts down cleanly on Ctrl+C. Inside the core monorepo, `npm run ws` (or `npm run dev`) starts the same hub from source.
54
+ The CLI reads `WS_TOKEN_SECRET` and `WS_REQUIRE_TOKEN` (`1` turns on `requireToken`, `0` or unset leaves it off, anything else stops the CLI) from the environment and shuts down cleanly on Ctrl+C. Inside the core monorepo, `npm run ws` (or `npm run dev`) starts the same hub from source.
package/dist/server.d.ts CHANGED
@@ -27,8 +27,10 @@ export declare class WebsocketHub {
27
27
  private readState;
28
28
  private broadcast;
29
29
  private deny;
30
+ private refuse;
31
+ private refuseUnidentified;
30
32
  private isExpired;
31
- private isRenewal;
33
+ private carriesToken;
32
34
  private isOverRate;
33
35
  private rawSize;
34
36
  private addLog;
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import { CHANNEL_NAMESPACE_PREFIX as e, RealtimeHelpers as t, WEBSOCKET_HUB_CHAN
2
2
  import c from "node:http";
3
3
  import { WebSocket as l, WebSocketServer as u } from "ws";
4
4
  // src/server.ts
5
- var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 4408, y = "/", b = class {
5
+ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 4408, y = 5e3, b = "/", x = class {
6
6
  httpServer;
7
7
  wss;
8
8
  config;
@@ -14,6 +14,7 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
14
14
  statusTimer = null;
15
15
  statusDirty = !1;
16
16
  constructor(e) {
17
+ if (e.requireToken && !e.tokenSecret) throw Error("requireToken needs a tokenSecret, or the hub refuses every client");
17
18
  this.config = e, this.httpServer = c.createServer((e, t) => {
18
19
  e.method === "GET" && e.url === "/health" ? (t.writeHead(200, { "Content-Type": "application/json" }), t.end(JSON.stringify({
19
20
  ok: !0,
@@ -42,17 +43,21 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
42
43
  channels: /* @__PURE__ */ new Set(),
43
44
  alive: !0,
44
45
  token: null,
46
+ identifying: null,
45
47
  windowStart: Date.now(),
46
48
  messages: 0
47
49
  };
48
- this.clients.add(t), e.on("error", () => {}), e.on("pong", () => {
50
+ this.clients.add(t);
51
+ let n = this.config.requireToken ? setTimeout(() => this.refuseUnidentified(t, e), y) : null;
52
+ e.on("error", () => {}), e.on("pong", () => {
49
53
  t.alive = !0;
50
54
  }), e.on("message", (n) => {
51
- if (t.token && (this.rawSize(n) > f || this.isOverRate(t))) return;
52
- let r = this.parseMessage(n);
53
- r && (this.isExpired(t) && !this.isRenewal(r) ? e.close(v) : o(this.handleMessage(t, e, r)));
55
+ let r = !t.token && !!this.config.requireToken;
56
+ if ((t.token || r) && (this.rawSize(n) > f || this.isOverRate(t))) return;
57
+ let i = this.parseMessage(n);
58
+ i ? this.isExpired(t) && !this.carriesToken(i) ? e.close(v) : o(this.handleMessage(t, e, i)) : r && this.refuse(e);
54
59
  }), e.on("close", () => {
55
- this.clients.delete(t), this.addLog(t, a.DISCONNECT, ""), this.broadcastStatus();
60
+ n && clearTimeout(n), this.clients.delete(t), this.addLog(t, a.DISCONNECT, ""), this.broadcastStatus();
56
61
  });
57
62
  }
58
63
  parseMessage(e) {
@@ -66,10 +71,13 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
66
71
  return n.success ? n.data : null;
67
72
  }
68
73
  async handleMessage(e, n, o) {
69
- switch (o.type) {
74
+ if (e.identifying && await e.identifying, this.config.requireToken && !e.token && !this.carriesToken(o)) this.refuse(n);
75
+ else switch (o.type) {
70
76
  case i.IDENTIFY:
71
77
  if (o.token) {
72
- await this.identifyWithToken(e, n, o.id, o.token);
78
+ e.identifying = this.identifyWithToken(e, n, o.id, o.token).finally(() => {
79
+ e.identifying = null;
80
+ }), await e.identifying;
73
81
  break;
74
82
  }
75
83
  if (e.token) {
@@ -141,7 +149,7 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
141
149
  async identifyWithToken(e, n, i, a) {
142
150
  let o = this.config.tokenSecret ? await t.verify(a, this.config.tokenSecret) : null;
143
151
  if (!o || e.token && e.token.app !== o.app) {
144
- this.deny(n), n.close(_);
152
+ this.refuse(n);
145
153
  return;
146
154
  }
147
155
  let s = !!e.token;
@@ -165,11 +173,11 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
165
173
  return e.token ? this.namespacedKey(e.token.app, t) : t;
166
174
  }
167
175
  namespacedKey(t, n) {
168
- return `${e}${t}${y}${n}`;
176
+ return `${e}${t}${b}${n}`;
169
177
  }
170
178
  storeState(t, n, r) {
171
179
  if (t.startsWith(e) && !this.state.has(t)) {
172
- let e = t.slice(0, t.indexOf(y) + 1);
180
+ let e = t.slice(0, t.indexOf(b) + 1);
173
181
  if ([...this.state.keys()].filter((t) => t.startsWith(e)).length >= h) return !1;
174
182
  }
175
183
  return this.state.set(t, {
@@ -197,10 +205,16 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
197
205
  };
198
206
  e.readyState === l.OPEN && e.send(JSON.stringify(n));
199
207
  }
208
+ refuse(e) {
209
+ this.deny(e), e.close(_);
210
+ }
211
+ refuseUnidentified(e, t) {
212
+ e.token || this.refuse(t);
213
+ }
200
214
  isExpired(e) {
201
215
  return !!e.token && e.token.exp * 1e3 <= Date.now();
202
216
  }
203
- isRenewal(e) {
217
+ carriesToken(e) {
204
218
  return e.type === i.IDENTIFY && !!e.token;
205
219
  }
206
220
  isOverRate(e) {
@@ -260,6 +274,6 @@ var d = 1048576, f = 65536, p = 1e3, m = 20, h = 1e3, g = 4e3, _ = 4401, v = 440
260
274
  }), this.wss.close(), this.httpServer.close();
261
275
  }
262
276
  };
263
- export { b as WebsocketHub };
277
+ export { x as WebsocketHub };
264
278
 
265
279
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["http","MAX_PAYLOAD_BYTES","MAX_TOKENED_BYTES","RATE_WINDOW","RATE_LIMIT","MAX_STATES_PER_APP","KICKED_CODE","DENIED_CODE","EXPIRED_CODE","NAMESPACE_SEPARATOR","WebsocketHub","config","req","res","WebSocketServer","socket","terminated","client","now","stored","key","raw","message","tryCatch","WebsocketLogEvent","data","result","websocketClientMessageSchema","WebsocketClientEvent","allowed","channel","stateMessage","WebSocketHubEvent","outgoing","h","RealtimeHelpers","existing","c","id","app","first","second","payload","token","isRefresh","identified","prefix","CHANNEL_NAMESPACE_PREFIX","namespace","retainSeconds","sender","WebSocket","denied","sum","chunk","watchers","WEBSOCKET_HUB_CHANNEL","legacyClients","legacyStates","status","handler"],"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { RawData, WebSocketServer, WebSocket } from 'ws';\n\nimport { CHANNEL_NAMESPACE_PREFIX, RealtimeHelpers, RealtimeTokenPayload, tryCatch, WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL, websocketClientMessageSchema } from '@digo-labs/common';\n\nconst MAX_PAYLOAD_BYTES = 1024 * 1024;\nconst MAX_TOKENED_BYTES = 64 * 1024;\nconst RATE_WINDOW = 1_000;\nconst RATE_LIMIT = 20;\nconst MAX_STATES_PER_APP = 1_000;\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst EXPIRED_CODE = 4408;\nconst NAMESPACE_SEPARATOR = '/';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n token: RealtimeTokenPayload | null;\n windowStart: number;\n messages: number;\n}\n\ninterface StoredState {\n data: unknown;\n expiresAt: number | null;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n private config: WebsocketsHubConfig;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, StoredState>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n private statusTimer: ReturnType<typeof setTimeout> | null = null;\n private statusDirty = false;\n\n constructor(config: WebsocketsHubConfig) {\n this.config = config;\n\n this.httpServer = http.createServer((req, res) => {\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, clients: this.clients.size }));\n return;\n }\n\n res.writeHead(404);\n res.end();\n });\n\n this.wss = new WebSocketServer({ server: this.httpServer, maxPayload: MAX_PAYLOAD_BYTES });\n\n this.heartbeatInterval = setInterval(() => this.heartbeat(), 30000);\n\n this.wss.on('close', () => clearInterval(this.heartbeatInterval));\n this.wss.on('connection', socket => this.handleConnection(socket));\n\n this.httpServer.listen(config.port);\n }\n\n private heartbeat(): void {\n let terminated = false;\n\n this.clients.forEach((client) => {\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n if (!client.alive) {\n client.ws.terminate();\n\n this.clients.delete(client);\n\n terminated = true;\n\n return;\n }\n\n client.alive = false;\n\n client.ws.ping();\n });\n\n const now = Date.now();\n\n this.state.forEach((stored, key) => {\n if (stored.expiresAt !== null && stored.expiresAt <= now) this.state.delete(key);\n });\n\n if (terminated) this.broadcastStatus();\n }\n\n private handleConnection(socket: WebSocket): void {\n const client: ConnectedClient = {\n id: 'unknown',\n app: 'unknown',\n ws: socket,\n channels: new Set(),\n alive: true,\n token: null,\n windowStart: Date.now(),\n messages: 0,\n };\n\n this.clients.add(client);\n\n socket.on('error', () => {});\n\n socket.on('pong', () => {\n client.alive = true;\n });\n\n socket.on('message', (raw) => {\n if (client.token && (this.rawSize(raw) > MAX_TOKENED_BYTES || this.isOverRate(client))) return;\n\n const message = this.parseMessage(raw);\n\n if (!message) return;\n\n if (this.isExpired(client) && !this.isRenewal(message)) {\n socket.close(EXPIRED_CODE);\n\n return;\n }\n\n void tryCatch(this.handleMessage(client, socket, message));\n });\n\n socket.on('close', () => {\n this.clients.delete(client);\n\n this.addLog(client, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private parseMessage(raw: RawData): WebsocketClientMessage | null {\n let data: unknown;\n\n try {\n data = JSON.parse(raw.toString());\n } catch { return null; }\n\n const result = websocketClientMessageSchema.safeParse(data);\n\n return result.success ? result.data : null;\n }\n\n private async handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): Promise<void> {\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n if (message.token) {\n await this.identifyWithToken(client, socket, message.id, message.token);\n\n break;\n }\n\n if (client.token) {\n this.deny(socket);\n\n break;\n }\n\n this.identify(client, message.id, message.app);\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n const allowed = message.channels.filter(channel => this.canSubscribe(client, channel));\n\n allowed.forEach((channel) => {\n const key = this.channelKey(client, channel);\n\n client.channels.add(key);\n\n const stored = this.readState(key);\n\n if (stored !== undefined) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: stored,\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n message.channels.filter(channel => !allowed.includes(channel)).forEach(channel => this.deny(socket, channel));\n\n this.addLog(client, WebsocketLogEvent.SUBSCRIBE, allowed.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(this.channelKey(client, channel)));\n\n this.addLog(client, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, this.channelKey(client, message.channel), outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const key = this.channelKey(client, message.channel);\n\n if (!this.storeState(key, message.data, client.token ? RealtimeHelpers.getRetainSeconds(client.token, message.channel) : null)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, key, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private identify(client: ConnectedClient, id: string, app: string): void {\n const existing = [...this.clients].find(c => c.id === id && c.app === app && c !== client && this.isSameCaller(c, client));\n\n if (existing) {\n existing.ws.close(KICKED_CODE);\n\n this.clients.delete(existing);\n }\n\n client.id = id;\n client.app = app;\n\n this.addLog(client, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n }\n\n private isSameCaller(first: ConnectedClient, second: ConnectedClient): boolean {\n if (!first.token || !second.token) return !first.token && !second.token;\n\n return first.token.subject === second.token.subject && first.token.tier !== 'public' && second.token.tier !== 'public';\n }\n\n private async identifyWithToken(client: ConnectedClient, socket: WebSocket, id: string, token: string): Promise<void> {\n const payload = this.config.tokenSecret ? await RealtimeHelpers.verify(token, this.config.tokenSecret) : null;\n\n if (!payload || (client.token && client.token.app !== payload.app)) {\n this.deny(socket);\n\n socket.close(DENIED_CODE);\n\n return;\n }\n\n const isRefresh = !!client.token;\n\n client.token = payload;\n\n if (isRefresh) {\n this.pruneChannels(client, socket);\n } else {\n this.identify(client, id, payload.app);\n }\n\n const identified: WebsocketHubMessage = { type: WebSocketHubEvent.IDENTIFIED };\n\n socket.send(JSON.stringify(identified));\n }\n\n private pruneChannels(client: ConnectedClient, socket: WebSocket): void {\n const prefix = this.channelKey(client, '');\n\n [...client.channels].map(key => key.slice(prefix.length)).filter(channel => !this.canSubscribe(client, channel)).forEach((channel) => {\n client.channels.delete(this.channelKey(client, channel));\n\n this.deny(socket, channel);\n });\n }\n\n private canSubscribe(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.subscribe, channel);\n }\n\n private canPublish(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.publish, channel);\n }\n\n private channelKey(client: ConnectedClient, channel: string): string {\n return client.token ? this.namespacedKey(client.token.app, channel) : channel;\n }\n\n private namespacedKey(app: string, channel: string): string {\n return `${CHANNEL_NAMESPACE_PREFIX}${app}${NAMESPACE_SEPARATOR}${channel}`;\n }\n\n private storeState(key: string, data: unknown, retainSeconds: number | null): boolean {\n if (key.startsWith(CHANNEL_NAMESPACE_PREFIX) && !this.state.has(key)) {\n const namespace = key.slice(0, key.indexOf(NAMESPACE_SEPARATOR) + 1);\n const count = [...this.state.keys()].filter(stored => stored.startsWith(namespace)).length;\n\n if (count >= MAX_STATES_PER_APP) return false;\n }\n\n this.state.set(key, { data: data, expiresAt: retainSeconds === null ? null : Date.now() + retainSeconds * 1000 });\n\n return true;\n }\n\n private readState(key: string): unknown {\n const stored = this.state.get(key);\n\n if (!stored) return undefined;\n\n if (stored.expiresAt !== null && stored.expiresAt <= Date.now()) {\n this.state.delete(key);\n\n return undefined;\n }\n\n return stored.data;\n }\n\n private broadcast(sender: ConnectedClient, key: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client === sender || !client.channels.has(key) || client.ws.readyState !== WebSocket.OPEN) return;\n\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n client.ws.send(payload);\n });\n }\n\n private deny(socket: WebSocket, channel?: string): void {\n const denied: WebsocketHubMessage = { type: WebSocketHubEvent.DENIED, channel: channel };\n\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(denied));\n }\n\n private isExpired(client: ConnectedClient): boolean {\n return !!client.token && client.token.exp * 1000 <= Date.now();\n }\n\n private isRenewal(message: WebsocketClientMessage): boolean {\n return message.type === WebsocketClientEvent.IDENTIFY && !!message.token;\n }\n\n private isOverRate(client: ConnectedClient): boolean {\n const now = Date.now();\n\n if (now - client.windowStart >= RATE_WINDOW) {\n client.windowStart = now;\n client.messages = 0;\n }\n\n client.messages++;\n\n return client.messages > RATE_LIMIT;\n }\n\n private rawSize(raw: RawData): number {\n if (Array.isArray(raw)) return raw.reduce((sum, chunk) => sum + chunk.length, 0);\n\n return raw instanceof ArrayBuffer ? raw.byteLength : raw.length;\n }\n\n private addLog(client: ConnectedClient, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n if (client.token) return;\n\n this.logs.push({ timestamp: Date.now(), sender: client.id, app: client.app, type: type, channel: channel, data: data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcastStatus(): void {\n if (this.statusTimer) {\n this.statusDirty = true;\n\n return;\n }\n\n this.sendStatus();\n\n this.statusTimer = setTimeout(() => {\n this.statusTimer = null;\n\n if (!this.statusDirty) return;\n\n this.statusDirty = false;\n\n this.broadcastStatus();\n }, 200);\n }\n\n private sendStatus(): void {\n const watchers = [...this.clients].filter(client => client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN);\n\n if (!watchers.length) return;\n\n const legacyClients = [...this.clients].filter(client => !client.token);\n const legacyStates = [...this.state].filter(([key]) => key !== WEBSOCKET_HUB_CHANNEL && !key.startsWith(CHANNEL_NAMESPACE_PREFIX));\n\n const status: WebsocketHubStatus = {\n clients: legacyClients.map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries(legacyStates.map(([key, stored]) => [key, stored.data])),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, { data: status, expiresAt: null });\n\n const message: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: WEBSOCKET_HUB_CHANNEL,\n data: status,\n };\n\n const payload = JSON.stringify(message);\n\n watchers.forEach(client => client.ws.send(payload));\n }\n\n public onMessage(handler: (channel: string, data: unknown) => void): void { this.messageHandlers.push(handler); }\n\n public getState(channel: string): unknown { return this.readState(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\n\n if (this.statusTimer) clearTimeout(this.statusTimer);\n\n this.clients.forEach((client) => {\n if (client.ws.readyState === WebSocket.OPEN) {\n client.ws.close(1001, 'Hub shutting down');\n }\n });\n\n this.wss.close();\n this.httpServer.close();\n }\n}\n"],"mappings":";;;;AAKA,IAAMC,IAAsB,SACtBC,IAAsB,OACtBC,IAAsB,KACtBC,IAAsB,IACtBC,IAAsB,KACtBC,IAAsB,KACtBC,IAAsB,MACtBC,IAAsB,MACtBC,IAAsB,KAkBfC,IAAb,MAA0B;CACxB;CACA;CACA;CAEA,0BAAkB,IAAI,IAAqB;CAC3C,wBAAkB,IAAI,IAAyB;CAE/C,OAAoD,CAAC;CACrD,kBAA0E,CAAC;CAC3E;CACA,cAAkE;CAClE,cAAsB;CAEtB,YAAY,GAA6B;EAqBvC,AApBA,KAAK,SAASC,GAEd,KAAK,aAAaX,EAAK,cAAc,GAAK,MAAQ;GAChD,AAAIY,EAAI,WAAW,SAASA,EAAI,QAAQ,aACtCC,EAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC,GACzDA,EAAI,IAAI,KAAK,UAAU;IAAE,IAAI;IAAM,SAAS,KAAK,QAAQ;GAAK,CAAC,CAAC,MAIlEA,EAAI,UAAU,GAAG,GACjBA,EAAI,IAAI;EACV,CAAC,GAED,KAAK,MAAM,IAAIC,EAAgB;GAAE,QAAQ,KAAK;GAAY,YAAYb;EAAkB,CAAC,GAEzF,KAAK,oBAAoB,kBAAkB,KAAK,UAAU,GAAG,GAAK,GAElE,KAAK,IAAI,GAAG,eAAoB,cAAc,KAAK,iBAAiB,CAAC,GACrE,KAAK,IAAI,GAAG,eAAc,MAAU,KAAK,iBAAiBc,CAAM,CAAC,GAEjE,KAAK,WAAW,OAAOJ,EAAO,IAAI;CACpC;CAEA,YAA0B;EACxB,IAAIK,IAAa;EAEjB,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAI,KAAK,UAAUC,CAAM,IACvBA,EAAO,GAAG,MAAMT,CAAY,IAKzBS,EAAO,SAUZ,EAAO,QAAQ,IAEfA,EAAO,GAAG,KAAK,MAXbA,EAAO,GAAG,UAAU,GAEpB,KAAK,QAAQ,OAAOA,CAAM,GAE1B,IAAa;EAQjB,CAAC;EAED,IAAMC,IAAM,KAAK,IAAI;EAMrB,AAJA,KAAK,MAAM,SAAS,GAAQ,MAAQ;GAClC,AAAIC,EAAO,cAAc,QAAQA,EAAO,aAAaD,KAAK,KAAK,MAAM,OAAOE,CAAG;EACjF,CAAC,GAEGJ,KAAY,KAAK,gBAAgB;CACvC;CAEA,iBAAyB,GAAyB;EAChD,IAAMC,IAA0B;GAC9B,IAAa;GACb,KAAa;GACb,IAAaF;GACb,0BAAa,IAAI,IAAI;GACrB,OAAa;GACb,OAAa;GACb,aAAa,KAAK,IAAI;GACtB,UAAa;EACf;EA0BA,AAxBA,KAAK,QAAQ,IAAIE,CAAM,GAEvBF,EAAO,GAAG,eAAe,CAAC,CAAC,GAE3BA,EAAO,GAAG,cAAc;GACtB,EAAO,QAAQ;EACjB,CAAC,GAEDA,EAAO,GAAG,YAAY,MAAQ;GAC5B,IAAIE,EAAO,UAAU,KAAK,QAAQI,CAAG,IAAInB,KAAqB,KAAK,WAAWe,CAAM,IAAI;GAExF,IAAMK,IAAU,KAAK,aAAaD,CAAG;GAErC,AAAKC,MAED,KAAK,UAAUL,CAAM,KAAK,CAAC,KAAK,UAAUK,CAAO,IACnDP,EAAO,MAAMP,CAAY,IAK3Be,EAAc,KAAK,cAAcN,GAAQF,GAAQO,CAAO,CAAC;EAC3D,CAAC,GAEDP,EAAO,GAAG,eAAe;GAKvB,AAJA,KAAK,QAAQ,OAAOE,CAAM,GAE1B,KAAK,OAAOA,GAAQO,EAAkB,YAAY,EAAE,GAEpD,KAAK,gBAAgB;EACvB,CAAC;CACH;CAEA,aAAqB,GAA6C;EAChE,IAAIC;EAEJ,IAAI;GACF,IAAO,KAAK,MAAMJ,EAAI,SAAS,CAAC;EAClC,QAAQ;GAAE,OAAO;EAAM;EAEvB,IAAMK,IAASC,EAA6B,UAAUF,CAAI;EAE1D,OAAOC,EAAO,UAAUA,EAAO,OAAO;CACxC;CAEA,MAAc,cAAc,GAAyB,GAAmB,GAAgD;EACtH,QAAQJ,EAAQ,MAAhB;GACE,KAAKM,EAAqB;IACxB,IAAIN,EAAQ,OAAO;KACjB,MAAM,KAAK,kBAAkBL,GAAQF,GAAQO,EAAQ,IAAIA,EAAQ,KAAK;KAEtE;IACF;IAEA,IAAIL,EAAO,OAAO;KAChB,KAAK,KAAKF,CAAM;KAEhB;IACF;IAEA,KAAK,SAASE,GAAQK,EAAQ,IAAIA,EAAQ,GAAG;IAC7C;GAGF,KAAKM,EAAqB,WAAW;IACnC,IAAMC,IAAUP,EAAQ,SAAS,QAAO,MAAW,KAAK,aAAaL,GAAQa,CAAO,CAAC;IAuBrF,AArBAD,EAAQ,SAAS,MAAY;KAC3B,IAAMT,IAAM,KAAK,WAAWH,GAAQa,CAAO;KAE3C,EAAO,SAAS,IAAIV,CAAG;KAEvB,IAAMD,IAAS,KAAK,UAAUC,CAAG;KAEjC,IAAID,MAAW,KAAA,GAAW;MACxB,IAAMY,IAAoC;OACxC,MAASC,EAAkB;OAClB;OACT,MAASb;MACX;MAEA,EAAO,KAAK,KAAK,UAAUY,CAAY,CAAC;KAC1C;IACF,CAAC,GAEDT,EAAQ,SAAS,QAAO,MAAW,CAACO,EAAQ,SAASC,CAAO,CAAC,CAAC,CAAC,SAAQ,MAAW,KAAK,KAAKf,GAAQe,CAAO,CAAC,GAE5G,KAAK,OAAOb,GAAQO,EAAkB,WAAWK,EAAQ,KAAK,IAAI,CAAC,GACnE,KAAK,gBAAgB;IACrB;GACF;GAEA,KAAKD,EAAqB;IAIxB,AAHAN,EAAQ,SAAS,SAAQ,MAAWL,EAAO,SAAS,OAAO,KAAK,WAAWA,GAAQa,CAAO,CAAC,CAAC,GAE5F,KAAK,OAAOb,GAAQO,EAAkB,aAAaF,EAAQ,SAAS,KAAK,IAAI,CAAC,GAC9E,KAAK,gBAAgB;IACrB;GAGF,KAAKM,EAAqB,SAAS;IACjC,IAAI,CAAC,KAAK,WAAWX,GAAQK,EAAQ,OAAO,GAAG;KAC7C,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMW,IAAgC;KACpC,MAASD,EAAkB;KAC3B,SAASV,EAAQ;KACjB,MAASA,EAAQ;IACnB;IAOA,AALA,KAAK,UAAUL,GAAQ,KAAK,WAAWA,GAAQK,EAAQ,OAAO,GAAGW,CAAQ,GAEzE,KAAK,gBAAgB,SAAQ,MAAKC,EAAEZ,EAAQ,SAASA,EAAQ,IAAI,CAAC,GAElE,KAAK,OAAOL,GAAQO,EAAkB,SAASF,EAAQ,SAASA,EAAQ,IAAI,GAC5E,KAAK,gBAAgB;IACrB;GACF;GAEA,KAAKM,EAAqB,OAAO;IAC/B,IAAI,CAAC,KAAK,WAAWX,GAAQK,EAAQ,OAAO,GAAG;KAC7C,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMF,IAAM,KAAK,WAAWH,GAAQK,EAAQ,OAAO;IAEnD,IAAI,CAAC,KAAK,WAAWF,GAAKE,EAAQ,MAAML,EAAO,QAAQkB,EAAgB,iBAAiBlB,EAAO,OAAOK,EAAQ,OAAO,IAAI,IAAI,GAAG;KAC9H,KAAK,KAAKP,GAAQO,EAAQ,OAAO;KAEjC;IACF;IAEA,IAAMW,IAAgC;KACpC,MAASD,EAAkB;KAC3B,SAASV,EAAQ;KACjB,MAASA,EAAQ;IACnB;IAQA,AANA,KAAK,UAAUL,GAAQG,GAAKa,CAAQ,GAEpC,KAAK,gBAAgB,SAAQ,MAAKC,EAAEZ,EAAQ,SAASA,EAAQ,IAAI,CAAC,GAElE,KAAK,OAAOL,GAAQO,EAAkB,OAAOF,EAAQ,SAASA,EAAQ,IAAI,GAE1E,KAAK,gBAAgB;IACrB;GACF;EACF;CACF;CAEA,SAAiB,GAAyB,GAAY,GAAmB;EACvE,IAAMc,IAAW,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,MAAK,MAAKC,EAAE,OAAOC,KAAMD,EAAE,QAAQE,KAAOF,MAAMpB,KAAU,KAAK,aAAaoB,GAAGpB,CAAM,CAAC;EAazH,AAXImB,MACFA,EAAS,GAAG,MAAM9B,CAAW,GAE7B,KAAK,QAAQ,OAAO8B,CAAQ,IAG9B,EAAO,KAAME,GACb,EAAO,MAAMC,GAEb,KAAK,OAAOtB,GAAQO,EAAkB,SAAS,EAAE,GAEjD,KAAK,gBAAgB;CACvB;CAEA,aAAqB,GAAwB,GAAkC;EAG7E,OAFI,CAACgB,EAAM,SAAS,CAACC,EAAO,QAAc,CAACD,EAAM,SAAS,CAACC,EAAO,QAE3DD,EAAM,MAAM,YAAYC,EAAO,MAAM,WAAWD,EAAM,MAAM,SAAS,YAAYC,EAAO,MAAM,SAAS;CAChH;CAEA,MAAc,kBAAkB,GAAyB,GAAmB,GAAY,GAA8B;EACpH,IAAMC,IAAU,KAAK,OAAO,cAAc,MAAMP,EAAgB,OAAOQ,GAAO,KAAK,OAAO,WAAW,IAAI;EAEzG,IAAI,CAACD,KAAYzB,EAAO,SAASA,EAAO,MAAM,QAAQyB,EAAQ,KAAM;GAGlE,AAFA,KAAK,KAAK3B,CAAM,GAEhBA,EAAO,MAAMR,CAAW;GAExB;EACF;EAEA,IAAMqC,IAAY,CAAC,CAAC3B,EAAO;EAI3B,AAFA,EAAO,QAAQyB,GAEXE,IACF,KAAK,cAAc3B,GAAQF,CAAM,IAEjC,KAAK,SAASE,GAAQqB,GAAII,EAAQ,GAAG;EAGvC,IAAMG,IAAkC,EAAE,MAAMb,EAAkB,WAAW;EAE7E,EAAO,KAAK,KAAK,UAAUa,CAAU,CAAC;CACxC;CAEA,cAAsB,GAAyB,GAAyB;EACtE,IAAMC,IAAS,KAAK,WAAW7B,GAAQ,EAAE;EAEzC,CAAC,GAAGA,EAAO,QAAQ,CAAC,CAAC,KAAI,MAAOG,EAAI,MAAM0B,EAAO,MAAM,CAAC,CAAC,CAAC,QAAO,MAAW,CAAC,KAAK,aAAa7B,GAAQa,CAAO,CAAC,CAAC,CAAC,SAAS,MAAY;GAGpI,AAFAb,EAAO,SAAS,OAAO,KAAK,WAAWA,GAAQa,CAAO,CAAC,GAEvD,KAAK,KAAKf,GAAQe,CAAO;EAC3B,CAAC;CACH;CAEA,aAAqB,GAAyB,GAA0B;EAGtE,OAFA,CAAKb,EAAO,SAELkB,EAAgB,WAAWlB,EAAO,MAAM,WAAWa,CAAO;CACnE;CAEA,WAAmB,GAAyB,GAA0B;EAGpE,OAFA,CAAKb,EAAO,SAELkB,EAAgB,WAAWlB,EAAO,MAAM,SAASa,CAAO;CACjE;CAEA,WAAmB,GAAyB,GAAyB;EACnE,OAAOb,EAAO,QAAQ,KAAK,cAAcA,EAAO,MAAM,KAAKa,CAAO,IAAIA;CACxE;CAEA,cAAsB,GAAa,GAAyB;EAC1D,OAAO,GAAGiB,IAA2BR,IAAM9B,IAAsBqB;CACnE;CAEA,WAAmB,GAAa,GAAe,GAAuC;EACpF,IAAIV,EAAI,WAAW2B,CAAwB,KAAK,CAAC,KAAK,MAAM,IAAI3B,CAAG,GAAG;GACpE,IAAM4B,IAAY5B,EAAI,MAAM,GAAGA,EAAI,QAAQX,CAAmB,IAAI,CAAC;GAGnE,IAFkB,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC,CAAC,QAAO,MAAUU,EAAO,WAAW6B,CAAS,CAAC,CAAC,CAAC,UAE3E3C,GAAoB,OAAO;EAC1C;EAIA,OAFA,KAAK,MAAM,IAAIe,GAAK;GAAQ;GAAM,WAAW6B,MAAkB,OAAO,OAAO,KAAK,IAAI,IAAIA,IAAgB;EAAK,CAAC,GAEzG;CACT;CAEA,UAAkB,GAAsB;EACtC,IAAM9B,IAAS,KAAK,MAAM,IAAIC,CAAG;EAEjC,IAAKD,GAEL;OAAIA,EAAO,cAAc,QAAQA,EAAO,aAAa,KAAK,IAAI,GAC5D,KAAK,MAAM,OAAOC,CAAG;QAKvB,OAAOD,EAAO;EAFd;CAGF;CAEA,UAAkB,GAAyB,GAAa,GAAoC;EAC1F,IAAMuB,IAAU,KAAK,UAAUpB,CAAO;EAEtC,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAIL,MAAWiC,KAAWjC,EAAO,SAAS,IAAIG,CAAG,KAAKH,EAAO,GAAG,eAAekC,EAAU,SAErF,KAAK,UAAUlC,CAAM,IACvBA,EAAO,GAAG,MAAMT,CAAY,IAK9BS,EAAO,GAAG,KAAKyB,CAAO;EACxB,CAAC;CACH;CAEA,KAAa,GAAmB,GAAwB;EACtD,IAAMU,IAA8B;GAAE,MAAMpB,EAAkB;GAAiB;EAAQ;EAEvF,AAAIjB,EAAO,eAAeoC,EAAU,QAAMpC,EAAO,KAAK,KAAK,UAAUqC,CAAM,CAAC;CAC9E;CAEA,UAAkB,GAAkC;EAClD,OAAO,CAAC,CAACnC,EAAO,SAASA,EAAO,MAAM,MAAM,OAAQ,KAAK,IAAI;CAC/D;CAEA,UAAkB,GAA0C;EAC1D,OAAOK,EAAQ,SAASM,EAAqB,YAAY,CAAC,CAACN,EAAQ;CACrE;CAEA,WAAmB,GAAkC;EACnD,IAAMJ,IAAM,KAAK,IAAI;EASrB,OAPIA,IAAMD,EAAO,eAAed,MAC9B,EAAO,cAAce,GACrB,EAAO,WAAc,IAGvB,EAAO,YAEAD,EAAO,WAAWb;CAC3B;CAEA,QAAgB,GAAsB;EAGpC,OAFI,MAAM,QAAQiB,CAAG,IAAUA,EAAI,QAAQ,GAAK,MAAUgC,IAAMC,EAAM,QAAQ,CAAC,IAExEjC,aAAe,cAAcA,EAAI,aAAaA,EAAI;CAC3D;CAEA,OAAe,GAAyB,GAAyB,GAAiB,GAAsB;EACtG,AAAIJ,EAAO,UAEX,KAAK,KAAK,KAAK;GAAE,WAAW,KAAK,IAAI;GAAG,QAAQA,EAAO;GAAI,KAAKA,EAAO;GAAW;GAAe;GAAe;EAAK,CAAC,GAElH,KAAK,KAAK,SAAS,OAAK,KAAK,KAAK,MAAM;CAC9C;CAEA,kBAAgC;EAC9B,AAAI,KAAK,cACP,KAAK,cAAc,MAKrB,KAAK,WAAW,GAEhB,KAAK,cAAc,iBAAiB;GAGlC,AAFA,KAAK,cAAc,MAEd,KAAK,gBAEV,KAAK,cAAc,IAEnB,KAAK,gBAAgB;EACvB,GAAG,GAAG;CACR;CAEA,aAA2B;EACzB,IAAMsC,IAAW,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,QAAO,MAAUtC,EAAO,SAAS,IAAIuC,CAAqB,KAAKvC,EAAO,GAAG,eAAekC,EAAU,IAAI;EAEzI,IAAI,CAACI,EAAS,QAAQ;EAEtB,IAAME,IAAgB,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,QAAO,MAAU,CAACxC,EAAO,KAAK,GAChEyC,IAAgB,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,QAAQ,CAACtC,OAASA,MAAQoC,KAAyB,CAACpC,EAAI,WAAW2B,CAAwB,CAAC,GAE5HY,IAA6B;GACjC,SAASF,EAAc,KAAI,OAAW;IAAE,IAAIxC,EAAO;IAAI,KAAKA,EAAO;IAAK,UAAU,CAAC,GAAGA,EAAO,QAAQ;GAAE,EAAE;GACzG,OAAS,OAAO,YAAYyC,EAAa,KAAK,CAACtC,GAAKD,OAAY,CAACC,GAAKD,EAAO,IAAI,CAAC,CAAC;GACnF,MAAS,KAAK;EAChB;EAEA,KAAK,MAAM,IAAIqC,GAAuB;GAAE,MAAMG;GAAQ,WAAW;EAAK,CAAC;EAEvE,IAAMrC,IAA+B;GACnC,MAASU,EAAkB;GAC3B,SAASwB;GACT,MAASG;EACX,GAEMjB,IAAU,KAAK,UAAUpB,CAAO;EAEtC,EAAS,SAAQ,MAAUL,EAAO,GAAG,KAAKyB,CAAO,CAAC;CACpD;CAEA,UAAiB,GAAyD;EAAE,KAAK,gBAAgB,KAAKkB,CAAO;CAAG;CAEhH,SAAgB,GAA0B;EAAE,OAAO,KAAK,UAAU9B,CAAO;CAAG;CAE5E,QAAqB;EAYnB,AAXA,cAAc,KAAK,iBAAiB,GAEhC,KAAK,eAAa,aAAa,KAAK,WAAW,GAEnD,KAAK,QAAQ,SAAS,MAAW;GAC/B,AAAIb,EAAO,GAAG,eAAekC,EAAU,QACrClC,EAAO,GAAG,MAAM,MAAM,mBAAmB;EAE7C,CAAC,GAED,KAAK,IAAI,MAAM,GACf,KAAK,WAAW,MAAM;CACxB;AACF"}
1
+ {"version":3,"file":"server.js","names":["http","MAX_PAYLOAD_BYTES","MAX_TOKENED_BYTES","RATE_WINDOW","RATE_LIMIT","MAX_STATES_PER_APP","KICKED_CODE","DENIED_CODE","EXPIRED_CODE","IDENTIFY_DEADLINE","NAMESPACE_SEPARATOR","WebsocketHub","config","req","res","WebSocketServer","socket","terminated","client","now","stored","key","deadline","isUnidentifiedOnStrictHub","raw","message","tryCatch","WebsocketLogEvent","data","result","websocketClientMessageSchema","WebsocketClientEvent","allowed","channel","stateMessage","WebSocketHubEvent","outgoing","h","RealtimeHelpers","existing","c","id","app","first","second","payload","token","isRefresh","identified","prefix","CHANNEL_NAMESPACE_PREFIX","namespace","retainSeconds","sender","WebSocket","denied","sum","chunk","watchers","WEBSOCKET_HUB_CHANNEL","legacyClients","legacyStates","status","handler"],"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport { RawData, WebSocketServer, WebSocket } from 'ws';\n\nimport { CHANNEL_NAMESPACE_PREFIX, RealtimeHelpers, RealtimeTokenPayload, tryCatch, WebsocketClientMessage, WebsocketClientEvent, WebSocketHubEvent, WebsocketHubMessage, WebsocketHubLogEntry, WebsocketLogEvent, WebsocketHubStatus, WebsocketsHubConfig, WEBSOCKET_HUB_CHANNEL, websocketClientMessageSchema } from '@digo-labs/common';\n\nconst MAX_PAYLOAD_BYTES = 1024 * 1024;\nconst MAX_TOKENED_BYTES = 64 * 1024;\nconst RATE_WINDOW = 1_000;\nconst RATE_LIMIT = 20;\nconst MAX_STATES_PER_APP = 1_000;\nconst KICKED_CODE = 4000;\nconst DENIED_CODE = 4401;\nconst EXPIRED_CODE = 4408;\nconst IDENTIFY_DEADLINE = 5_000;\nconst NAMESPACE_SEPARATOR = '/';\n\ninterface ConnectedClient {\n id: string;\n app: string;\n ws: WebSocket;\n channels: Set<string>;\n alive: boolean;\n token: RealtimeTokenPayload | null;\n identifying: Promise<void> | null;\n windowStart: number;\n messages: number;\n}\n\ninterface StoredState {\n data: unknown;\n expiresAt: number | null;\n}\n\nexport class WebsocketHub {\n private httpServer: http.Server;\n private wss: WebSocketServer;\n private config: WebsocketsHubConfig;\n\n private clients = new Set<ConnectedClient>();\n private state = new Map<string, StoredState>();\n\n private logs: WebsocketHubLogEntry[] = [];\n private messageHandlers: ((channel: string, data: unknown) => void)[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval>;\n private statusTimer: ReturnType<typeof setTimeout> | null = null;\n private statusDirty = false;\n\n constructor(config: WebsocketsHubConfig) {\n if (config.requireToken && !config.tokenSecret) throw new Error('requireToken needs a tokenSecret, or the hub refuses every client');\n\n this.config = config;\n\n this.httpServer = http.createServer((req, res) => {\n if (req.method === 'GET' && req.url === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, clients: this.clients.size }));\n return;\n }\n\n res.writeHead(404);\n res.end();\n });\n\n this.wss = new WebSocketServer({ server: this.httpServer, maxPayload: MAX_PAYLOAD_BYTES });\n\n this.heartbeatInterval = setInterval(() => this.heartbeat(), 30000);\n\n this.wss.on('close', () => clearInterval(this.heartbeatInterval));\n this.wss.on('connection', socket => this.handleConnection(socket));\n\n this.httpServer.listen(config.port);\n }\n\n private heartbeat(): void {\n let terminated = false;\n\n this.clients.forEach((client) => {\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n if (!client.alive) {\n client.ws.terminate();\n\n this.clients.delete(client);\n\n terminated = true;\n\n return;\n }\n\n client.alive = false;\n\n client.ws.ping();\n });\n\n const now = Date.now();\n\n this.state.forEach((stored, key) => {\n if (stored.expiresAt !== null && stored.expiresAt <= now) this.state.delete(key);\n });\n\n if (terminated) this.broadcastStatus();\n }\n\n private handleConnection(socket: WebSocket): void {\n const client: ConnectedClient = {\n id: 'unknown',\n app: 'unknown',\n ws: socket,\n channels: new Set(),\n alive: true,\n token: null,\n identifying: null,\n windowStart: Date.now(),\n messages: 0,\n };\n\n this.clients.add(client);\n\n const deadline = this.config.requireToken ? setTimeout(() => this.refuseUnidentified(client, socket), IDENTIFY_DEADLINE) : null;\n\n socket.on('error', () => {});\n\n socket.on('pong', () => {\n client.alive = true;\n });\n\n socket.on('message', (raw) => {\n const isUnidentifiedOnStrictHub = !client.token && !!this.config.requireToken;\n\n if ((client.token || isUnidentifiedOnStrictHub) && (this.rawSize(raw) > MAX_TOKENED_BYTES || this.isOverRate(client))) return;\n\n const message = this.parseMessage(raw);\n\n if (!message) {\n if (isUnidentifiedOnStrictHub) this.refuse(socket);\n\n return;\n }\n\n if (this.isExpired(client) && !this.carriesToken(message)) {\n socket.close(EXPIRED_CODE);\n\n return;\n }\n\n void tryCatch(this.handleMessage(client, socket, message));\n });\n\n socket.on('close', () => {\n if (deadline) clearTimeout(deadline);\n\n this.clients.delete(client);\n\n this.addLog(client, WebsocketLogEvent.DISCONNECT, '');\n\n this.broadcastStatus();\n });\n }\n\n private parseMessage(raw: RawData): WebsocketClientMessage | null {\n let data: unknown;\n\n try {\n data = JSON.parse(raw.toString());\n } catch { return null; }\n\n const result = websocketClientMessageSchema.safeParse(data);\n\n return result.success ? result.data : null;\n }\n\n private async handleMessage(client: ConnectedClient, socket: WebSocket, message: WebsocketClientMessage): Promise<void> {\n if (client.identifying) await client.identifying;\n\n if (this.config.requireToken && !client.token && !this.carriesToken(message)) {\n this.refuse(socket);\n\n return;\n }\n\n switch (message.type) {\n case WebsocketClientEvent.IDENTIFY: {\n if (message.token) {\n client.identifying = this.identifyWithToken(client, socket, message.id, message.token).finally(() => {\n client.identifying = null;\n });\n\n await client.identifying;\n\n break;\n }\n\n if (client.token) {\n this.deny(socket);\n\n break;\n }\n\n this.identify(client, message.id, message.app);\n break;\n }\n\n case WebsocketClientEvent.SUBSCRIBE: {\n const allowed = message.channels.filter(channel => this.canSubscribe(client, channel));\n\n allowed.forEach((channel) => {\n const key = this.channelKey(client, channel);\n\n client.channels.add(key);\n\n const stored = this.readState(key);\n\n if (stored !== undefined) {\n const stateMessage: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: channel,\n data: stored,\n };\n\n socket.send(JSON.stringify(stateMessage));\n }\n });\n\n message.channels.filter(channel => !allowed.includes(channel)).forEach(channel => this.deny(socket, channel));\n\n this.addLog(client, WebsocketLogEvent.SUBSCRIBE, allowed.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.UNSUBSCRIBE: {\n message.channels.forEach(channel => client.channels.delete(this.channelKey(client, channel)));\n\n this.addLog(client, WebsocketLogEvent.UNSUBSCRIBE, message.channels.join(', '));\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.MESSAGE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.MESSAGE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, this.channelKey(client, message.channel), outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.MESSAGE, message.channel, message.data);\n this.broadcastStatus();\n break;\n }\n\n case WebsocketClientEvent.STATE: {\n if (!this.canPublish(client, message.channel)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const key = this.channelKey(client, message.channel);\n\n if (!this.storeState(key, message.data, client.token ? RealtimeHelpers.getRetainSeconds(client.token, message.channel) : null)) {\n this.deny(socket, message.channel);\n\n break;\n }\n\n const outgoing: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: message.channel,\n data: message.data,\n };\n\n this.broadcast(client, key, outgoing);\n\n this.messageHandlers.forEach(h => h(message.channel, message.data));\n\n this.addLog(client, WebsocketLogEvent.STATE, message.channel, message.data);\n\n this.broadcastStatus();\n break;\n }\n }\n }\n\n private identify(client: ConnectedClient, id: string, app: string): void {\n const existing = [...this.clients].find(c => c.id === id && c.app === app && c !== client && this.isSameCaller(c, client));\n\n if (existing) {\n existing.ws.close(KICKED_CODE);\n\n this.clients.delete(existing);\n }\n\n client.id = id;\n client.app = app;\n\n this.addLog(client, WebsocketLogEvent.CONNECT, '');\n\n this.broadcastStatus();\n }\n\n private isSameCaller(first: ConnectedClient, second: ConnectedClient): boolean {\n if (!first.token || !second.token) return !first.token && !second.token;\n\n return first.token.subject === second.token.subject && first.token.tier !== 'public' && second.token.tier !== 'public';\n }\n\n private async identifyWithToken(client: ConnectedClient, socket: WebSocket, id: string, token: string): Promise<void> {\n const payload = this.config.tokenSecret ? await RealtimeHelpers.verify(token, this.config.tokenSecret) : null;\n\n if (!payload || (client.token && client.token.app !== payload.app)) {\n this.refuse(socket);\n\n return;\n }\n\n const isRefresh = !!client.token;\n\n client.token = payload;\n\n if (isRefresh) {\n this.pruneChannels(client, socket);\n } else {\n this.identify(client, id, payload.app);\n }\n\n const identified: WebsocketHubMessage = { type: WebSocketHubEvent.IDENTIFIED };\n\n socket.send(JSON.stringify(identified));\n }\n\n private pruneChannels(client: ConnectedClient, socket: WebSocket): void {\n const prefix = this.channelKey(client, '');\n\n [...client.channels].map(key => key.slice(prefix.length)).filter(channel => !this.canSubscribe(client, channel)).forEach((channel) => {\n client.channels.delete(this.channelKey(client, channel));\n\n this.deny(socket, channel);\n });\n }\n\n private canSubscribe(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.subscribe, channel);\n }\n\n private canPublish(client: ConnectedClient, channel: string): boolean {\n if (!client.token) return true;\n\n return RealtimeHelpers.matchesAny(client.token.publish, channel);\n }\n\n private channelKey(client: ConnectedClient, channel: string): string {\n return client.token ? this.namespacedKey(client.token.app, channel) : channel;\n }\n\n private namespacedKey(app: string, channel: string): string {\n return `${CHANNEL_NAMESPACE_PREFIX}${app}${NAMESPACE_SEPARATOR}${channel}`;\n }\n\n private storeState(key: string, data: unknown, retainSeconds: number | null): boolean {\n if (key.startsWith(CHANNEL_NAMESPACE_PREFIX) && !this.state.has(key)) {\n const namespace = key.slice(0, key.indexOf(NAMESPACE_SEPARATOR) + 1);\n const count = [...this.state.keys()].filter(stored => stored.startsWith(namespace)).length;\n\n if (count >= MAX_STATES_PER_APP) return false;\n }\n\n this.state.set(key, { data: data, expiresAt: retainSeconds === null ? null : Date.now() + retainSeconds * 1000 });\n\n return true;\n }\n\n private readState(key: string): unknown {\n const stored = this.state.get(key);\n\n if (!stored) return undefined;\n\n if (stored.expiresAt !== null && stored.expiresAt <= Date.now()) {\n this.state.delete(key);\n\n return undefined;\n }\n\n return stored.data;\n }\n\n private broadcast(sender: ConnectedClient, key: string, message: WebsocketHubMessage): void {\n const payload = JSON.stringify(message);\n\n this.clients.forEach((client) => {\n if (client === sender || !client.channels.has(key) || client.ws.readyState !== WebSocket.OPEN) return;\n\n if (this.isExpired(client)) {\n client.ws.close(EXPIRED_CODE);\n\n return;\n }\n\n client.ws.send(payload);\n });\n }\n\n private deny(socket: WebSocket, channel?: string): void {\n const denied: WebsocketHubMessage = { type: WebSocketHubEvent.DENIED, channel: channel };\n\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(denied));\n }\n\n private refuse(socket: WebSocket): void {\n this.deny(socket);\n\n socket.close(DENIED_CODE);\n }\n\n private refuseUnidentified(client: ConnectedClient, socket: WebSocket): void {\n if (!client.token) this.refuse(socket);\n }\n\n private isExpired(client: ConnectedClient): boolean {\n return !!client.token && client.token.exp * 1000 <= Date.now();\n }\n\n private carriesToken(message: WebsocketClientMessage): boolean {\n return message.type === WebsocketClientEvent.IDENTIFY && !!message.token;\n }\n\n private isOverRate(client: ConnectedClient): boolean {\n const now = Date.now();\n\n if (now - client.windowStart >= RATE_WINDOW) {\n client.windowStart = now;\n client.messages = 0;\n }\n\n client.messages++;\n\n return client.messages > RATE_LIMIT;\n }\n\n private rawSize(raw: RawData): number {\n if (Array.isArray(raw)) return raw.reduce((sum, chunk) => sum + chunk.length, 0);\n\n return raw instanceof ArrayBuffer ? raw.byteLength : raw.length;\n }\n\n private addLog(client: ConnectedClient, type: WebsocketLogEvent, channel: string, data?: unknown): void {\n if (client.token) return;\n\n this.logs.push({ timestamp: Date.now(), sender: client.id, app: client.app, type: type, channel: channel, data: data });\n\n if (this.logs.length > 808) this.logs.shift();\n }\n\n private broadcastStatus(): void {\n if (this.statusTimer) {\n this.statusDirty = true;\n\n return;\n }\n\n this.sendStatus();\n\n this.statusTimer = setTimeout(() => {\n this.statusTimer = null;\n\n if (!this.statusDirty) return;\n\n this.statusDirty = false;\n\n this.broadcastStatus();\n }, 200);\n }\n\n private sendStatus(): void {\n const watchers = [...this.clients].filter(client => client.channels.has(WEBSOCKET_HUB_CHANNEL) && client.ws.readyState === WebSocket.OPEN);\n\n if (!watchers.length) return;\n\n const legacyClients = [...this.clients].filter(client => !client.token);\n const legacyStates = [...this.state].filter(([key]) => key !== WEBSOCKET_HUB_CHANNEL && !key.startsWith(CHANNEL_NAMESPACE_PREFIX));\n\n const status: WebsocketHubStatus = {\n clients: legacyClients.map(client => ({ id: client.id, app: client.app, channels: [...client.channels] })),\n state: Object.fromEntries(legacyStates.map(([key, stored]) => [key, stored.data])),\n logs: this.logs,\n };\n\n this.state.set(WEBSOCKET_HUB_CHANNEL, { data: status, expiresAt: null });\n\n const message: WebsocketHubMessage = {\n type: WebSocketHubEvent.STATE,\n channel: WEBSOCKET_HUB_CHANNEL,\n data: status,\n };\n\n const payload = JSON.stringify(message);\n\n watchers.forEach(client => client.ws.send(payload));\n }\n\n public onMessage(handler: (channel: string, data: unknown) => void): void { this.messageHandlers.push(handler); }\n\n public getState(channel: string): unknown { return this.readState(channel); }\n\n public close(): void {\n clearInterval(this.heartbeatInterval);\n\n if (this.statusTimer) clearTimeout(this.statusTimer);\n\n this.clients.forEach((client) => {\n if (client.ws.readyState === WebSocket.OPEN) {\n client.ws.close(1001, 'Hub shutting down');\n }\n });\n\n this.wss.close();\n this.httpServer.close();\n }\n}\n"],"mappings":"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"}