@webiny/api-websockets-standalone 6.6.0-alpha.4

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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/WebsocketsStandaloneFeature.d.ts +10 -0
  4. package/WebsocketsStandaloneFeature.js +11 -0
  5. package/WebsocketsStandaloneFeature.js.map +1 -0
  6. package/adapter/NodeWsAdapter.d.ts +22 -0
  7. package/adapter/NodeWsAdapter.js +58 -0
  8. package/adapter/NodeWsAdapter.js.map +1 -0
  9. package/adapter/abstractions.d.ts +18 -0
  10. package/adapter/abstractions.js +5 -0
  11. package/adapter/abstractions.js.map +1 -0
  12. package/adapter/types.d.ts +2 -0
  13. package/adapter/types.js +0 -0
  14. package/connectionManager/ServerConnectionManager.d.ts +18 -0
  15. package/connectionManager/ServerConnectionManager.js +69 -0
  16. package/connectionManager/ServerConnectionManager.js.map +1 -0
  17. package/connectionManager/abstractions.d.ts +33 -0
  18. package/connectionManager/abstractions.js +5 -0
  19. package/connectionManager/abstractions.js.map +1 -0
  20. package/heartbeat/HeartbeatManager.d.ts +9 -0
  21. package/heartbeat/HeartbeatManager.js +20 -0
  22. package/heartbeat/HeartbeatManager.js.map +1 -0
  23. package/index.d.ts +9 -0
  24. package/index.js +8 -0
  25. package/package.json +40 -0
  26. package/server/WebsocketsServer.d.ts +53 -0
  27. package/server/WebsocketsServer.js +188 -0
  28. package/server/WebsocketsServer.js.map +1 -0
  29. package/server/types.d.ts +39 -0
  30. package/server/types.js +0 -0
  31. package/transport/ServerWebsocketsTransport.d.ts +14 -0
  32. package/transport/ServerWebsocketsTransport.js +52 -0
  33. package/transport/ServerWebsocketsTransport.js.map +1 -0
  34. package/upgradeHandler/DefaultUpgradeHandler.d.ts +8 -0
  35. package/upgradeHandler/DefaultUpgradeHandler.js +15 -0
  36. package/upgradeHandler/DefaultUpgradeHandler.js.map +1 -0
  37. package/upgradeHandler/abstractions.d.ts +17 -0
  38. package/upgradeHandler/abstractions.js +5 -0
  39. package/upgradeHandler/abstractions.js.map +1 -0
  40. package/validator/ServerWebsocketsEventValidator.d.ts +4 -0
  41. package/validator/ServerWebsocketsEventValidator.js +53 -0
  42. package/validator/ServerWebsocketsEventValidator.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Webiny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @webiny/api-websockets-standalone
2
+
3
+ > [!NOTE]
4
+ > This package is part of the [Webiny](https://www.webiny.com) monorepo.
5
+ > It’s **included in every Webiny project by default** and is not meant to be used as a standalone package.
6
+
7
+ 📘 **Documentation:** [https://www.webiny.com/docs](https://www.webiny.com/docs)
8
+
9
+ ---
10
+
11
+ _This README file is automatically generated during the publish process._
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Per-request real-time transport for the standalone server: registers `ServerWebsocketsTransport`,
3
+ * overriding the domain's NullWebsocketsTransport. Resolves the shared connection manager + adapter
4
+ * from the root (registered there as singletons by the handler). Exposed as a Feature so the handler's
5
+ * `transports` wiring reads uniformly (`WebsocketsStandaloneFeature.register(c)`).
6
+ */
7
+ export declare const WebsocketsStandaloneFeature: {
8
+ name: string;
9
+ register(container: import("@webiny/di").Container): void;
10
+ };
@@ -0,0 +1,11 @@
1
+ import { createFeature } from "@webiny/feature/api";
2
+ import { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
3
+ const WebsocketsStandaloneFeature = createFeature({
4
+ name: "WebsocketsServer",
5
+ register (container) {
6
+ container.register(ServerWebsocketsTransport);
7
+ }
8
+ });
9
+ export { WebsocketsStandaloneFeature };
10
+
11
+ //# sourceMappingURL=WebsocketsStandaloneFeature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebsocketsStandaloneFeature.js","sources":["../src/WebsocketsStandaloneFeature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { ServerWebsocketsTransport } from \"~/transport/ServerWebsocketsTransport.js\";\n\n/**\n * Per-request real-time transport for the standalone server: registers `ServerWebsocketsTransport`,\n * overriding the domain's NullWebsocketsTransport. Resolves the shared connection manager + adapter\n * from the root (registered there as singletons by the handler). Exposed as a Feature so the handler's\n * `transports` wiring reads uniformly (`WebsocketsStandaloneFeature.register(c)`).\n */\nexport const WebsocketsStandaloneFeature = createFeature({\n name: \"WebsocketsServer\",\n register(container) {\n container.register(ServerWebsocketsTransport);\n }\n});\n"],"names":["WebsocketsStandaloneFeature","createFeature","container","ServerWebsocketsTransport"],"mappings":";;AASO,MAAMA,8BAA8BC,cAAc;IACrD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
@@ -0,0 +1,22 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { IncomingMessage } from "node:http";
3
+ import type { Duplex } from "node:stream";
4
+ import { WebSocket } from "ws";
5
+ import { WebsocketsServerAdapter } from "./abstractions.js";
6
+ declare class NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {
7
+ private wss;
8
+ private getWss;
9
+ start(_server: HttpServer): void;
10
+ handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
11
+ stop(): Promise<void>;
12
+ onConnection(cb: (socket: WebSocket, request: import("node:http").IncomingMessage) => void): void;
13
+ onMessage(socket: WebSocket, cb: (data: Buffer) => void): void;
14
+ onClose(socket: WebSocket, cb: (code: number, reason: Buffer) => void): void;
15
+ onError(socket: WebSocket, cb: (error: Error) => void): void;
16
+ send(socket: WebSocket, data: string): Promise<void>;
17
+ close(socket: WebSocket, code?: number, reason?: string): void;
18
+ }
19
+ export { NodeWsAdapterImpl };
20
+ export declare const NodeWsAdapter: typeof NodeWsAdapterImpl & {
21
+ __abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").IWebsocketsServerAdapter<unknown>>;
22
+ };
@@ -0,0 +1,58 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { WebsocketsServerAdapter } from "./abstractions.js";
3
+ class NodeWsAdapterImpl {
4
+ getWss() {
5
+ if (!this.wss) throw new Error("WebSocketServer has not been started. Call start() first.");
6
+ return this.wss;
7
+ }
8
+ start(_server) {
9
+ this.wss = new WebSocketServer({
10
+ noServer: true
11
+ });
12
+ }
13
+ handleUpgrade(request, socket, head) {
14
+ this.getWss().handleUpgrade(request, socket, head, (ws)=>{
15
+ this.getWss().emit("connection", ws, request);
16
+ });
17
+ }
18
+ stop() {
19
+ const wss = this.getWss();
20
+ for (const client of wss.clients)client.terminate();
21
+ return new Promise((resolve, reject)=>{
22
+ wss.close((err)=>{
23
+ if (err) return void reject(err);
24
+ resolve();
25
+ });
26
+ });
27
+ }
28
+ onConnection(cb) {
29
+ this.getWss().on("connection", cb);
30
+ }
31
+ onMessage(socket, cb) {
32
+ socket.on("message", cb);
33
+ }
34
+ onClose(socket, cb) {
35
+ socket.on("close", cb);
36
+ }
37
+ onError(socket, cb) {
38
+ socket.on("error", cb);
39
+ }
40
+ send(socket, data) {
41
+ return new Promise((resolve, reject)=>{
42
+ socket.send(data, (err)=>{
43
+ if (err) return void reject(err);
44
+ resolve();
45
+ });
46
+ });
47
+ }
48
+ close(socket, code, reason) {
49
+ socket.close(code, reason);
50
+ }
51
+ }
52
+ const NodeWsAdapter = WebsocketsServerAdapter.createImplementation({
53
+ implementation: NodeWsAdapterImpl,
54
+ dependencies: []
55
+ });
56
+ export { NodeWsAdapter, NodeWsAdapterImpl };
57
+
58
+ //# sourceMappingURL=NodeWsAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter/NodeWsAdapter.js","sources":["../../src/adapter/NodeWsAdapter.ts"],"sourcesContent":["import type { Server as HttpServer } from \"node:http\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { WebSocketServer } from \"ws\";\nimport { WebSocket } from \"ws\";\nimport { WebsocketsServerAdapter } from \"./abstractions.js\";\n\nclass NodeWsAdapterImpl implements WebsocketsServerAdapter.Interface<WebSocket> {\n private wss: WebSocketServer | undefined;\n\n private getWss(): WebSocketServer {\n if (!this.wss) {\n throw new Error(\"WebSocketServer has not been started. Call start() first.\");\n }\n return this.wss;\n }\n\n /* The server param is kept for interface compliance but not used —\n the HTTP upgrade event is handled externally via handleUpgrade(). */\n public start(_server: HttpServer): void {\n this.wss = new WebSocketServer({ noServer: true });\n }\n\n public handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void {\n this.getWss().handleUpgrade(request, socket, head, ws => {\n this.getWss().emit(\"connection\", ws, request);\n });\n }\n\n public stop(): Promise<void> {\n const wss = this.getWss();\n /* Terminate all open client connections so wss.close() resolves immediately. */\n for (const client of wss.clients) {\n client.terminate();\n }\n return new Promise((resolve, reject) => {\n wss.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public onConnection(\n cb: (socket: WebSocket, request: import(\"node:http\").IncomingMessage) => void\n ): void {\n this.getWss().on(\"connection\", cb);\n }\n\n public onMessage(socket: WebSocket, cb: (data: Buffer) => void): void {\n socket.on(\"message\", cb);\n }\n\n public onClose(socket: WebSocket, cb: (code: number, reason: Buffer) => void): void {\n socket.on(\"close\", cb);\n }\n\n public onError(socket: WebSocket, cb: (error: Error) => void): void {\n socket.on(\"error\", cb);\n }\n\n public send(socket: WebSocket, data: string): Promise<void> {\n return new Promise((resolve, reject) => {\n socket.send(data, err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n\n public close(socket: WebSocket, code?: number, reason?: string): void {\n socket.close(code, reason);\n }\n}\n\nexport { NodeWsAdapterImpl };\n\nexport const NodeWsAdapter = WebsocketsServerAdapter.createImplementation({\n implementation: NodeWsAdapterImpl,\n dependencies: []\n});\n"],"names":["NodeWsAdapterImpl","Error","_server","WebSocketServer","request","socket","head","ws","wss","client","Promise","resolve","reject","err","cb","data","code","reason","NodeWsAdapter","WebsocketsServerAdapter"],"mappings":";;AAOA,MAAMA;IAGM,SAA0B;QAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,EACT,MAAM,IAAIC,MAAM;QAEpB,OAAO,IAAI,CAAC,GAAG;IACnB;IAIO,MAAMC,OAAmB,EAAQ;QACpC,IAAI,CAAC,GAAG,GAAG,IAAIC,gBAAgB;YAAE,UAAU;QAAK;IACpD;IAEO,cAAcC,OAAwB,EAAEC,MAAc,EAAEC,IAAY,EAAQ;QAC/E,IAAI,CAAC,MAAM,GAAG,aAAa,CAACF,SAASC,QAAQC,MAAMC,CAAAA;YAC/C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,cAAcA,IAAIH;QACzC;IACJ;IAEO,OAAsB;QACzB,MAAMI,MAAM,IAAI,CAAC,MAAM;QAEvB,KAAK,MAAMC,UAAUD,IAAI,OAAO,CAC5BC,OAAO,SAAS;QAEpB,OAAO,IAAIC,QAAQ,CAACC,SAASC;YACzBJ,IAAI,KAAK,CAACK,CAAAA;gBACN,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,aACHG,EAA6E,EACzE;QACJ,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,cAAcA;IACnC;IAEO,UAAUT,MAAiB,EAAES,EAA0B,EAAQ;QAClET,OAAO,EAAE,CAAC,WAAWS;IACzB;IAEO,QAAQT,MAAiB,EAAES,EAA0C,EAAQ;QAChFT,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,QAAQT,MAAiB,EAAES,EAA0B,EAAQ;QAChET,OAAO,EAAE,CAAC,SAASS;IACvB;IAEO,KAAKT,MAAiB,EAAEU,IAAY,EAAiB;QACxD,OAAO,IAAIL,QAAQ,CAACC,SAASC;YACzBP,OAAO,IAAI,CAACU,MAAMF,CAAAA;gBACd,IAAIA,KAAK,YACLD,OAAOC;gBAGXF;YACJ;QACJ;IACJ;IAEO,MAAMN,MAAiB,EAAEW,IAAa,EAAEC,MAAe,EAAQ;QAClEZ,OAAO,KAAK,CAACW,MAAMC;IACvB;AACJ;AAIO,MAAMC,gBAAgBC,wBAAwB,oBAAoB,CAAC;IACtE,gBAAgBnB;IAChB,cAAc,EAAE;AACpB"}
@@ -0,0 +1,18 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { IncomingMessage } from "node:http";
3
+ import type { Duplex } from "node:stream";
4
+ export interface IWebsocketsServerAdapter<TSocket> {
5
+ start(server: HttpServer): void;
6
+ stop(): Promise<void>;
7
+ onConnection(cb: (socket: TSocket, request: IncomingMessage) => void): void;
8
+ onMessage(socket: TSocket, cb: (data: Buffer) => void): void;
9
+ onClose(socket: TSocket, cb: (code: number, reason: Buffer) => void): void;
10
+ onError(socket: TSocket, cb: (error: Error) => void): void;
11
+ send(socket: TSocket, data: string): Promise<void>;
12
+ close(socket: TSocket, code?: number, reason?: string): void;
13
+ handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
14
+ }
15
+ export declare const WebsocketsServerAdapter: import("@webiny/di").Abstraction<IWebsocketsServerAdapter<unknown>>;
16
+ export declare namespace WebsocketsServerAdapter {
17
+ type Interface<TSocket> = IWebsocketsServerAdapter<TSocket>;
18
+ }
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const WebsocketsServerAdapter = createAbstraction("WebsocketsServerAdapter");
3
+ export { WebsocketsServerAdapter };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter/abstractions.js","sources":["../../src/adapter/abstractions.ts"],"sourcesContent":["import type { Server as HttpServer } from \"node:http\";\nimport type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface IWebsocketsServerAdapter<TSocket> {\n start(server: HttpServer): void;\n stop(): Promise<void>;\n onConnection(cb: (socket: TSocket, request: IncomingMessage) => void): void;\n onMessage(socket: TSocket, cb: (data: Buffer) => void): void;\n onClose(socket: TSocket, cb: (code: number, reason: Buffer) => void): void;\n onError(socket: TSocket, cb: (error: Error) => void): void;\n send(socket: TSocket, data: string): Promise<void>;\n close(socket: TSocket, code?: number, reason?: string): void;\n handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;\n}\n\nexport const WebsocketsServerAdapter =\n createAbstraction<IWebsocketsServerAdapter<unknown>>(\"WebsocketsServerAdapter\");\n\nexport namespace WebsocketsServerAdapter {\n export type Interface<TSocket> = IWebsocketsServerAdapter<TSocket>;\n}\n"],"names":["WebsocketsServerAdapter","createAbstraction"],"mappings":";AAiBO,MAAMA,0BACTC,kBAAqD"}
@@ -0,0 +1,2 @@
1
+ import type { WebSocket } from "ws";
2
+ export type NodeSocket = WebSocket;
File without changes
@@ -0,0 +1,18 @@
1
+ import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
+ import { WebsocketsConnectionManager } from "./abstractions.js";
3
+ export declare class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {
4
+ private readonly registry;
5
+ private readonly sockets;
6
+ private readonly metadata;
7
+ constructor(registry: ConnectionRegistry.Interface);
8
+ add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void>;
9
+ remove(connectionId: string): Promise<void>;
10
+ getSocket(connectionId: string): unknown | undefined;
11
+ getMetadata(connectionId: string): WebsocketsConnectionManager.ConnectionMetadata | undefined;
12
+ updateLastSeen(connectionId: string): Promise<void>;
13
+ cleanup(maxAge: number): Promise<string[]>;
14
+ getActiveConnectionIds(): string[];
15
+ }
16
+ export declare const ServerConnectionManager: typeof ServerConnectionManagerImpl & {
17
+ __abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").IWebsocketsConnectionManager<unknown>>;
18
+ };
@@ -0,0 +1,69 @@
1
+ import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
+ import { WebsocketsConnectionManager } from "./abstractions.js";
3
+ class ServerConnectionManagerImpl {
4
+ constructor(registry){
5
+ this.registry = registry;
6
+ this.sockets = new Map();
7
+ this.metadata = new Map();
8
+ }
9
+ async add(params) {
10
+ this.sockets.set(params.connectionId, params.socket);
11
+ this.metadata.set(params.connectionId, {
12
+ connectionId: params.connectionId,
13
+ endpoint: params.endpoint,
14
+ connectedAt: params.connectedAt,
15
+ host: params.host,
16
+ headers: params.headers
17
+ });
18
+ await this.registry.register({
19
+ connectionId: params.connectionId,
20
+ identity: params.identity,
21
+ tenant: params.tenant,
22
+ endpoint: params.endpoint,
23
+ connectedOn: new Date(params.connectedAt).toISOString()
24
+ });
25
+ }
26
+ async remove(connectionId) {
27
+ this.sockets.delete(connectionId);
28
+ this.metadata.delete(connectionId);
29
+ try {
30
+ await this.registry.unregister({
31
+ connectionId
32
+ });
33
+ } catch (err) {
34
+ if ("CONNECTION_NOT_FOUND" === err.code) return;
35
+ throw err;
36
+ }
37
+ }
38
+ getSocket(connectionId) {
39
+ return this.sockets.get(connectionId);
40
+ }
41
+ getMetadata(connectionId) {
42
+ return this.metadata.get(connectionId);
43
+ }
44
+ async updateLastSeen(connectionId) {
45
+ await this.registry.updateLastSeen(connectionId);
46
+ }
47
+ async cleanup(maxAge) {
48
+ const olderThan = new Date(Date.now() - maxAge);
49
+ const stale = await this.registry.listStale(olderThan);
50
+ const removed = [];
51
+ for (const entry of stale){
52
+ await this.remove(entry.connectionId);
53
+ removed.push(entry.connectionId);
54
+ }
55
+ return removed;
56
+ }
57
+ getActiveConnectionIds() {
58
+ return Array.from(this.sockets.keys());
59
+ }
60
+ }
61
+ const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({
62
+ implementation: ServerConnectionManagerImpl,
63
+ dependencies: [
64
+ ConnectionRegistry
65
+ ]
66
+ });
67
+ export { ServerConnectionManager, ServerConnectionManagerImpl };
68
+
69
+ //# sourceMappingURL=ServerConnectionManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectionManager/ServerConnectionManager.js","sources":["../../src/connectionManager/ServerConnectionManager.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { WebsocketsConnectionManager } from \"./abstractions.js\";\n\nexport class ServerConnectionManagerImpl implements WebsocketsConnectionManager.Interface<unknown> {\n private readonly sockets = new Map<string, unknown>();\n private readonly metadata = new Map<string, WebsocketsConnectionManager.ConnectionMetadata>();\n\n public constructor(private readonly registry: ConnectionRegistry.Interface) {}\n\n public async add(params: WebsocketsConnectionManager.AddParams<unknown>): Promise<void> {\n this.sockets.set(params.connectionId, params.socket);\n this.metadata.set(params.connectionId, {\n connectionId: params.connectionId,\n endpoint: params.endpoint,\n connectedAt: params.connectedAt,\n host: params.host,\n headers: params.headers\n });\n\n // Save the connection to the shared registry. This is what lets the server later push a\n // message to a specific user — e.g. when a background task finishes and calls\n // SendToIdentity. That flow looks connections up in the registry by identity id to get their\n // connection ids, then comes back here to fetch each live socket by connection id and send.\n // The in-memory map above only answers \"socket for this connection id?\" — it can't be\n // searched by identity, so without this registry row a targeted send finds nobody. The\n // registry stores `connectedOn` as an ISO string (it compares connections against a recency\n // cutoff), so we convert the epoch-ms `connectedAt`.\n await this.registry.register({\n connectionId: params.connectionId,\n identity: params.identity,\n tenant: params.tenant,\n endpoint: params.endpoint,\n connectedOn: new Date(params.connectedAt).toISOString()\n });\n }\n\n public async remove(connectionId: string): Promise<void> {\n this.sockets.delete(connectionId);\n this.metadata.delete(connectionId);\n try {\n await this.registry.unregister({ connectionId });\n } catch (err) {\n if ((err as any).code === \"CONNECTION_NOT_FOUND\") {\n return;\n }\n throw err;\n }\n }\n\n public getSocket(connectionId: string): unknown | undefined {\n return this.sockets.get(connectionId);\n }\n\n public getMetadata(\n connectionId: string\n ): WebsocketsConnectionManager.ConnectionMetadata | undefined {\n return this.metadata.get(connectionId);\n }\n\n public async updateLastSeen(connectionId: string): Promise<void> {\n await this.registry.updateLastSeen(connectionId);\n }\n\n public async cleanup(maxAge: number): Promise<string[]> {\n const olderThan = new Date(Date.now() - maxAge);\n const stale = await this.registry.listStale(olderThan);\n const removed: string[] = [];\n for (const entry of stale) {\n await this.remove(entry.connectionId);\n removed.push(entry.connectionId);\n }\n return removed;\n }\n\n public getActiveConnectionIds(): string[] {\n return Array.from(this.sockets.keys());\n }\n}\n\nexport const ServerConnectionManager = WebsocketsConnectionManager.createImplementation({\n implementation: ServerConnectionManagerImpl,\n dependencies: [ConnectionRegistry]\n});\n"],"names":["ServerConnectionManagerImpl","registry","Map","params","Date","connectionId","err","maxAge","olderThan","stale","removed","entry","Array","ServerConnectionManager","WebsocketsConnectionManager","ConnectionRegistry"],"mappings":";;AAGO,MAAMA;IAIT,YAAoCC,QAAsC,CAAE;aAAxCA,QAAQ,GAARA;aAHnB,OAAO,GAAG,IAAIC;aACd,QAAQ,GAAG,IAAIA;IAE6C;IAE7E,MAAa,IAAIC,MAAsD,EAAiB;QACpF,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA,OAAO,YAAY,EAAEA,OAAO,MAAM;QACnD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA,OAAO,YAAY,EAAE;YACnC,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,aAAaA,OAAO,WAAW;YAC/B,MAAMA,OAAO,IAAI;YACjB,SAASA,OAAO,OAAO;QAC3B;QAUA,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACzB,cAAcA,OAAO,YAAY;YACjC,UAAUA,OAAO,QAAQ;YACzB,QAAQA,OAAO,MAAM;YACrB,UAAUA,OAAO,QAAQ;YACzB,aAAa,IAAIC,KAAKD,OAAO,WAAW,EAAE,WAAW;QACzD;IACJ;IAEA,MAAa,OAAOE,YAAoB,EAAiB;QACrD,IAAI,CAAC,OAAO,CAAC,MAAM,CAACA;QACpB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAACA;QACrB,IAAI;YACA,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAEA;YAAa;QAClD,EAAE,OAAOC,KAAK;YACV,IAAKA,AAAqB,2BAArBA,IAAY,IAAI,EACjB;YAEJ,MAAMA;QACV;IACJ;IAEO,UAAUD,YAAoB,EAAuB;QACxD,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAACA;IAC5B;IAEO,YACHA,YAAoB,EACsC;QAC1D,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAACA;IAC7B;IAEA,MAAa,eAAeA,YAAoB,EAAiB;QAC7D,MAAM,IAAI,CAAC,QAAQ,CAAC,cAAc,CAACA;IACvC;IAEA,MAAa,QAAQE,MAAc,EAAqB;QACpD,MAAMC,YAAY,IAAIJ,KAAKA,KAAK,GAAG,KAAKG;QACxC,MAAME,QAAQ,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAACD;QAC5C,MAAME,UAAoB,EAAE;QAC5B,KAAK,MAAMC,SAASF,MAAO;YACvB,MAAM,IAAI,CAAC,MAAM,CAACE,MAAM,YAAY;YACpCD,QAAQ,IAAI,CAACC,MAAM,YAAY;QACnC;QACA,OAAOD;IACX;IAEO,yBAAmC;QACtC,OAAOE,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI;IACvC;AACJ;AAEO,MAAMC,0BAA0BC,4BAA4B,oBAAoB,CAAC;IACpF,gBAAgBd;IAChB,cAAc;QAACe;KAAmB;AACtC"}
@@ -0,0 +1,33 @@
1
+ import { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
2
+ export interface IWebsocketsConnectionManagerAddParams<TSocket> {
3
+ connectionId: string;
4
+ socket: TSocket;
5
+ endpoint: string;
6
+ identity: ConnectionRegistry.Identity;
7
+ tenant: string;
8
+ connectedAt: number;
9
+ host: string;
10
+ headers: Record<string, string>;
11
+ }
12
+ export interface IWebsocketsConnectionManagerMetadata {
13
+ connectionId: string;
14
+ endpoint: string;
15
+ connectedAt: number;
16
+ host: string;
17
+ headers: Record<string, string>;
18
+ }
19
+ export interface IWebsocketsConnectionManager<TSocket> {
20
+ add(params: IWebsocketsConnectionManagerAddParams<TSocket>): Promise<void>;
21
+ remove(connectionId: string): Promise<void>;
22
+ getSocket(connectionId: string): TSocket | undefined;
23
+ getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;
24
+ updateLastSeen(connectionId: string): Promise<void>;
25
+ cleanup(maxAge: number): Promise<string[]>;
26
+ getActiveConnectionIds(): string[];
27
+ }
28
+ export declare const WebsocketsConnectionManager: import("@webiny/di").Abstraction<IWebsocketsConnectionManager<unknown>>;
29
+ export declare namespace WebsocketsConnectionManager {
30
+ type Interface<TSocket> = IWebsocketsConnectionManager<TSocket>;
31
+ type AddParams<TSocket> = IWebsocketsConnectionManagerAddParams<TSocket>;
32
+ type ConnectionMetadata = IWebsocketsConnectionManagerMetadata;
33
+ }
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const WebsocketsConnectionManager = createAbstraction("WebsocketsConnectionManager");
3
+ export { WebsocketsConnectionManager };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectionManager/abstractions.js","sources":["../../src/connectionManager/abstractions.ts"],"sourcesContent":["import { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface IWebsocketsConnectionManagerAddParams<TSocket> {\n connectionId: string;\n socket: TSocket;\n endpoint: string;\n identity: ConnectionRegistry.Identity;\n tenant: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManagerMetadata {\n connectionId: string;\n endpoint: string;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n}\n\nexport interface IWebsocketsConnectionManager<TSocket> {\n add(params: IWebsocketsConnectionManagerAddParams<TSocket>): Promise<void>;\n remove(connectionId: string): Promise<void>;\n getSocket(connectionId: string): TSocket | undefined;\n getMetadata(connectionId: string): IWebsocketsConnectionManagerMetadata | undefined;\n updateLastSeen(connectionId: string): Promise<void>;\n cleanup(maxAge: number): Promise<string[]>;\n getActiveConnectionIds(): string[];\n}\n\nexport const WebsocketsConnectionManager = createAbstraction<IWebsocketsConnectionManager<unknown>>(\n \"WebsocketsConnectionManager\"\n);\n\nexport namespace WebsocketsConnectionManager {\n export type Interface<TSocket> = IWebsocketsConnectionManager<TSocket>;\n export type AddParams<TSocket> = IWebsocketsConnectionManagerAddParams<TSocket>;\n export type ConnectionMetadata = IWebsocketsConnectionManagerMetadata;\n}\n"],"names":["WebsocketsConnectionManager","createAbstraction"],"mappings":";AAgCO,MAAMA,8BAA8BC,kBACvC"}
@@ -0,0 +1,9 @@
1
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
2
+ export declare class HeartbeatManager {
3
+ private readonly connectionManager;
4
+ private readonly interval;
5
+ private timer;
6
+ constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, interval: number);
7
+ start(): void;
8
+ stop(): void;
9
+ }
@@ -0,0 +1,20 @@
1
+ class HeartbeatManager {
2
+ constructor(connectionManager, interval){
3
+ this.connectionManager = connectionManager;
4
+ this.interval = interval;
5
+ }
6
+ start() {
7
+ this.timer = setInterval(()=>{
8
+ this.connectionManager.cleanup(5 * this.interval);
9
+ }, this.interval);
10
+ }
11
+ stop() {
12
+ if (this.timer) {
13
+ clearInterval(this.timer);
14
+ this.timer = void 0;
15
+ }
16
+ }
17
+ }
18
+ export { HeartbeatManager };
19
+
20
+ //# sourceMappingURL=HeartbeatManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartbeat/HeartbeatManager.js","sources":["../../src/heartbeat/HeartbeatManager.ts"],"sourcesContent":["import type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\n\nexport class HeartbeatManager {\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n private readonly interval: number;\n private timer: ReturnType<typeof setInterval> | undefined;\n\n public constructor(\n connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n interval: number\n ) {\n this.connectionManager = connectionManager;\n this.interval = interval;\n }\n\n public start(): void {\n this.timer = setInterval(() => {\n this.connectionManager.cleanup(5 * this.interval);\n }, this.interval);\n }\n\n public stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n }\n}\n"],"names":["HeartbeatManager","connectionManager","interval","setInterval","clearInterval","undefined"],"mappings":"AAEO,MAAMA;IAKT,YACIC,iBAAiE,EACjEC,QAAgB,CAClB;QACE,IAAI,CAAC,iBAAiB,GAAGD;QACzB,IAAI,CAAC,QAAQ,GAAGC;IACpB;IAEO,QAAc;QACjB,IAAI,CAAC,KAAK,GAAGC,YAAY;YACrB,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ;QACpD,GAAG,IAAI,CAAC,QAAQ;IACpB;IAEO,OAAa;QAChB,IAAI,IAAI,CAAC,KAAK,EAAE;YACZC,cAAc,IAAI,CAAC,KAAK;YACxB,IAAI,CAAC,KAAK,GAAGC;QACjB;IACJ;AACJ"}
package/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { createWebsocketsServer, attachWebsocketsServer } from "./server/WebsocketsServer.js";
2
+ export type { IWebsocketsServer, WebsocketsConnectionAuthenticator } from "./server/types.js";
3
+ export * from "./adapter/abstractions.js";
4
+ export * from "./upgradeHandler/abstractions.js";
5
+ export * from "./connectionManager/abstractions.js";
6
+ export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
7
+ export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
8
+ export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
9
+ export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./adapter/abstractions.js";
2
+ export * from "./upgradeHandler/abstractions.js";
3
+ export * from "./connectionManager/abstractions.js";
4
+ export { attachWebsocketsServer, createWebsocketsServer } from "./server/WebsocketsServer.js";
5
+ export { ServerConnectionManager } from "./connectionManager/ServerConnectionManager.js";
6
+ export { NodeWsAdapter } from "./adapter/NodeWsAdapter.js";
7
+ export { ServerWebsocketsTransport } from "./transport/ServerWebsocketsTransport.js";
8
+ export { WebsocketsStandaloneFeature } from "./WebsocketsStandaloneFeature.js";
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@webiny/api-websockets-standalone",
3
+ "version": "6.6.0-alpha.4",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/webiny/webiny-js.git"
8
+ },
9
+ "description": "Standalone WebSocket server transport for @webiny/api-websockets",
10
+ "contributors": [
11
+ "Bruno Zorić <bruno@webiny.com>"
12
+ ],
13
+ "exports": {
14
+ "./*": "./*",
15
+ ".": "./index.js"
16
+ },
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "@webiny/api": "6.6.0-alpha.4",
20
+ "@webiny/api-websockets": "6.6.0-alpha.4",
21
+ "@webiny/feature": "6.6.0-alpha.4",
22
+ "@webiny/plugins": "6.6.0-alpha.4",
23
+ "@webiny/utils": "6.6.0-alpha.4",
24
+ "ws": "8.21.3",
25
+ "zod": "4.4.3"
26
+ },
27
+ "devDependencies": {
28
+ "@types/ws": "8.18.1",
29
+ "@webiny/build-tools": "6.6.0-alpha.4",
30
+ "rimraf": "6.1.3",
31
+ "typescript": "7.0.2",
32
+ "vitest": "4.1.11"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "webiny": {
38
+ "publishFrom": "dist"
39
+ }
40
+ }
@@ -0,0 +1,53 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { WebsocketsServerAdapter } from "../adapter/abstractions.js";
3
+ import type { WebsocketsUpgradeHandler } from "../upgradeHandler/abstractions.js";
4
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
5
+ import type { CreateWebsocketsServerParams, AttachWebsocketsServerParams, IWebsocketsServer, WebsocketsConnectionAuthenticator } from "./types.js";
6
+ interface WebsocketsServerParams {
7
+ httpServer: HttpServer;
8
+ ownsHttpServer: boolean;
9
+ adapter: WebsocketsServerAdapter.Interface<unknown>;
10
+ upgradeHandler: WebsocketsUpgradeHandler.Interface;
11
+ connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
12
+ authenticate?: WebsocketsConnectionAuthenticator;
13
+ heartbeatInterval: number;
14
+ debug: boolean;
15
+ port: number;
16
+ host: string;
17
+ }
18
+ declare class WebsocketsServer implements IWebsocketsServer {
19
+ private readonly httpServer;
20
+ private readonly ownsHttpServer;
21
+ private readonly adapter;
22
+ private readonly upgradeHandler;
23
+ private readonly connectionManager;
24
+ private readonly authenticate;
25
+ private readonly heartbeat;
26
+ private readonly debug;
27
+ private readonly requestedPort;
28
+ private readonly requestedHost;
29
+ private shuttingDown;
30
+ private endpoint;
31
+ private resolvedPort;
32
+ constructor(params: WebsocketsServerParams);
33
+ start(): Promise<void>;
34
+ stop(): Promise<void>;
35
+ port(): number;
36
+ private wireUpgradeHandler;
37
+ private wireConnectionHandler;
38
+ /**
39
+ * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,
40
+ * then registers the live socket in the shared connection manager under the resolved identity and
41
+ * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds
42
+ * connections by identity id in the registry, so a connection that's anonymous (or never made it
43
+ * into the registry) can't be reached. Everything here is guarded — if something goes wrong we
44
+ * don't crash the server or the upgrade, the connection just ends up unaddressable.
45
+ */
46
+ private registerConnection;
47
+ private resolvePortFromExistingServer;
48
+ private listen;
49
+ private closeHttpServer;
50
+ }
51
+ export { WebsocketsServer };
52
+ export declare const createWebsocketsServer: (params: CreateWebsocketsServerParams) => IWebsocketsServer;
53
+ export declare const attachWebsocketsServer: (params: AttachWebsocketsServerParams) => IWebsocketsServer;
@@ -0,0 +1,188 @@
1
+ import { createServer } from "node:http";
2
+ import { mdbid } from "@webiny/utils";
3
+ import { NodeWsAdapterImpl } from "../adapter/NodeWsAdapter.js";
4
+ import { DefaultUpgradeHandlerImpl } from "../upgradeHandler/DefaultUpgradeHandler.js";
5
+ import { HeartbeatManager } from "../heartbeat/HeartbeatManager.js";
6
+ const ANONYMOUS_IDENTITY = {
7
+ id: "",
8
+ displayName: "",
9
+ type: ""
10
+ };
11
+ const toHeaders = (raw)=>{
12
+ const headers = {};
13
+ for (const [key, value] of Object.entries(raw))if ("string" == typeof value) headers[key] = value;
14
+ return headers;
15
+ };
16
+ class WebsocketsServer {
17
+ constructor(params){
18
+ this.shuttingDown = false;
19
+ this.endpoint = "";
20
+ this.resolvedPort = 0;
21
+ this.httpServer = params.httpServer;
22
+ this.ownsHttpServer = params.ownsHttpServer;
23
+ this.adapter = params.adapter;
24
+ this.upgradeHandler = params.upgradeHandler;
25
+ this.connectionManager = params.connectionManager;
26
+ this.authenticate = params.authenticate;
27
+ this.debug = params.debug;
28
+ this.requestedPort = params.port;
29
+ this.requestedHost = params.host;
30
+ if (params.connectionManager) this.heartbeat = new HeartbeatManager(params.connectionManager, params.heartbeatInterval);
31
+ }
32
+ async start() {
33
+ this.adapter.start(this.httpServer);
34
+ this.wireUpgradeHandler();
35
+ this.wireConnectionHandler();
36
+ if (this.ownsHttpServer) await this.listen();
37
+ else this.resolvePortFromExistingServer();
38
+ this.heartbeat?.start();
39
+ if (this.debug) console.log(`[WebsocketsServer] started on ${this.endpoint}`);
40
+ }
41
+ async stop() {
42
+ this.shuttingDown = true;
43
+ this.heartbeat?.stop();
44
+ await this.adapter.stop();
45
+ if (this.connectionManager) {
46
+ const activeIds = this.connectionManager.getActiveConnectionIds();
47
+ for (const connectionId of activeIds)await this.connectionManager.remove(connectionId);
48
+ }
49
+ if (this.ownsHttpServer) await this.closeHttpServer();
50
+ if (this.debug) console.log("[WebsocketsServer] stopped");
51
+ }
52
+ port() {
53
+ return this.resolvedPort;
54
+ }
55
+ wireUpgradeHandler() {
56
+ this.httpServer.on("upgrade", async (request, socket, head)=>{
57
+ if (this.shuttingDown) return void socket.destroy();
58
+ const decision = await this.upgradeHandler.shouldUpgrade(request);
59
+ if (!decision.allowed) {
60
+ socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\r\n\r\n`);
61
+ socket.destroy();
62
+ return;
63
+ }
64
+ this.adapter.handleUpgrade(request, socket, head);
65
+ });
66
+ }
67
+ wireConnectionHandler() {
68
+ this.adapter.onConnection((socket, request)=>{
69
+ if (this.shuttingDown) return;
70
+ const connectionId = mdbid();
71
+ const connectedAt = Date.now();
72
+ const host = request.headers.host || "localhost";
73
+ const headers = toHeaders(request.headers);
74
+ this.registerConnection({
75
+ connectionId,
76
+ socket,
77
+ request,
78
+ connectedAt,
79
+ host,
80
+ headers
81
+ });
82
+ this.adapter.onMessage(socket, (data)=>{
83
+ if (this.shuttingDown) return;
84
+ const metadata = this.connectionManager?.getMetadata(connectionId);
85
+ if (!metadata) return;
86
+ try {
87
+ JSON.parse(data.toString());
88
+ } catch {
89
+ return;
90
+ }
91
+ this.connectionManager?.updateLastSeen(connectionId);
92
+ });
93
+ this.adapter.onClose(socket, ()=>{
94
+ if (this.shuttingDown) return;
95
+ this.connectionManager?.remove(connectionId);
96
+ });
97
+ this.adapter.onError(socket, (error)=>{
98
+ console.error(`WebSocket error for connection "${connectionId}":`, error.message);
99
+ });
100
+ });
101
+ }
102
+ async registerConnection(params) {
103
+ const { connectionId, socket, request, connectedAt, host, headers } = params;
104
+ let identity = ANONYMOUS_IDENTITY;
105
+ let tenant = "";
106
+ try {
107
+ const url = new URL(request.url ?? "", `http://${host}`);
108
+ tenant = url.searchParams.get("tenant") ?? "";
109
+ const token = url.searchParams.get("token") ?? "";
110
+ if (this.authenticate && token) {
111
+ const authenticated = await this.authenticate(token);
112
+ if (authenticated?.id) identity = authenticated;
113
+ }
114
+ await this.connectionManager?.add({
115
+ connectionId,
116
+ socket,
117
+ endpoint: this.endpoint,
118
+ identity,
119
+ tenant,
120
+ connectedAt,
121
+ host,
122
+ headers
123
+ });
124
+ } catch (error) {
125
+ console.error(`Failed to register WebSocket connection "${connectionId}":`, error instanceof Error ? error.message : String(error));
126
+ }
127
+ }
128
+ resolvePortFromExistingServer() {
129
+ const address = this.httpServer.address();
130
+ if (address && "string" != typeof address) {
131
+ this.resolvedPort = address.port;
132
+ this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;
133
+ }
134
+ }
135
+ listen() {
136
+ return new Promise((resolve, reject)=>{
137
+ this.httpServer.listen(this.requestedPort, this.requestedHost, ()=>{
138
+ const address = this.httpServer.address();
139
+ if (!address || "string" == typeof address) return void reject(new Error("Unexpected server address format."));
140
+ this.resolvedPort = address.port;
141
+ this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;
142
+ resolve();
143
+ });
144
+ });
145
+ }
146
+ closeHttpServer() {
147
+ return new Promise((resolve, reject)=>{
148
+ this.httpServer.close((err)=>{
149
+ if (err) return void reject(err);
150
+ resolve();
151
+ });
152
+ });
153
+ }
154
+ }
155
+ const createWebsocketsServer = (params)=>{
156
+ const httpServer = createServer();
157
+ const server = new WebsocketsServer({
158
+ httpServer,
159
+ ownsHttpServer: true,
160
+ adapter: new NodeWsAdapterImpl(),
161
+ upgradeHandler: new DefaultUpgradeHandlerImpl(),
162
+ connectionManager: params.connectionManager,
163
+ authenticate: params.authenticate,
164
+ heartbeatInterval: params.heartbeatInterval ?? 60000,
165
+ debug: params.debug ?? false,
166
+ port: params.port ?? 0,
167
+ host: params.host ?? "127.0.0.1"
168
+ });
169
+ return server;
170
+ };
171
+ const attachWebsocketsServer = (params)=>{
172
+ const server = new WebsocketsServer({
173
+ httpServer: params.server,
174
+ ownsHttpServer: false,
175
+ adapter: new NodeWsAdapterImpl(),
176
+ upgradeHandler: new DefaultUpgradeHandlerImpl(),
177
+ connectionManager: params.connectionManager,
178
+ authenticate: params.authenticate,
179
+ heartbeatInterval: params.heartbeatInterval ?? 60000,
180
+ debug: params.debug ?? false,
181
+ port: 0,
182
+ host: "127.0.0.1"
183
+ });
184
+ return server;
185
+ };
186
+ export { WebsocketsServer, attachWebsocketsServer, createWebsocketsServer };
187
+
188
+ //# sourceMappingURL=WebsocketsServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server/WebsocketsServer.js","sources":["../../src/server/WebsocketsServer.ts"],"sourcesContent":["import { createServer } from \"node:http\";\nimport type { Server as HttpServer, IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport { mdbid } from \"@webiny/utils\";\nimport type { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\nimport type { WebsocketsUpgradeHandler } from \"~/upgradeHandler/abstractions.js\";\nimport type { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport type { ConnectionRegistry } from \"@webiny/api-websockets/exports/api.js\";\nimport { NodeWsAdapterImpl } from \"~/adapter/NodeWsAdapter.js\";\nimport { DefaultUpgradeHandlerImpl } from \"~/upgradeHandler/DefaultUpgradeHandler.js\";\nimport { HeartbeatManager } from \"~/heartbeat/HeartbeatManager.js\";\nimport type {\n CreateWebsocketsServerParams,\n AttachWebsocketsServerParams,\n IWebsocketsServer,\n WebsocketsConnectionAuthenticator\n} from \"./types.js\";\n\nconst ANONYMOUS_IDENTITY: ConnectionRegistry.Identity = { id: \"\", displayName: \"\", type: \"\" };\n\nconst toHeaders = (raw: IncomingMessage[\"headers\"]): Record<string, string> => {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (typeof value === \"string\") {\n headers[key] = value;\n }\n }\n return headers;\n};\n\ninterface WebsocketsServerParams {\n httpServer: HttpServer;\n ownsHttpServer: boolean;\n adapter: WebsocketsServerAdapter.Interface<unknown>;\n upgradeHandler: WebsocketsUpgradeHandler.Interface;\n connectionManager?: WebsocketsConnectionManager.Interface<unknown>;\n authenticate?: WebsocketsConnectionAuthenticator;\n heartbeatInterval: number;\n debug: boolean;\n port: number;\n host: string;\n}\n\nclass WebsocketsServer implements IWebsocketsServer {\n private readonly httpServer: HttpServer;\n private readonly ownsHttpServer: boolean;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n private readonly upgradeHandler: WebsocketsUpgradeHandler.Interface;\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown> | undefined;\n private readonly authenticate: WebsocketsConnectionAuthenticator | undefined;\n private readonly heartbeat: HeartbeatManager | undefined;\n private readonly debug: boolean;\n private readonly requestedPort: number;\n private readonly requestedHost: string;\n\n private shuttingDown: boolean = false;\n private endpoint: string = \"\";\n private resolvedPort: number = 0;\n\n public constructor(params: WebsocketsServerParams) {\n this.httpServer = params.httpServer;\n this.ownsHttpServer = params.ownsHttpServer;\n this.adapter = params.adapter;\n this.upgradeHandler = params.upgradeHandler;\n this.connectionManager = params.connectionManager;\n this.authenticate = params.authenticate;\n this.debug = params.debug;\n this.requestedPort = params.port;\n this.requestedHost = params.host;\n\n if (params.connectionManager) {\n this.heartbeat = new HeartbeatManager(\n params.connectionManager,\n params.heartbeatInterval\n );\n }\n }\n\n public async start(): Promise<void> {\n this.adapter.start(this.httpServer);\n this.wireUpgradeHandler();\n this.wireConnectionHandler();\n\n if (this.ownsHttpServer) {\n await this.listen();\n } else {\n this.resolvePortFromExistingServer();\n }\n\n this.heartbeat?.start();\n\n if (this.debug) {\n console.log(`[WebsocketsServer] started on ${this.endpoint}`);\n }\n }\n\n public async stop(): Promise<void> {\n this.shuttingDown = true;\n this.heartbeat?.stop();\n\n await this.adapter.stop();\n\n if (this.connectionManager) {\n const activeIds = this.connectionManager.getActiveConnectionIds();\n for (const connectionId of activeIds) {\n await this.connectionManager.remove(connectionId);\n }\n }\n\n if (this.ownsHttpServer) {\n await this.closeHttpServer();\n }\n\n if (this.debug) {\n console.log(\"[WebsocketsServer] stopped\");\n }\n }\n\n public port(): number {\n return this.resolvedPort;\n }\n\n private wireUpgradeHandler(): void {\n this.httpServer.on(\n \"upgrade\",\n async (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n if (this.shuttingDown) {\n socket.destroy();\n return;\n }\n\n const decision = await this.upgradeHandler.shouldUpgrade(request);\n if (!decision.allowed) {\n socket.write(`HTTP/1.1 ${decision.statusCode} ${decision.reason}\\r\\n\\r\\n`);\n socket.destroy();\n return;\n }\n\n this.adapter.handleUpgrade(request, socket, head);\n }\n );\n }\n\n private wireConnectionHandler(): void {\n this.adapter.onConnection((socket, request) => {\n if (this.shuttingDown) {\n return;\n }\n\n const connectionId = mdbid();\n const connectedAt = Date.now();\n const host = request.headers.host || \"localhost\";\n const headers = toHeaders(request.headers);\n\n // Register the connection under its real identity, decoded from the `?token` JWT. Both\n // the authentication and the registry write are async, so we kick this off without\n // awaiting it and wire the socket handlers below synchronously — that way we don't miss a\n // close or message that arrives during the gap. If a message does arrive before\n // registration finishes, the `getMetadata` guard below just drops it, which is fine since\n // the client doesn't send anything meaningful right at connect time.\n void this.registerConnection({\n connectionId,\n socket,\n request,\n connectedAt,\n host,\n headers\n });\n\n this.adapter.onMessage(socket, data => {\n if (this.shuttingDown) {\n return;\n }\n\n const metadata = this.connectionManager?.getMetadata(connectionId);\n if (!metadata) {\n return;\n }\n\n try {\n JSON.parse(data.toString());\n } catch {\n return;\n }\n\n this.connectionManager?.updateLastSeen(connectionId);\n });\n\n this.adapter.onClose(socket, () => {\n if (this.shuttingDown) {\n return;\n }\n this.connectionManager?.remove(connectionId);\n });\n\n this.adapter.onError(socket, error => {\n console.error(`WebSocket error for connection \"${connectionId}\":`, error.message);\n });\n });\n }\n\n /**\n * Authenticates the upgrade using the `?token`/`?tenant` query the app-websockets client sends,\n * then registers the live socket in the shared connection manager under the resolved identity and\n * tenant. Getting both right matters for targeted server→client sends: `SendToIdentity` finds\n * connections by identity id in the registry, so a connection that's anonymous (or never made it\n * into the registry) can't be reached. Everything here is guarded — if something goes wrong we\n * don't crash the server or the upgrade, the connection just ends up unaddressable.\n */\n private async registerConnection(params: {\n connectionId: string;\n socket: unknown;\n request: IncomingMessage;\n connectedAt: number;\n host: string;\n headers: Record<string, string>;\n }): Promise<void> {\n const { connectionId, socket, request, connectedAt, host, headers } = params;\n\n let identity: ConnectionRegistry.Identity = ANONYMOUS_IDENTITY;\n let tenant = \"\";\n\n try {\n const url = new URL(request.url ?? \"\", `http://${host}`);\n tenant = url.searchParams.get(\"tenant\") ?? \"\";\n const token = url.searchParams.get(\"token\") ?? \"\";\n\n if (this.authenticate && token) {\n const authenticated = await this.authenticate(token);\n if (authenticated?.id) {\n identity = authenticated;\n }\n }\n\n await this.connectionManager?.add({\n connectionId,\n socket,\n endpoint: this.endpoint,\n identity,\n tenant,\n connectedAt,\n host,\n headers\n });\n } catch (error) {\n console.error(\n `Failed to register WebSocket connection \"${connectionId}\":`,\n error instanceof Error ? error.message : String(error)\n );\n }\n }\n\n private resolvePortFromExistingServer(): void {\n const address = this.httpServer.address();\n if (address && typeof address !== \"string\") {\n this.resolvedPort = address.port;\n this.endpoint = `ws://127.0.0.1:${this.resolvedPort}`;\n }\n }\n\n private listen(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.listen(this.requestedPort, this.requestedHost, () => {\n const address = this.httpServer.address();\n if (!address || typeof address === \"string\") {\n reject(new Error(\"Unexpected server address format.\"));\n return;\n }\n this.resolvedPort = address.port;\n this.endpoint = `ws://${this.requestedHost}:${this.resolvedPort}`;\n resolve();\n });\n });\n }\n\n private closeHttpServer(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.close(err => {\n if (err) {\n reject(err);\n return;\n }\n resolve();\n });\n });\n }\n}\n\nexport { WebsocketsServer };\n\nexport const createWebsocketsServer = (params: CreateWebsocketsServerParams): IWebsocketsServer => {\n const httpServer = createServer();\n const server = new WebsocketsServer({\n httpServer,\n ownsHttpServer: true,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: params.port ?? 0,\n host: params.host ?? \"127.0.0.1\"\n });\n return server;\n};\n\nexport const attachWebsocketsServer = (params: AttachWebsocketsServerParams): IWebsocketsServer => {\n const server = new WebsocketsServer({\n httpServer: params.server,\n ownsHttpServer: false,\n adapter: new NodeWsAdapterImpl(),\n upgradeHandler: new DefaultUpgradeHandlerImpl(),\n connectionManager: params.connectionManager,\n authenticate: params.authenticate,\n heartbeatInterval: params.heartbeatInterval ?? 60_000,\n debug: params.debug ?? false,\n port: 0,\n host: \"127.0.0.1\"\n });\n return server;\n};\n"],"names":["ANONYMOUS_IDENTITY","toHeaders","raw","headers","key","value","Object","WebsocketsServer","params","HeartbeatManager","console","activeIds","connectionId","request","socket","head","decision","mdbid","connectedAt","Date","host","data","metadata","JSON","error","identity","tenant","url","URL","token","authenticated","Error","String","address","Promise","resolve","reject","err","createWebsocketsServer","httpServer","createServer","server","NodeWsAdapterImpl","DefaultUpgradeHandlerImpl","attachWebsocketsServer"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { Server as HttpServer } from "node:http";
2
+ import type { PluginsContainer } from "@webiny/plugins";
3
+ import type { PluginCollection } from "@webiny/plugins/types.js";
4
+ import type { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
5
+ import type { ConnectionRegistry } from "@webiny/api-websockets/exports/api.js";
6
+ /**
7
+ * Resolves a connection's identity from the `?token` JWT sent on the WebSocket upgrade — the SAME
8
+ * token→identity step the HTTP request stack runs (RequestIdentityLoader). Returns `null` for an
9
+ * absent/invalid/anonymous token, in which case the connection registers anonymously and won't
10
+ * receive targeted (`SendToIdentity`) messages.
11
+ *
12
+ * It's a plain callback rather than a resolved dependency for two reasons: this WebSocket server is a
13
+ * DI-agnostic class (the handler resolves what it needs and passes plain values in), and the auth it
14
+ * needs — `AuthenticationContext` — lives in the PER-REQUEST stack, not the root container, so a
15
+ * boot-time object like this couldn't resolve it directly anyway. The handler builds the callback
16
+ * (spinning up a request-scoped child container per token). So authenticate has to be a callback the
17
+ * handler supplies, regardless of DI.
18
+ */
19
+ export type WebsocketsConnectionAuthenticator = (token: string) => Promise<ConnectionRegistry.Identity | null>;
20
+ interface BaseServerParams {
21
+ plugins?: PluginsContainer | PluginCollection;
22
+ heartbeatInterval?: number;
23
+ debug?: boolean;
24
+ connectionManager?: WebsocketsConnectionManager.Interface<unknown>;
25
+ authenticate?: WebsocketsConnectionAuthenticator;
26
+ }
27
+ export interface CreateWebsocketsServerParams extends BaseServerParams {
28
+ port?: number;
29
+ host?: string;
30
+ }
31
+ export interface AttachWebsocketsServerParams extends BaseServerParams {
32
+ server: HttpServer;
33
+ }
34
+ export interface IWebsocketsServer {
35
+ start(): Promise<void>;
36
+ stop(): Promise<void>;
37
+ port(): number;
38
+ }
39
+ export {};
File without changes
@@ -0,0 +1,14 @@
1
+ import { WebsocketsTransport } from "@webiny/api-websockets/exports/api.js";
2
+ import type { GenericRecord } from "@webiny/api/types.js";
3
+ import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
4
+ import { WebsocketsServerAdapter } from "../adapter/abstractions.js";
5
+ export declare class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {
6
+ private readonly connectionManager;
7
+ private readonly adapter;
8
+ constructor(connectionManager: WebsocketsConnectionManager.Interface<unknown>, adapter: WebsocketsServerAdapter.Interface<unknown>);
9
+ send<T extends GenericRecord = GenericRecord>(connections: WebsocketsTransport.SendConnection[], data: WebsocketsTransport.SendData<T>): Promise<void>;
10
+ disconnect(connections: WebsocketsTransport.DisconnectConnection[]): Promise<void>;
11
+ }
12
+ export declare const ServerWebsocketsTransport: typeof ServerWebsocketsTransportImpl & {
13
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/api-websockets/transport").IWebsocketsTransport>;
14
+ };
@@ -0,0 +1,52 @@
1
+ import { WebsocketsTransport } from "@webiny/api-websockets/exports/api.js";
2
+ import { WebsocketsConnectionManager } from "../connectionManager/abstractions.js";
3
+ import { WebsocketsServerAdapter } from "../adapter/abstractions.js";
4
+ class ServerWebsocketsTransportImpl {
5
+ constructor(connectionManager, adapter){
6
+ this.connectionManager = connectionManager;
7
+ this.adapter = adapter;
8
+ }
9
+ async send(connections, data) {
10
+ for (const connection of connections){
11
+ const { connectionId } = connection;
12
+ const socket = this.connectionManager.getSocket(connectionId);
13
+ if (void 0 === socket) {
14
+ await this.connectionManager.remove(connectionId);
15
+ continue;
16
+ }
17
+ try {
18
+ await this.adapter.send(socket, JSON.stringify(data));
19
+ } catch (ex) {
20
+ console.error(`Failed to send message to connection "${connectionId}". Check logs for more information.`);
21
+ console.log(ex);
22
+ }
23
+ }
24
+ }
25
+ async disconnect(connections) {
26
+ for (const connection of connections){
27
+ const { connectionId } = connection;
28
+ const socket = this.connectionManager.getSocket(connectionId);
29
+ if (void 0 === socket) {
30
+ await this.connectionManager.remove(connectionId);
31
+ continue;
32
+ }
33
+ try {
34
+ this.adapter.close(socket);
35
+ await this.connectionManager.remove(connectionId);
36
+ } catch (ex) {
37
+ console.error(`Failed to disconnect connection "${connectionId}". Check logs for more information.`);
38
+ console.log(ex);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({
44
+ implementation: ServerWebsocketsTransportImpl,
45
+ dependencies: [
46
+ WebsocketsConnectionManager,
47
+ WebsocketsServerAdapter
48
+ ]
49
+ });
50
+ export { ServerWebsocketsTransport, ServerWebsocketsTransportImpl };
51
+
52
+ //# sourceMappingURL=ServerWebsocketsTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport/ServerWebsocketsTransport.js","sources":["../../src/transport/ServerWebsocketsTransport.ts"],"sourcesContent":["import { WebsocketsTransport } from \"@webiny/api-websockets/exports/api.js\";\nimport type { GenericRecord } from \"@webiny/api/types.js\";\nimport { WebsocketsConnectionManager } from \"~/connectionManager/abstractions.js\";\nimport { WebsocketsServerAdapter } from \"~/adapter/abstractions.js\";\n\nexport class ServerWebsocketsTransportImpl implements WebsocketsTransport.Interface {\n private readonly connectionManager: WebsocketsConnectionManager.Interface<unknown>;\n private readonly adapter: WebsocketsServerAdapter.Interface<unknown>;\n\n public constructor(\n connectionManager: WebsocketsConnectionManager.Interface<unknown>,\n adapter: WebsocketsServerAdapter.Interface<unknown>\n ) {\n this.connectionManager = connectionManager;\n this.adapter = adapter;\n }\n\n public async send<T extends GenericRecord = GenericRecord>(\n connections: WebsocketsTransport.SendConnection[],\n data: WebsocketsTransport.SendData<T>\n ): Promise<void> {\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n await this.connectionManager.remove(connectionId);\n continue;\n }\n try {\n await this.adapter.send(socket, JSON.stringify(data));\n } catch (ex) {\n console.error(\n `Failed to send message to connection \"${connectionId}\". Check logs for more information.`\n );\n console.log(ex);\n }\n }\n }\n\n public async disconnect(\n connections: WebsocketsTransport.DisconnectConnection[]\n ): Promise<void> {\n for (const connection of connections) {\n const { connectionId } = connection;\n const socket = this.connectionManager.getSocket(connectionId);\n if (socket === undefined) {\n await this.connectionManager.remove(connectionId);\n continue;\n }\n try {\n this.adapter.close(socket);\n await this.connectionManager.remove(connectionId);\n } catch (ex) {\n console.error(\n `Failed to disconnect connection \"${connectionId}\". Check logs for more information.`\n );\n console.log(ex);\n }\n }\n }\n}\n\nexport const ServerWebsocketsTransport = WebsocketsTransport.createImplementation({\n implementation: ServerWebsocketsTransportImpl,\n dependencies: [WebsocketsConnectionManager, WebsocketsServerAdapter]\n});\n"],"names":["ServerWebsocketsTransportImpl","connectionManager","adapter","connections","data","connection","connectionId","socket","undefined","JSON","ex","console","ServerWebsocketsTransport","WebsocketsTransport","WebsocketsConnectionManager","WebsocketsServerAdapter"],"mappings":";;;AAKO,MAAMA;IAIT,YACIC,iBAAiE,EACjEC,OAAmD,CACrD;QACE,IAAI,CAAC,iBAAiB,GAAGD;QACzB,IAAI,CAAC,OAAO,GAAGC;IACnB;IAEA,MAAa,KACTC,WAAiD,EACjDC,IAAqC,EACxB;QACb,KAAK,MAAMC,cAAcF,YAAa;YAClC,MAAM,EAAEG,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;gBACpC;YACJ;YACA,IAAI;gBACA,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAACC,QAAQE,KAAK,SAAS,CAACL;YACnD,EAAE,OAAOM,IAAI;gBACTC,QAAQ,KAAK,CACT,CAAC,sCAAsC,EAAEL,aAAa,mCAAmC,CAAC;gBAE9FK,QAAQ,GAAG,CAACD;YAChB;QACJ;IACJ;IAEA,MAAa,WACTP,WAAuD,EAC1C;QACb,KAAK,MAAME,cAAcF,YAAa;YAClC,MAAM,EAAEG,YAAY,EAAE,GAAGD;YACzB,MAAME,SAAS,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAACD;YAChD,IAAIC,AAAWC,WAAXD,QAAsB;gBACtB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;gBACpC;YACJ;YACA,IAAI;gBACA,IAAI,CAAC,OAAO,CAAC,KAAK,CAACC;gBACnB,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAACD;YACxC,EAAE,OAAOI,IAAI;gBACTC,QAAQ,KAAK,CACT,CAAC,iCAAiC,EAAEL,aAAa,mCAAmC,CAAC;gBAEzFK,QAAQ,GAAG,CAACD;YAChB;QACJ;IACJ;AACJ;AAEO,MAAME,4BAA4BC,oBAAoB,oBAAoB,CAAC;IAC9E,gBAAgBb;IAChB,cAAc;QAACc;QAA6BC;KAAwB;AACxE"}
@@ -0,0 +1,8 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { WebsocketsUpgradeHandler } from "./abstractions.js";
3
+ export declare class DefaultUpgradeHandlerImpl implements WebsocketsUpgradeHandler.Interface {
4
+ shouldUpgrade(_request: IncomingMessage): Promise<WebsocketsUpgradeHandler.Decision>;
5
+ }
6
+ export declare const DefaultUpgradeHandler: typeof DefaultUpgradeHandlerImpl & {
7
+ __abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").IWebsocketsUpgradeHandler>;
8
+ };
@@ -0,0 +1,15 @@
1
+ import { WebsocketsUpgradeHandler } from "./abstractions.js";
2
+ class DefaultUpgradeHandlerImpl {
3
+ async shouldUpgrade(_request) {
4
+ return {
5
+ allowed: true
6
+ };
7
+ }
8
+ }
9
+ const DefaultUpgradeHandler = WebsocketsUpgradeHandler.createImplementation({
10
+ implementation: DefaultUpgradeHandlerImpl,
11
+ dependencies: []
12
+ });
13
+ export { DefaultUpgradeHandler, DefaultUpgradeHandlerImpl };
14
+
15
+ //# sourceMappingURL=DefaultUpgradeHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgradeHandler/DefaultUpgradeHandler.js","sources":["../../src/upgradeHandler/DefaultUpgradeHandler.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { WebsocketsUpgradeHandler } from \"./abstractions.js\";\n\nexport class DefaultUpgradeHandlerImpl implements WebsocketsUpgradeHandler.Interface {\n public async shouldUpgrade(\n _request: IncomingMessage\n ): Promise<WebsocketsUpgradeHandler.Decision> {\n return { allowed: true };\n }\n}\n\nexport const DefaultUpgradeHandler = WebsocketsUpgradeHandler.createImplementation({\n implementation: DefaultUpgradeHandlerImpl,\n dependencies: []\n});\n"],"names":["DefaultUpgradeHandlerImpl","_request","DefaultUpgradeHandler","WebsocketsUpgradeHandler"],"mappings":";AAGO,MAAMA;IACT,MAAa,cACTC,QAAyB,EACiB;QAC1C,OAAO;YAAE,SAAS;QAAK;IAC3B;AACJ;AAEO,MAAMC,wBAAwBC,yBAAyB,oBAAoB,CAAC;IAC/E,gBAAgBH;IAChB,cAAc,EAAE;AACpB"}
@@ -0,0 +1,17 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ type UpgradeDecision = {
3
+ allowed: true;
4
+ } | {
5
+ allowed: false;
6
+ statusCode: number;
7
+ reason: string;
8
+ };
9
+ export interface IWebsocketsUpgradeHandler {
10
+ shouldUpgrade(request: IncomingMessage): Promise<UpgradeDecision>;
11
+ }
12
+ export declare const WebsocketsUpgradeHandler: import("@webiny/di").Abstraction<IWebsocketsUpgradeHandler>;
13
+ export declare namespace WebsocketsUpgradeHandler {
14
+ type Interface = IWebsocketsUpgradeHandler;
15
+ type Decision = UpgradeDecision;
16
+ }
17
+ export {};
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const WebsocketsUpgradeHandler = createAbstraction("WebsocketsUpgradeHandler");
3
+ export { WebsocketsUpgradeHandler };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgradeHandler/abstractions.js","sources":["../../src/upgradeHandler/abstractions.ts"],"sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport { createAbstraction } from \"@webiny/feature/api\";\n\ntype UpgradeDecision = { allowed: true } | { allowed: false; statusCode: number; reason: string };\n\nexport interface IWebsocketsUpgradeHandler {\n shouldUpgrade(request: IncomingMessage): Promise<UpgradeDecision>;\n}\n\nexport const WebsocketsUpgradeHandler = createAbstraction<IWebsocketsUpgradeHandler>(\n \"WebsocketsUpgradeHandler\"\n);\n\nexport namespace WebsocketsUpgradeHandler {\n export type Interface = IWebsocketsUpgradeHandler;\n export type Decision = UpgradeDecision;\n}\n"],"names":["WebsocketsUpgradeHandler","createAbstraction"],"mappings":";AASO,MAAMA,2BAA2BC,kBACpC"}
@@ -0,0 +1,4 @@
1
+ import { WebsocketsEventValidator, WebsocketsRunner } from "@webiny/api-websockets/exports/api.js";
2
+ export declare class ServerWebsocketsEventValidator implements WebsocketsEventValidator.Interface {
3
+ validate<T extends WebsocketsRunner.EventData = WebsocketsRunner.EventData>(input: unknown): Promise<WebsocketsRunner.Event<T>>;
4
+ }
@@ -0,0 +1,53 @@
1
+ import zod from "zod";
2
+ import { createZodError } from "@webiny/utils";
3
+ const eventTypeEnum = zod["enum"]([
4
+ "message",
5
+ "connect",
6
+ "disconnect"
7
+ ]);
8
+ const contextValidation = zod.object({
9
+ connectionId: zod.string(),
10
+ connectedAt: zod.number(),
11
+ host: zod.string(),
12
+ eventType: eventTypeEnum,
13
+ route: zod.string(),
14
+ endpoint: zod.string()
15
+ });
16
+ const bodyValidation = zod.looseObject({
17
+ token: zod.string().optional(),
18
+ tenant: zod.string().optional(),
19
+ messageId: zod.string().nullish(),
20
+ action: zod.string().optional(),
21
+ data: zod.looseObject({}).nullish()
22
+ }).optional();
23
+ const validation = zod.object({
24
+ headers: zod.looseObject({}).optional(),
25
+ context: contextValidation,
26
+ body: bodyValidation
27
+ }).superRefine((output, ctx)=>{
28
+ if ("message" !== output.context.eventType) return;
29
+ if (!output.body) ctx.addIssue({
30
+ path: [
31
+ "body"
32
+ ],
33
+ message: "Message event must have a body.",
34
+ code: zod.ZodIssueCode.custom,
35
+ fatal: true
36
+ });
37
+ });
38
+ class ServerWebsocketsEventValidator {
39
+ async validate(input) {
40
+ const result = await validation.safeParseAsync(input);
41
+ if (!result.success) throw createZodError(result.error);
42
+ const event = {
43
+ headers: result.data.headers,
44
+ context: result.data.context,
45
+ body: result.data.body
46
+ };
47
+ if ("message" === event.context.eventType && event.body?.action) event.context.route = event.body.action;
48
+ return event;
49
+ }
50
+ }
51
+ export { ServerWebsocketsEventValidator };
52
+
53
+ //# sourceMappingURL=ServerWebsocketsEventValidator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator/ServerWebsocketsEventValidator.js","sources":["../../src/validator/ServerWebsocketsEventValidator.ts"],"sourcesContent":["import zod from \"zod\";\nimport { WebsocketsEventValidator, WebsocketsRunner } from \"@webiny/api-websockets/exports/api.js\";\nimport { createZodError } from \"@webiny/utils\";\n\nconst eventTypeEnum = zod.enum([\"message\", \"connect\", \"disconnect\"]);\n\nconst contextValidation = zod.object({\n connectionId: zod.string(),\n connectedAt: zod.number(),\n host: zod.string(),\n eventType: eventTypeEnum,\n route: zod.string(),\n endpoint: zod.string()\n});\n\nconst bodyValidation = zod\n .looseObject({\n token: zod.string().optional(),\n tenant: zod.string().optional(),\n messageId: zod.string().nullish(),\n action: zod.string().optional(),\n data: zod.looseObject({}).nullish()\n })\n .optional();\n\nconst validation = zod\n .object({\n headers: zod.looseObject({}).optional(),\n context: contextValidation,\n body: bodyValidation\n })\n .superRefine((output, ctx) => {\n if (output.context.eventType !== \"message\") {\n return;\n }\n if (!output.body) {\n ctx.addIssue({\n path: [\"body\"],\n message: \"Message event must have a body.\",\n code: zod.ZodIssueCode.custom,\n fatal: true\n });\n }\n });\n\nexport class ServerWebsocketsEventValidator implements WebsocketsEventValidator.Interface {\n public async validate<T extends WebsocketsRunner.EventData = WebsocketsRunner.EventData>(\n input: unknown\n ): Promise<WebsocketsRunner.Event<T>> {\n const result = await validation.safeParseAsync(input);\n if (!result.success) {\n throw createZodError(result.error);\n }\n\n const event: WebsocketsRunner.Event<T> = {\n headers: result.data.headers as Record<string, string>,\n context: result.data.context,\n body: result.data.body as T\n };\n\n /* Use body.action as custom route if provided. */\n if (event.context.eventType === \"message\" && event.body?.action) {\n event.context.route = event.body.action;\n }\n\n return event;\n }\n}\n"],"names":["eventTypeEnum","zod","contextValidation","bodyValidation","validation","output","ctx","ServerWebsocketsEventValidator","input","result","createZodError","event"],"mappings":";;AAIA,MAAMA,gBAAgBC,GAAG,CAAHA,OAAQ,CAAC;IAAC;IAAW;IAAW;CAAa;AAEnE,MAAMC,oBAAoBD,IAAI,MAAM,CAAC;IACjC,cAAcA,IAAI,MAAM;IACxB,aAAaA,IAAI,MAAM;IACvB,MAAMA,IAAI,MAAM;IAChB,WAAWD;IACX,OAAOC,IAAI,MAAM;IACjB,UAAUA,IAAI,MAAM;AACxB;AAEA,MAAME,iBAAiBF,IAAAA,WACP,CAAC;IACT,OAAOA,IAAI,MAAM,GAAG,QAAQ;IAC5B,QAAQA,IAAI,MAAM,GAAG,QAAQ;IAC7B,WAAWA,IAAI,MAAM,GAAG,OAAO;IAC/B,QAAQA,IAAI,MAAM,GAAG,QAAQ;IAC7B,MAAMA,IAAI,WAAW,CAAC,CAAC,GAAG,OAAO;AACrC,GACC,QAAQ;AAEb,MAAMG,aAAaH,IAAAA,MACR,CAAC;IACJ,SAASA,IAAI,WAAW,CAAC,CAAC,GAAG,QAAQ;IACrC,SAASC;IACT,MAAMC;AACV,GACC,WAAW,CAAC,CAACE,QAAQC;IAClB,IAAID,AAA6B,cAA7BA,OAAO,OAAO,CAAC,SAAS,EACxB;IAEJ,IAAI,CAACA,OAAO,IAAI,EACZC,IAAI,QAAQ,CAAC;QACT,MAAM;YAAC;SAAO;QACd,SAAS;QACT,MAAML,IAAI,YAAY,CAAC,MAAM;QAC7B,OAAO;IACX;AAER;AAEG,MAAMM;IACT,MAAa,SACTC,KAAc,EACoB;QAClC,MAAMC,SAAS,MAAML,WAAW,cAAc,CAACI;QAC/C,IAAI,CAACC,OAAO,OAAO,EACf,MAAMC,eAAeD,OAAO,KAAK;QAGrC,MAAME,QAAmC;YACrC,SAASF,OAAO,IAAI,CAAC,OAAO;YAC5B,SAASA,OAAO,IAAI,CAAC,OAAO;YAC5B,MAAMA,OAAO,IAAI,CAAC,IAAI;QAC1B;QAGA,IAAIE,AAA4B,cAA5BA,MAAM,OAAO,CAAC,SAAS,IAAkBA,MAAM,IAAI,EAAE,QACrDA,MAAM,OAAO,CAAC,KAAK,GAAGA,MAAM,IAAI,CAAC,MAAM;QAG3C,OAAOA;IACX;AACJ"}