crossws 0.4.4 → 0.4.6
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/adapters/bunny.d.ts +2 -0
- package/adapters/vercel.d.ts +2 -0
- package/dist/THIRD-PARTY-LICENSES.md +33 -0
- package/dist/_chunks/_request.mjs +1 -4
- package/dist/_chunks/_types.d.mts +9 -10
- package/dist/_chunks/adapter.d.mts +7 -15
- package/dist/_chunks/adapter.mjs +6 -7
- package/dist/_chunks/bun.d.mts +2 -5
- package/dist/_chunks/bunny.d.mts +22 -0
- package/dist/_chunks/cloudflare.d.mts +3 -6
- package/dist/_chunks/deno.d.mts +2 -5
- package/dist/_chunks/error.mjs +1 -4
- package/dist/_chunks/libs/ws.mjs +96 -1171
- package/dist/_chunks/node.d.mts +36 -7
- package/dist/_chunks/node.mjs +129 -0
- package/dist/_chunks/peer.mjs +2 -60
- package/dist/_chunks/rolldown-runtime.mjs +8 -16
- package/dist/_chunks/sse.d.mts +2 -5
- package/dist/_chunks/web.d.mts +1 -3
- package/dist/adapters/bun.d.mts +1 -1
- package/dist/adapters/bun.mjs +3 -8
- package/dist/adapters/bunny.d.mts +2 -0
- package/dist/adapters/bunny.mjs +68 -0
- package/dist/adapters/cloudflare.d.mts +1 -1
- package/dist/adapters/cloudflare.mjs +11 -15
- package/dist/adapters/deno.d.mts +1 -1
- package/dist/adapters/deno.mjs +4 -9
- package/dist/adapters/node.d.mts +2 -2
- package/dist/adapters/node.mjs +2 -125
- package/dist/adapters/sse.d.mts +1 -1
- package/dist/adapters/sse.mjs +3 -8
- package/dist/adapters/uws.d.mts +2 -7
- package/dist/adapters/uws.mjs +6 -10
- package/dist/adapters/vercel.d.mts +25 -0
- package/dist/adapters/vercel.mjs +48 -0
- package/dist/index.d.mts +97 -2
- package/dist/index.mjs +178 -3
- package/dist/server/bun.d.mts +1 -7
- package/dist/server/bun.mjs +3 -7
- package/dist/server/bunny.d.mts +5 -0
- package/dist/server/bunny.mjs +23 -0
- package/dist/server/cloudflare.d.mts +1 -7
- package/dist/server/cloudflare.mjs +3 -7
- package/dist/server/default.d.mts +1 -7
- package/dist/server/default.mjs +3 -7
- package/dist/server/deno.d.mts +1 -7
- package/dist/server/deno.mjs +3 -7
- package/dist/server/node.d.mts +1 -7
- package/dist/server/node.mjs +3 -9
- package/dist/websocket/native.d.mts +0 -2
- package/dist/websocket/native.mjs +1 -5
- package/dist/websocket/node.d.mts +0 -2
- package/dist/websocket/node.mjs +2 -8
- package/dist/websocket/sse.d.mts +1 -4
- package/dist/websocket/sse.mjs +1 -4
- package/package.json +45 -42
- package/server/bunny.d.ts +2 -0
package/dist/adapters/node.mjs
CHANGED
|
@@ -1,125 +1,2 @@
|
|
|
1
|
-
import "../_chunks/
|
|
2
|
-
|
|
3
|
-
import { n as import_websocket_server } from "../_chunks/libs/ws.mjs";
|
|
4
|
-
import { n as Message, r as toBufferLike, t as Peer } from "../_chunks/peer.mjs";
|
|
5
|
-
import { t as StubRequest } from "../_chunks/_request.mjs";
|
|
6
|
-
import { t as WSError } from "../_chunks/error.mjs";
|
|
7
|
-
|
|
8
|
-
//#region src/adapters/node.ts
|
|
9
|
-
const nodeAdapter = (options = {}) => {
|
|
10
|
-
if ("Deno" in globalThis || "Bun" in globalThis) throw new Error("[crossws] Using Node.js adapter in an incompatible environment.");
|
|
11
|
-
const hooks = new AdapterHookable(options);
|
|
12
|
-
const globalPeers = /* @__PURE__ */ new Map();
|
|
13
|
-
const wss = options.wss || new import_websocket_server.default({
|
|
14
|
-
noServer: true,
|
|
15
|
-
handleProtocols: () => false,
|
|
16
|
-
...options.serverOptions
|
|
17
|
-
});
|
|
18
|
-
wss.on("connection", (ws, nodeReq) => {
|
|
19
|
-
const request = new NodeReqProxy(nodeReq);
|
|
20
|
-
const peers = getPeers(globalPeers, nodeReq._namespace);
|
|
21
|
-
const peer = new NodePeer({
|
|
22
|
-
ws,
|
|
23
|
-
request,
|
|
24
|
-
peers,
|
|
25
|
-
nodeReq,
|
|
26
|
-
namespace: nodeReq._namespace
|
|
27
|
-
});
|
|
28
|
-
peers.add(peer);
|
|
29
|
-
hooks.callHook("open", peer);
|
|
30
|
-
ws.on("message", (data) => {
|
|
31
|
-
if (Array.isArray(data)) data = Buffer.concat(data);
|
|
32
|
-
hooks.callHook("message", peer, new Message(data, peer));
|
|
33
|
-
});
|
|
34
|
-
ws.on("error", (error) => {
|
|
35
|
-
peers.delete(peer);
|
|
36
|
-
hooks.callHook("error", peer, new WSError(error));
|
|
37
|
-
});
|
|
38
|
-
ws.on("close", (code, reason) => {
|
|
39
|
-
peers.delete(peer);
|
|
40
|
-
hooks.callHook("close", peer, {
|
|
41
|
-
code,
|
|
42
|
-
reason: reason?.toString()
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
wss.on("headers", (outgoingHeaders, req) => {
|
|
47
|
-
const upgradeHeaders = req._upgradeHeaders;
|
|
48
|
-
if (upgradeHeaders) for (const [key, value] of new Headers(upgradeHeaders)) outgoingHeaders.push(`${key}: ${value}`);
|
|
49
|
-
});
|
|
50
|
-
return {
|
|
51
|
-
...adapterUtils(globalPeers),
|
|
52
|
-
handleUpgrade: async (nodeReq, socket, head, webRequest) => {
|
|
53
|
-
const request = webRequest || new NodeReqProxy(nodeReq);
|
|
54
|
-
const { upgradeHeaders, endResponse, context, namespace } = await hooks.upgrade(request);
|
|
55
|
-
if (endResponse) return sendResponse(socket, endResponse);
|
|
56
|
-
nodeReq._request = request;
|
|
57
|
-
nodeReq._upgradeHeaders = upgradeHeaders;
|
|
58
|
-
nodeReq._context = context;
|
|
59
|
-
nodeReq._namespace = namespace;
|
|
60
|
-
wss.handleUpgrade(nodeReq, socket, head, (ws) => {
|
|
61
|
-
wss.emit("connection", ws, nodeReq);
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
closeAll: (code, data, force) => {
|
|
65
|
-
for (const client of wss.clients) if (force) client.terminate();
|
|
66
|
-
else client.close(code, data);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
var node_default = nodeAdapter;
|
|
71
|
-
var NodePeer = class extends Peer {
|
|
72
|
-
get remoteAddress() {
|
|
73
|
-
return this._internal.nodeReq.socket?.remoteAddress;
|
|
74
|
-
}
|
|
75
|
-
get context() {
|
|
76
|
-
return this._internal.nodeReq._context;
|
|
77
|
-
}
|
|
78
|
-
send(data, options) {
|
|
79
|
-
const dataBuff = toBufferLike(data);
|
|
80
|
-
const isBinary = typeof dataBuff !== "string";
|
|
81
|
-
this._internal.ws.send(dataBuff, {
|
|
82
|
-
compress: options?.compress,
|
|
83
|
-
binary: isBinary,
|
|
84
|
-
...options
|
|
85
|
-
});
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
publish(topic, data, options) {
|
|
89
|
-
const dataBuff = toBufferLike(data);
|
|
90
|
-
const isBinary = typeof data !== "string";
|
|
91
|
-
const sendOptions = {
|
|
92
|
-
compress: options?.compress,
|
|
93
|
-
binary: isBinary,
|
|
94
|
-
...options
|
|
95
|
-
};
|
|
96
|
-
for (const peer of this._internal.peers) if (peer !== this && peer._topics.has(topic)) peer._internal.ws.send(dataBuff, sendOptions);
|
|
97
|
-
}
|
|
98
|
-
close(code, data) {
|
|
99
|
-
this._internal.ws.close(code, data);
|
|
100
|
-
}
|
|
101
|
-
terminate() {
|
|
102
|
-
this._internal.ws.terminate();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
var NodeReqProxy = class extends StubRequest {
|
|
106
|
-
constructor(req) {
|
|
107
|
-
const host = req.headers["host"] || "localhost";
|
|
108
|
-
const url = `${req.socket?.encrypted ?? req.headers["x-forwarded-proto"] === "https" ? "https" : "http"}://${host}${req.url}`;
|
|
109
|
-
super(url, { headers: req.headers });
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
async function sendResponse(socket, res) {
|
|
113
|
-
const head = [`HTTP/1.1 ${res.status || 200} ${res.statusText || ""}`, ...[...res.headers.entries()].map(([key, value]) => `${encodeURIComponent(key)}: ${encodeURIComponent(value)}`)];
|
|
114
|
-
socket.write(head.join("\r\n") + "\r\n\r\n");
|
|
115
|
-
if (res.body) for await (const chunk of res.body) socket.write(chunk);
|
|
116
|
-
return new Promise((resolve) => {
|
|
117
|
-
socket.end(() => {
|
|
118
|
-
socket.destroy();
|
|
119
|
-
resolve();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
//#endregion
|
|
125
|
-
export { node_default as default };
|
|
1
|
+
import { fromNodeUpgradeHandler, nodeAdapter } from "../_chunks/node.mjs";
|
|
2
|
+
export { nodeAdapter as default, fromNodeUpgradeHandler };
|
package/dist/adapters/sse.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SSEAdapter, SSEOptions, sseAdapter } from "../_chunks/sse.mjs";
|
|
2
2
|
export { SSEAdapter, SSEOptions, sseAdapter as default };
|
package/dist/adapters/sse.mjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
//#region src/adapters/sse.ts
|
|
1
|
+
import { AdapterHookable, adapterUtils, getPeers } from "../_chunks/adapter.mjs";
|
|
2
|
+
import { Message, Peer, toString } from "../_chunks/peer.mjs";
|
|
5
3
|
const sseAdapter = (opts = {}) => {
|
|
6
4
|
const hooks = new AdapterHookable(opts);
|
|
7
5
|
const globalPeers = /* @__PURE__ */ new Map();
|
|
@@ -55,7 +53,6 @@ const sseAdapter = (opts = {}) => {
|
|
|
55
53
|
}
|
|
56
54
|
};
|
|
57
55
|
};
|
|
58
|
-
var sse_default = sseAdapter;
|
|
59
56
|
var SSEPeer = class extends Peer {
|
|
60
57
|
_sseStream;
|
|
61
58
|
_sseStreamController;
|
|
@@ -97,6 +94,4 @@ var SSEPeer = class extends Peer {
|
|
|
97
94
|
var SSEWebSocketStub = class {
|
|
98
95
|
readyState;
|
|
99
96
|
};
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
|
-
export { sse_default as default };
|
|
97
|
+
export { sseAdapter as default };
|
package/dist/adapters/uws.d.mts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Adapter, AdapterInstance, AdapterOptions, Peer, PeerContext } from "../_chunks/adapter.mjs";
|
|
2
|
+
import { WebSocket } from "../_chunks/web.mjs";
|
|
3
3
|
import uws from "uWebSockets.js";
|
|
4
|
-
|
|
5
|
-
//#region src/_request.d.ts
|
|
6
4
|
declare const StubRequest: {
|
|
7
5
|
new (url: string, init?: RequestInit): Request;
|
|
8
6
|
};
|
|
9
|
-
//#endregion
|
|
10
|
-
//#region src/adapters/uws.d.ts
|
|
11
7
|
type UserData = {
|
|
12
8
|
peer?: UWSPeer;
|
|
13
9
|
req: uws.HttpRequest;
|
|
@@ -58,5 +54,4 @@ declare class UwsWebSocketProxy implements Partial<WebSocket> {
|
|
|
58
54
|
get protocol(): string;
|
|
59
55
|
get extensions(): string;
|
|
60
56
|
}
|
|
61
|
-
//#endregion
|
|
62
57
|
export { UWSAdapter, UWSOptions, uwsAdapter as default };
|
package/dist/adapters/uws.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
//#region src/adapters/uws.ts
|
|
1
|
+
import { AdapterHookable, adapterUtils, getPeers } from "../_chunks/adapter.mjs";
|
|
2
|
+
import { Message, Peer, toBufferLike } from "../_chunks/peer.mjs";
|
|
3
|
+
import { StubRequest } from "../_chunks/_request.mjs";
|
|
6
4
|
const uwsAdapter = (options = {}) => {
|
|
7
5
|
const hooks = new AdapterHookable(options);
|
|
8
6
|
const globalPeers = /* @__PURE__ */ new Map();
|
|
@@ -21,7 +19,7 @@ const uwsAdapter = (options = {}) => {
|
|
|
21
19
|
});
|
|
22
20
|
peer._internal.ws.readyState = 3;
|
|
23
21
|
},
|
|
24
|
-
message(ws, message,
|
|
22
|
+
message(ws, message, _isBinary) {
|
|
25
23
|
const peer = getPeer(ws, getPeers(globalPeers, ws.getUserData().namespace));
|
|
26
24
|
hooks.callHook("message", peer, new Message(message, peer));
|
|
27
25
|
},
|
|
@@ -72,7 +70,6 @@ const uwsAdapter = (options = {}) => {
|
|
|
72
70
|
}
|
|
73
71
|
};
|
|
74
72
|
};
|
|
75
|
-
var uws_default = uwsAdapter;
|
|
76
73
|
function getPeer(uws, peers) {
|
|
77
74
|
const uwsData = uws.getUserData();
|
|
78
75
|
if (uwsData.peer) return uwsData.peer;
|
|
@@ -139,6 +136,7 @@ var UWSReqProxy = class extends StubRequest {
|
|
|
139
136
|
}
|
|
140
137
|
};
|
|
141
138
|
var UwsWebSocketProxy = class {
|
|
139
|
+
_uws;
|
|
142
140
|
readyState = 1;
|
|
143
141
|
constructor(_uws) {
|
|
144
142
|
this._uws = _uws;
|
|
@@ -153,6 +151,4 @@ var UwsWebSocketProxy = class {
|
|
|
153
151
|
return this._uws?.getUserData().extensions;
|
|
154
152
|
}
|
|
155
153
|
};
|
|
156
|
-
|
|
157
|
-
//#endregion
|
|
158
|
-
export { uws_default as default };
|
|
154
|
+
export { uwsAdapter as default };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Adapter } from "../_chunks/adapter.mjs";
|
|
2
|
+
import { NodeAdapter, NodeOptions } from "../_chunks/node.mjs";
|
|
3
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
interface VercelAdapter extends Omit<NodeAdapter, "handleUpgrade"> {
|
|
5
|
+
/**
|
|
6
|
+
* Handle a WebSocket upgrade from a Web `Request` (fetch-style handlers).
|
|
7
|
+
*
|
|
8
|
+
* Returns a `204` {@link Response} when the upgrade was handled, or
|
|
9
|
+
* `undefined` when the request is not a WebSocket upgrade or Vercel's upgrade
|
|
10
|
+
* context is unavailable.
|
|
11
|
+
*/
|
|
12
|
+
handleWebUpgrade(request: Request): Promise<Response | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Handle a WebSocket upgrade from a Node.js `IncomingMessage` (Node-style
|
|
15
|
+
* handlers).
|
|
16
|
+
*
|
|
17
|
+
* Returns `true` when the upgrade was handled (and ends `res` with `204`), or
|
|
18
|
+
* `false` when the request is not a WebSocket upgrade or Vercel's upgrade
|
|
19
|
+
* context is unavailable.
|
|
20
|
+
*/
|
|
21
|
+
handleNodeUpgrade(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
interface VercelOptions extends NodeOptions {}
|
|
24
|
+
declare const vercelAdapter: Adapter<VercelAdapter, VercelOptions>;
|
|
25
|
+
export { VercelAdapter, VercelOptions, vercelAdapter as default };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { nodeAdapter } from "../_chunks/node.mjs";
|
|
2
|
+
import { NodeRequest } from "srvx/node";
|
|
3
|
+
const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for("@vercel/request-context");
|
|
4
|
+
const vercelAdapter = (options = {}) => {
|
|
5
|
+
const wss = nodeAdapter(options);
|
|
6
|
+
async function handleWebUpgrade(request) {
|
|
7
|
+
if (!_isWsUpgrade(request.method, request.headers.get("upgrade") || void 0)) return;
|
|
8
|
+
const upgrade = _getVercelUpgrade();
|
|
9
|
+
if (!upgrade) return;
|
|
10
|
+
await wss.handleUpgrade(upgrade.req, upgrade.socket, upgrade.head, request);
|
|
11
|
+
return new Response(null, { status: 204 });
|
|
12
|
+
}
|
|
13
|
+
async function handleNodeUpgrade(req, res) {
|
|
14
|
+
if (!_isWsUpgrade(req.method, req.headers.upgrade)) return false;
|
|
15
|
+
const upgrade = _getVercelUpgrade();
|
|
16
|
+
if (!upgrade) return false;
|
|
17
|
+
await wss.handleUpgrade(upgrade.req, upgrade.socket, upgrade.head, new NodeRequest({
|
|
18
|
+
req,
|
|
19
|
+
res
|
|
20
|
+
}));
|
|
21
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
22
|
+
res.statusCode = 204;
|
|
23
|
+
res.end();
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const { handleUpgrade: _, ...rest } = wss;
|
|
28
|
+
return {
|
|
29
|
+
...rest,
|
|
30
|
+
handleWebUpgrade,
|
|
31
|
+
handleNodeUpgrade
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
function _isWsUpgrade(method, upgradeHeader) {
|
|
35
|
+
return method === "GET" && upgradeHeader?.toLowerCase?.() === "websocket";
|
|
36
|
+
}
|
|
37
|
+
function _getVercelUpgrade() {
|
|
38
|
+
const upgrade = _getVercelRequestContext()?.upgradeWebSocket?.();
|
|
39
|
+
return upgrade?.req && upgrade?.socket && upgrade?.head ? upgrade : void 0;
|
|
40
|
+
}
|
|
41
|
+
function _getVercelRequestContext() {
|
|
42
|
+
const store = globalThis[VERCEL_REQUEST_CONTEXT_SYMBOL];
|
|
43
|
+
if (typeof store?.get !== "function") return;
|
|
44
|
+
const context = store.get();
|
|
45
|
+
if (!context || typeof context !== "object") return;
|
|
46
|
+
return context;
|
|
47
|
+
}
|
|
48
|
+
export { vercelAdapter as default };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Adapter, AdapterInstance, AdapterInternal, AdapterOptions, Hooks, Message, Peer, PeerContext, ResolveHooks, WSError, defineHooks, defineWebSocketAdapter } from "./_chunks/adapter.mjs";
|
|
2
|
+
import { ServerWithWSOptions, WSOptions } from "./_chunks/_types.mjs";
|
|
3
|
+
interface WebSocketProxyOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Target WebSocket URL to proxy to (`ws://` or `wss://`).
|
|
6
|
+
*
|
|
7
|
+
* Can be a static string/URL or a function that resolves the target dynamically
|
|
8
|
+
* based on the incoming {@link Peer}.
|
|
9
|
+
*/
|
|
10
|
+
target: string | URL | ((peer: Peer) => string | URL);
|
|
11
|
+
/**
|
|
12
|
+
* Subprotocol(s) to offer the upstream during the handshake.
|
|
13
|
+
*
|
|
14
|
+
* - `true` (default) — forward the client's `sec-websocket-protocol` verbatim.
|
|
15
|
+
* - `false` — offer no subprotocol upstream.
|
|
16
|
+
* - `string` / `string[]` — offer a fixed subprotocol (or list) upstream,
|
|
17
|
+
* regardless of what the client requested.
|
|
18
|
+
* - `Record<string, string>` — rewrite map applied to the client's offered
|
|
19
|
+
* tokens: a token that matches a key is swapped for its value; tokens not
|
|
20
|
+
* in the map are forwarded verbatim.
|
|
21
|
+
* - function — resolve the upstream subprotocol(s) per {@link Peer}. Return a
|
|
22
|
+
* string, an array of strings, or `undefined` to offer none. Useful when the
|
|
23
|
+
* rewrite depends on more than the token value alone.
|
|
24
|
+
*
|
|
25
|
+
* Note: this controls only what is offered to the *upstream*. The subprotocol
|
|
26
|
+
* echoed back to the *client* remains the first token the client offered (per
|
|
27
|
+
* RFC 6455, the selected protocol must be one the client proposed).
|
|
28
|
+
*
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
forwardProtocol?: boolean | string | string[] | Record<string, string> | ((peer: Peer) => string | string[] | undefined | void);
|
|
32
|
+
/**
|
|
33
|
+
* Maximum number of bytes buffered per peer while the upstream connection
|
|
34
|
+
* is still opening. If exceeded, the peer is closed with code `1009`
|
|
35
|
+
* (Message Too Big). Set to `0` to disable the limit.
|
|
36
|
+
*
|
|
37
|
+
* @default 1048576 (1 MiB)
|
|
38
|
+
*/
|
|
39
|
+
maxBufferSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Milliseconds to wait for the upstream WebSocket handshake to complete.
|
|
42
|
+
* If the upstream does not open within the timeout, the peer is closed
|
|
43
|
+
* with code `1011`. Set to `0` to disable the timeout.
|
|
44
|
+
*
|
|
45
|
+
* @default 10000
|
|
46
|
+
*/
|
|
47
|
+
connectTimeout?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Custom `WebSocket` constructor used to dial the upstream. Useful when
|
|
50
|
+
* the runtime does not expose a global `WebSocket` (Node.js < 22) or
|
|
51
|
+
* when you want to use a different client implementation (e.g. `ws`,
|
|
52
|
+
* `undici`, a mock for tests).
|
|
53
|
+
*
|
|
54
|
+
* @default globalThis.WebSocket
|
|
55
|
+
*/
|
|
56
|
+
WebSocket?: typeof WebSocket;
|
|
57
|
+
/**
|
|
58
|
+
* Extra headers to send on the upstream handshake. Can be a static
|
|
59
|
+
* object or a resolver called per peer.
|
|
60
|
+
*
|
|
61
|
+
* Useful to forward identity from the incoming request (`cookie`,
|
|
62
|
+
* `authorization`, `origin`), or to inject a shared secret the
|
|
63
|
+
* upstream expects.
|
|
64
|
+
*
|
|
65
|
+
* > [!NOTE]
|
|
66
|
+
* > The WHATWG global `WebSocket` constructor does not accept custom
|
|
67
|
+
* > headers — this option is only honored by `WebSocket` constructors
|
|
68
|
+
* > that take a third options argument (e.g. `ws`, `undici`). Pass
|
|
69
|
+
* > one via the {@link WebSocket} option to use it.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* createWebSocketProxy({
|
|
74
|
+
* target: "wss://backend.example.com",
|
|
75
|
+
* WebSocket: WsFromNodeWs,
|
|
76
|
+
* headers: (peer) => ({
|
|
77
|
+
* cookie: peer.request.headers.get("cookie") ?? "",
|
|
78
|
+
* "x-forwarded-for": peer.remoteAddress ?? "",
|
|
79
|
+
* }),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
headers?: HeadersInit | ((peer: Peer) => HeadersInit | undefined | void);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a set of crossws hooks that proxy incoming WebSocket connections
|
|
87
|
+
* to an upstream `ws://` or `wss://` target.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { createWebSocketProxy } from "crossws";
|
|
92
|
+
*
|
|
93
|
+
* const hooks = createWebSocketProxy("wss://echo.websocket.org");
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function createWebSocketProxy(target: WebSocketProxyOptions["target"] | WebSocketProxyOptions): Partial<Hooks>;
|
|
97
|
+
export { type Adapter, type AdapterInstance, type AdapterInternal, type AdapterOptions, type Hooks, type Message, type Peer, type PeerContext, type ResolveHooks, type ServerWithWSOptions, type WSError, type WSOptions, type WebSocketProxyOptions, createWebSocketProxy, defineHooks, defineWebSocketAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { defineHooks, defineWebSocketAdapter } from "./_chunks/adapter.mjs";
|
|
2
|
+
const DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
3
|
+
const DEFAULT_CONNECT_TIMEOUT = 1e4;
|
|
4
|
+
const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
5
|
+
function createWebSocketProxy(target) {
|
|
6
|
+
const options = typeof target === "string" || target instanceof URL || typeof target === "function" ? { target } : target;
|
|
7
|
+
const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket;
|
|
8
|
+
if (typeof WebSocketCtor !== "function") throw new TypeError("createWebSocketProxy requires a `WebSocket` constructor. Pass one via the `WebSocket` option, or use a runtime that provides a global `WebSocket` (Node.js >= 22, Bun, Deno, Cloudflare Workers, browsers).");
|
|
9
|
+
const upstreams = /* @__PURE__ */ new Map();
|
|
10
|
+
return {
|
|
11
|
+
upgrade(request) {
|
|
12
|
+
const reqProtocol = request.headers.get("sec-websocket-protocol");
|
|
13
|
+
if (options.forwardProtocol === false || !reqProtocol) return;
|
|
14
|
+
const accepted = _splitProtocolHeader(reqProtocol)[0];
|
|
15
|
+
if (!accepted || !TOKEN_RE.test(accepted)) return;
|
|
16
|
+
return { headers: { "sec-websocket-protocol": accepted } };
|
|
17
|
+
},
|
|
18
|
+
open(peer) {
|
|
19
|
+
let ws;
|
|
20
|
+
try {
|
|
21
|
+
const url = _resolveTarget(options.target, peer);
|
|
22
|
+
const protocols = _resolveProtocols(peer, options.forwardProtocol);
|
|
23
|
+
const wsOptions = _resolveWsOptions(options.headers, peer);
|
|
24
|
+
ws = wsOptions ? new WebSocketCtor(url, protocols, wsOptions) : new WebSocketCtor(url, protocols);
|
|
25
|
+
ws.binaryType = "arraybuffer";
|
|
26
|
+
} catch {
|
|
27
|
+
_safeClose(peer, 1011, "Upstream setup failed");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const state = {
|
|
31
|
+
ws,
|
|
32
|
+
buffer: [],
|
|
33
|
+
bufferSize: 0,
|
|
34
|
+
open: false,
|
|
35
|
+
timeout: void 0
|
|
36
|
+
};
|
|
37
|
+
upstreams.set(peer.id, state);
|
|
38
|
+
const timeoutMs = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
|
|
39
|
+
if (timeoutMs > 0) state.timeout = setTimeout(() => {
|
|
40
|
+
if (upstreams.get(peer.id) !== state || state.open) return;
|
|
41
|
+
_cleanupState(upstreams, peer.id, state);
|
|
42
|
+
_safeClose(peer, 1011, "Upstream connect timeout");
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
ws.addEventListener("open", () => {
|
|
45
|
+
_clearTimeout(state);
|
|
46
|
+
state.open = true;
|
|
47
|
+
for (const data of state.buffer) ws.send(data);
|
|
48
|
+
state.buffer.length = 0;
|
|
49
|
+
state.bufferSize = 0;
|
|
50
|
+
});
|
|
51
|
+
ws.addEventListener("message", (event) => {
|
|
52
|
+
_safeSend(peer, event.data);
|
|
53
|
+
});
|
|
54
|
+
ws.addEventListener("close", (event) => {
|
|
55
|
+
if (upstreams.get(peer.id) !== state) return;
|
|
56
|
+
_cleanupState(upstreams, peer.id, state);
|
|
57
|
+
_safeClose(peer, _remapIncomingCode(event.code), event.reason);
|
|
58
|
+
});
|
|
59
|
+
ws.addEventListener("error", () => {
|
|
60
|
+
if (upstreams.get(peer.id) !== state) return;
|
|
61
|
+
_cleanupState(upstreams, peer.id, state);
|
|
62
|
+
_safeClose(peer, 1011, "Upstream error");
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
message(peer, message) {
|
|
66
|
+
const state = upstreams.get(peer.id);
|
|
67
|
+
if (!state) return;
|
|
68
|
+
const raw = typeof message.rawData === "string" ? message.rawData : message.uint8Array();
|
|
69
|
+
if (state.open) {
|
|
70
|
+
try {
|
|
71
|
+
state.ws.send(raw);
|
|
72
|
+
} catch {}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const size = typeof raw === "string" ? raw.length * 3 : raw.byteLength;
|
|
76
|
+
const limit = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
77
|
+
if (limit > 0 && state.bufferSize + size > limit) {
|
|
78
|
+
_cleanupState(upstreams, peer.id, state);
|
|
79
|
+
_safeClose(peer, 1009, "Proxy buffer limit exceeded");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
state.buffer.push(typeof raw === "string" ? raw : Uint8Array.from(raw));
|
|
83
|
+
state.bufferSize += size;
|
|
84
|
+
},
|
|
85
|
+
close(peer, details) {
|
|
86
|
+
const state = upstreams.get(peer.id);
|
|
87
|
+
if (!state) return;
|
|
88
|
+
_clearTimeout(state);
|
|
89
|
+
upstreams.delete(peer.id);
|
|
90
|
+
try {
|
|
91
|
+
state.ws.close(_normalizeOutgoingCode(details.code), _truncateReason(details.reason));
|
|
92
|
+
} catch {}
|
|
93
|
+
},
|
|
94
|
+
error(peer) {
|
|
95
|
+
const state = upstreams.get(peer.id);
|
|
96
|
+
if (!state) return;
|
|
97
|
+
_clearTimeout(state);
|
|
98
|
+
upstreams.delete(peer.id);
|
|
99
|
+
try {
|
|
100
|
+
state.ws.close(1011, "Peer error");
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function _cleanupState(upstreams, id, state) {
|
|
106
|
+
_clearTimeout(state);
|
|
107
|
+
upstreams.delete(id);
|
|
108
|
+
try {
|
|
109
|
+
state.ws.close();
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
function _clearTimeout(state) {
|
|
113
|
+
if (state.timeout !== void 0) {
|
|
114
|
+
clearTimeout(state.timeout);
|
|
115
|
+
state.timeout = void 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function _resolveTarget(target, peer) {
|
|
119
|
+
const raw = typeof target === "function" ? target(peer) : target;
|
|
120
|
+
return raw instanceof URL ? raw : new URL(raw);
|
|
121
|
+
}
|
|
122
|
+
function _resolveWsOptions(headers, peer) {
|
|
123
|
+
if (!headers) return;
|
|
124
|
+
const resolved = typeof headers === "function" ? headers(peer) : headers;
|
|
125
|
+
if (!resolved) return;
|
|
126
|
+
return { headers: resolved };
|
|
127
|
+
}
|
|
128
|
+
function _resolveProtocols(peer, forwardProtocol) {
|
|
129
|
+
if (forwardProtocol === false) return;
|
|
130
|
+
if (typeof forwardProtocol === "function") return _normalizeProtocols(forwardProtocol(peer));
|
|
131
|
+
if (typeof forwardProtocol === "string" || Array.isArray(forwardProtocol)) return _normalizeProtocols(forwardProtocol);
|
|
132
|
+
const header = peer.request?.headers.get("sec-websocket-protocol");
|
|
133
|
+
if (!header) return;
|
|
134
|
+
const offered = _splitProtocolHeader(header);
|
|
135
|
+
if (forwardProtocol && typeof forwardProtocol === "object") {
|
|
136
|
+
const map = forwardProtocol;
|
|
137
|
+
return _normalizeProtocols(offered.map((p) => Object.prototype.hasOwnProperty.call(map, p) ? map[p] : p));
|
|
138
|
+
}
|
|
139
|
+
return _normalizeProtocols(offered);
|
|
140
|
+
}
|
|
141
|
+
function _splitProtocolHeader(header) {
|
|
142
|
+
return header.split(",").map((p) => p.trim()).filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
function _normalizeProtocols(value) {
|
|
145
|
+
if (value == null) return;
|
|
146
|
+
const list = (Array.isArray(value) ? value : [value]).filter((p) => p != null).map((p) => String(p).trim()).filter(Boolean);
|
|
147
|
+
const deduped = [...new Set(list)];
|
|
148
|
+
return deduped.length > 0 ? deduped : void 0;
|
|
149
|
+
}
|
|
150
|
+
function _safeClose(peer, code, reason) {
|
|
151
|
+
try {
|
|
152
|
+
peer.close(code, _truncateReason(reason));
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
function _safeSend(peer, data) {
|
|
156
|
+
try {
|
|
157
|
+
peer.send(data);
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
function _truncateReason(reason) {
|
|
161
|
+
if (!reason) return reason;
|
|
162
|
+
const bytes = new TextEncoder().encode(reason);
|
|
163
|
+
if (bytes.length <= 123) return reason;
|
|
164
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, 123));
|
|
165
|
+
}
|
|
166
|
+
function _remapIncomingCode(code) {
|
|
167
|
+
if (code === void 0) return void 0;
|
|
168
|
+
if (code === 1005) return 1e3;
|
|
169
|
+
if (code === 1006 || code === 1015) return 1011;
|
|
170
|
+
return code;
|
|
171
|
+
}
|
|
172
|
+
function _normalizeOutgoingCode(code) {
|
|
173
|
+
if (code === void 0) return void 0;
|
|
174
|
+
if (code === 1e3) return 1e3;
|
|
175
|
+
if (code >= 3e3 && code <= 4999) return code;
|
|
176
|
+
return 1e3;
|
|
177
|
+
}
|
|
178
|
+
export { createWebSocketProxy, defineHooks, defineWebSocketAdapter };
|
package/dist/server/bun.d.mts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import "../_chunks/
|
|
2
|
-
import "../_chunks/cloudflare.mjs";
|
|
3
|
-
import "../_chunks/node.mjs";
|
|
4
|
-
import { n as WSOptions, t as ServerWithWSOptions } from "../_chunks/_types.mjs";
|
|
1
|
+
import { ServerWithWSOptions, WSOptions } from "../_chunks/_types.mjs";
|
|
5
2
|
import { Server, ServerPlugin } from "srvx";
|
|
6
|
-
|
|
7
|
-
//#region src/server/bun.d.ts
|
|
8
3
|
declare function plugin(wsOpts: WSOptions): ServerPlugin;
|
|
9
4
|
declare function serve(options: ServerWithWSOptions): Server;
|
|
10
|
-
//#endregion
|
|
11
5
|
export { plugin, serve };
|
package/dist/server/bun.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import bunAdapter from "../adapters/bun.mjs";
|
|
2
2
|
import { serve as serve$1 } from "srvx/bun";
|
|
3
|
-
|
|
4
|
-
//#region src/server/bun.ts
|
|
5
3
|
function plugin(wsOpts) {
|
|
6
4
|
return (server) => {
|
|
7
|
-
const ws =
|
|
5
|
+
const ws = bunAdapter({
|
|
8
6
|
hooks: wsOpts,
|
|
9
7
|
resolve: wsOpts.resolve,
|
|
10
8
|
...wsOpts.options?.bun
|
|
@@ -25,6 +23,4 @@ function serve(options) {
|
|
|
25
23
|
}
|
|
26
24
|
return serve$1(options);
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
//#endregion
|
|
30
|
-
export { plugin, serve };
|
|
26
|
+
export { plugin, serve };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import bunnyAdapter from "../adapters/bunny.mjs";
|
|
2
|
+
import { serve as serve$1 } from "srvx/bunny";
|
|
3
|
+
function plugin(wsOpts) {
|
|
4
|
+
return (server) => {
|
|
5
|
+
const ws = bunnyAdapter({
|
|
6
|
+
hooks: wsOpts,
|
|
7
|
+
resolve: wsOpts.resolve,
|
|
8
|
+
...wsOpts.options?.bunny
|
|
9
|
+
});
|
|
10
|
+
server.options.middleware.unshift((req, next) => {
|
|
11
|
+
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") return ws.handleUpgrade(req);
|
|
12
|
+
return next();
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function serve(options) {
|
|
17
|
+
if (options.websocket) {
|
|
18
|
+
options.plugins ||= [];
|
|
19
|
+
options.plugins.push(plugin(options.websocket));
|
|
20
|
+
}
|
|
21
|
+
return serve$1(options);
|
|
22
|
+
}
|
|
23
|
+
export { plugin, serve };
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import "../_chunks/
|
|
2
|
-
import "../_chunks/cloudflare.mjs";
|
|
3
|
-
import "../_chunks/node.mjs";
|
|
4
|
-
import { n as WSOptions, t as ServerWithWSOptions } from "../_chunks/_types.mjs";
|
|
1
|
+
import { ServerWithWSOptions, WSOptions } from "../_chunks/_types.mjs";
|
|
5
2
|
import { Server, ServerPlugin } from "srvx";
|
|
6
|
-
|
|
7
|
-
//#region src/server/cloudflare.d.ts
|
|
8
3
|
declare function plugin(wsOpts: WSOptions): ServerPlugin;
|
|
9
4
|
declare function serve(options: ServerWithWSOptions): Server;
|
|
10
|
-
//#endregion
|
|
11
5
|
export { plugin, serve };
|