neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +573 -690
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +615 -846
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +2 -446
- package/dist/dev/functions.js +54 -68
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +2 -51
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +728 -984
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +2 -2193
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +2 -846
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +2 -2030
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +8 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +130 -189
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +39 -45
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
package/dist/dev/websocket.js
CHANGED
|
@@ -1,1031 +1,838 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket support for the `neon dev` server.
|
|
3
|
-
*
|
|
4
|
-
* Node's `http.Server` emits `'upgrade'` — not `'request'` — for anything carrying
|
|
5
|
-
* `Connection: Upgrade`. With no `'upgrade'` listener registered, Node routes the
|
|
6
|
-
* handshake to the ordinary request handler, which answers `200 OK` on a connection
|
|
7
|
-
* the client is waiting to see a `101` on. That is why WebSockets did not work under
|
|
8
|
-
* `neon dev`: a function's `upgrade` export was never called, and the client saw a
|
|
9
|
-
* nonsense 200 rather than an error.
|
|
10
|
-
*
|
|
11
|
-
* This module provides both shapes the deployed runtime supports:
|
|
12
|
-
*
|
|
13
|
-
* 1. `export function upgrade(req, socket, head)` — handed the raw Node triple,
|
|
14
|
-
* exactly like production. The bundle owns the handshake and framing.
|
|
15
|
-
* 2. `upgradeWebSocket(request)` from `@neon/functions`, called inside `fetch` —
|
|
16
|
-
* reached only when there is no `upgrade` export, matching the runtime's
|
|
17
|
-
* precedence rule so local behaviour cannot diverge from deployed behaviour.
|
|
18
|
-
*
|
|
19
|
-
* The protocol implementation here mirrors the deployed runtime's. Duplicating it is
|
|
20
|
-
* deliberate for now: the runtime is a zero-dependency file baked into a microVM image
|
|
21
|
-
* and cannot import from npm, so sharing a module would mean new vendoring machinery.
|
|
22
|
-
* The shared contract is the bridge global and the observable behaviour, which the
|
|
23
|
-
* tests on both sides pin. permessage-deflate is not negotiated on either side.
|
|
24
|
-
*/
|
|
25
|
-
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
26
|
-
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
27
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
28
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
29
|
-
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
30
|
-
};
|
|
31
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
32
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
33
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
34
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
35
|
-
};
|
|
36
|
-
var _DevServerWebSocket_instances, _DevServerWebSocket_socket, _DevServerWebSocket_protocol, _DevServerWebSocket_readyState, _DevServerWebSocket_binaryType, _DevServerWebSocket_bufferedAmount, _DevServerWebSocket_chunks, _DevServerWebSocket_queued, _DevServerWebSocket_fragmentOpcode, _DevServerWebSocket_fragments, _DevServerWebSocket_fragmentBytes, _DevServerWebSocket_closeSent, _DevServerWebSocket_closeTimer, _DevServerWebSocket_handlers, _DevServerWebSocket_setHandler, _DevServerWebSocket_onData, _DevServerWebSocket_parse, _DevServerWebSocket_pushFragment, _DevServerWebSocket_deliver, _DevServerWebSocket_handleControl, _DevServerWebSocket_sendClose, _DevServerWebSocket_fail, _DevServerWebSocket_finishClose, _DevServerWebSocket_emitError, _DevServerWebSocket_writeFrame, _DevServerWebSocket_peek, _DevServerWebSocket_take;
|
|
37
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
38
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/dev/websocket.ts
|
|
4
|
+
/**
|
|
5
|
+
* WebSocket support for the `neon dev` server.
|
|
6
|
+
*
|
|
7
|
+
* Node's `http.Server` emits `'upgrade'` — not `'request'` — for anything carrying
|
|
8
|
+
* `Connection: Upgrade`. With no `'upgrade'` listener registered, Node routes the
|
|
9
|
+
* handshake to the ordinary request handler, which answers `200 OK` on a connection
|
|
10
|
+
* the client is waiting to see a `101` on. That is why WebSockets did not work under
|
|
11
|
+
* `neon dev`: a function's `upgrade` export was never called, and the client saw a
|
|
12
|
+
* nonsense 200 rather than an error.
|
|
13
|
+
*
|
|
14
|
+
* This module provides both shapes the deployed runtime supports:
|
|
15
|
+
*
|
|
16
|
+
* 1. `export function upgrade(req, socket, head)` — handed the raw Node triple,
|
|
17
|
+
* exactly like production. The bundle owns the handshake and framing.
|
|
18
|
+
* 2. `upgradeWebSocket(request)` from `@neon/functions`, called inside `fetch` —
|
|
19
|
+
* reached only when there is no `upgrade` export, matching the runtime's
|
|
20
|
+
* precedence rule so local behaviour cannot diverge from deployed behaviour.
|
|
21
|
+
*
|
|
22
|
+
* The protocol implementation here mirrors the deployed runtime's. Duplicating it is
|
|
23
|
+
* deliberate for now: the runtime is a zero-dependency file baked into a microVM image
|
|
24
|
+
* and cannot import from npm, so sharing a module would mean new vendoring machinery.
|
|
25
|
+
* The shared contract is the bridge global and the observable behaviour, which the
|
|
26
|
+
* tests on both sides pin. permessage-deflate is not negotiated on either side.
|
|
27
|
+
*/
|
|
39
28
|
/** The bridge global `@neon/functions` reads. Must match the deployed runtime. */
|
|
40
29
|
const WS_BRIDGE_KEY = Symbol.for("neon.websocket.bridge");
|
|
41
30
|
/** Brand identifying the response the helper produced. Non-enumerable. */
|
|
42
31
|
const UPGRADE_RECORD = Symbol.for("neon.websocket.upgradeRecord");
|
|
43
32
|
/** RFC 6455 §1.3. */
|
|
44
33
|
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
45
|
-
const OP_CONTINUATION =
|
|
46
|
-
const OP_TEXT =
|
|
47
|
-
const OP_BINARY =
|
|
48
|
-
const OP_CLOSE =
|
|
49
|
-
const OP_PING =
|
|
50
|
-
const OP_PONG =
|
|
34
|
+
const OP_CONTINUATION = 0;
|
|
35
|
+
const OP_TEXT = 1;
|
|
36
|
+
const OP_BINARY = 2;
|
|
37
|
+
const OP_CLOSE = 8;
|
|
38
|
+
const OP_PING = 9;
|
|
39
|
+
const OP_PONG = 10;
|
|
51
40
|
const CONNECTING = 0;
|
|
52
41
|
const OPEN = 1;
|
|
53
42
|
const CLOSING = 2;
|
|
54
43
|
const CLOSED = 3;
|
|
55
44
|
/** Reassembly ceiling: a fragmented message buffers until FIN, so it must be bounded. */
|
|
56
|
-
const MAX_MESSAGE_BYTES =
|
|
45
|
+
const MAX_MESSAGE_BYTES = 16777216;
|
|
57
46
|
/** Control frames carry at most 125 bytes and are never fragmented (§5.5). */
|
|
58
47
|
const MAX_CONTROL_PAYLOAD = 125;
|
|
59
48
|
/** A close reason must fit the 123 bytes left after the 2-byte code. */
|
|
60
49
|
const MAX_CLOSE_REASON_BYTES = 123;
|
|
61
50
|
/** How long to wait for the peer's answering close frame (§7.1.1 leaves this open). */
|
|
62
|
-
const CLOSE_HANDSHAKE_TIMEOUT_MS =
|
|
51
|
+
const CLOSE_HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
63
52
|
const EMPTY = Buffer.alloc(0);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const ErrorEventCtor = typeof globalThis.ErrorEvent === "function"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
export const CloseEventCtor = typeof globalThis.CloseEvent === "function"
|
|
84
|
-
? globalThis
|
|
85
|
-
.CloseEvent
|
|
86
|
-
: ShimCloseEvent;
|
|
87
|
-
export const acceptKey = (key) => createHash("sha1")
|
|
88
|
-
.update(key + WS_GUID)
|
|
89
|
-
.digest("base64");
|
|
53
|
+
var ShimErrorEvent = class extends Event {
|
|
54
|
+
constructor(type, init = {}) {
|
|
55
|
+
super(type);
|
|
56
|
+
this.message = init.message ?? "";
|
|
57
|
+
this.error = init.error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const ErrorEventCtor = typeof globalThis.ErrorEvent === "function" ? globalThis.ErrorEvent : ShimErrorEvent;
|
|
61
|
+
var ShimCloseEvent = class extends Event {
|
|
62
|
+
constructor(type, init = {}) {
|
|
63
|
+
super(type);
|
|
64
|
+
this.code = init.code ?? 0;
|
|
65
|
+
this.reason = init.reason ?? "";
|
|
66
|
+
this.wasClean = init.wasClean ?? false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const CloseEventCtor = typeof globalThis.CloseEvent === "function" ? globalThis.CloseEvent : ShimCloseEvent;
|
|
70
|
+
const acceptKey = (key) => createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
90
71
|
const headerValue = (req, name) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return typeof value === "string" ? value : "";
|
|
72
|
+
const value = req.headers[name];
|
|
73
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
74
|
+
return typeof value === "string" ? value : "";
|
|
95
75
|
};
|
|
96
76
|
/** Does this request carry a WebSocket handshake (rather than some other upgrade)? */
|
|
97
|
-
|
|
98
|
-
headerValue(req, "sec-websocket-key") !== "";
|
|
99
|
-
// -- Request <-> socket association -----------------------------------------
|
|
77
|
+
const isWebSocketHandshake = (req) => headerValue(req, "upgrade").toLowerCase() === "websocket" && headerValue(req, "sec-websocket-key") !== "";
|
|
100
78
|
/** Primary lookup: the exact Request object the dev server built. */
|
|
101
|
-
const pendingByRequest = new WeakMap();
|
|
79
|
+
const pendingByRequest = /* @__PURE__ */ new WeakMap();
|
|
102
80
|
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
81
|
+
* Fallback for adapters that rebuild the Request before the handler sees it —
|
|
82
|
+
* Hono's Node adapter does, so the object identity we keyed on is lost.
|
|
83
|
+
*/
|
|
106
84
|
const upgradeStore = new AsyncLocalStorage();
|
|
107
85
|
const registerUpgrade = (request, parts) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
86
|
+
const record = {
|
|
87
|
+
req: parts.req,
|
|
88
|
+
socket: parts.socket,
|
|
89
|
+
head: parts.head.length ? Buffer.from(parts.head) : EMPTY,
|
|
90
|
+
claimed: false,
|
|
91
|
+
ws: null,
|
|
92
|
+
protocol: "",
|
|
93
|
+
accept: ""
|
|
94
|
+
};
|
|
95
|
+
pendingByRequest.set(request, record);
|
|
96
|
+
return record;
|
|
119
97
|
};
|
|
120
98
|
const readUpgradeRecord = (value) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return (value[UPGRADE_RECORD] ??
|
|
124
|
-
null);
|
|
99
|
+
if (!value || typeof value !== "object") return null;
|
|
100
|
+
return value[UPGRADE_RECORD] ?? null;
|
|
125
101
|
};
|
|
126
|
-
// -- The Response subclass ---------------------------------------------------
|
|
127
102
|
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
// -- The public helper -------------------------------------------------------
|
|
103
|
+
* `new Response(null, { status: 101 })` throws — the fetch spec restricts constructed
|
|
104
|
+
* responses to 200-599. A subclass can still report 101 from the getter while being a
|
|
105
|
+
* genuine `instanceof Response`.
|
|
106
|
+
*
|
|
107
|
+
* The lie does not survive `clone()` or `new Response(res.body, res)`; both drop the
|
|
108
|
+
* brand and the overridden getter. That is detected and failed loudly rather than
|
|
109
|
+
* silently answering 200 on a socket awaiting a 101.
|
|
110
|
+
*/
|
|
111
|
+
var UpgradeResponse = class extends Response {
|
|
112
|
+
constructor(record) {
|
|
113
|
+
super(null, { status: 200 });
|
|
114
|
+
Object.defineProperty(this, UPGRADE_RECORD, {
|
|
115
|
+
value: record,
|
|
116
|
+
enumerable: false,
|
|
117
|
+
configurable: false,
|
|
118
|
+
writable: false
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
get status() {
|
|
122
|
+
return 101;
|
|
123
|
+
}
|
|
124
|
+
get statusText() {
|
|
125
|
+
return "Switching Protocols";
|
|
126
|
+
}
|
|
127
|
+
get ok() {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
157
131
|
/** The implementation behind `upgradeWebSocket()` in `@neon/functions`. */
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (options.protocol != null) {
|
|
189
|
-
if (typeof options.protocol !== "string") {
|
|
190
|
-
throw new TypeError("options.protocol must be a string.");
|
|
191
|
-
}
|
|
192
|
-
const offered = headerValue(req, "sec-websocket-protocol")
|
|
193
|
-
.split(",")
|
|
194
|
-
.map((entry) => entry.trim())
|
|
195
|
-
.filter((entry) => entry !== "");
|
|
196
|
-
if (!offered.includes(options.protocol)) {
|
|
197
|
-
throw new TypeError(`the client did not offer the subprotocol "${options.protocol}" ` +
|
|
198
|
-
`(offered: ${offered.length ? offered.join(", ") : "none"}).`);
|
|
199
|
-
}
|
|
200
|
-
protocol = options.protocol;
|
|
201
|
-
}
|
|
202
|
-
record.claimed = true;
|
|
203
|
-
record.protocol = protocol;
|
|
204
|
-
record.accept = acceptKey(key);
|
|
205
|
-
record.ws = new DevServerWebSocket({
|
|
206
|
-
socket: record.socket,
|
|
207
|
-
url: socketUrl(req),
|
|
208
|
-
protocol,
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
socket: record.ws,
|
|
212
|
-
response: new UpgradeResponse(record),
|
|
213
|
-
};
|
|
132
|
+
const upgradeWebSocket = (request, options = {}) => {
|
|
133
|
+
const record = pendingByRequest.get(request) ?? upgradeStore.getStore();
|
|
134
|
+
if (!record) throw new TypeError("upgradeWebSocket() found no connection to upgrade. It is only usable inside a Neon Functions invocation, on a request carrying a WebSocket handshake.");
|
|
135
|
+
if (record.claimed) throw new TypeError("upgradeWebSocket() was already called for this request; a connection can only be upgraded once.");
|
|
136
|
+
const req = record.req;
|
|
137
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
138
|
+
if (method !== "GET") throw new TypeError(`upgradeWebSocket() requires a GET request, got ${method}.`);
|
|
139
|
+
const key = headerValue(req, "sec-websocket-key");
|
|
140
|
+
if (headerValue(req, "upgrade").toLowerCase() !== "websocket" || key === "") throw new TypeError("upgradeWebSocket() requires a WebSocket handshake: the request must carry `Upgrade: websocket` and `Sec-WebSocket-Key`.");
|
|
141
|
+
const version = headerValue(req, "sec-websocket-version");
|
|
142
|
+
if (version !== "" && version !== "13") throw new TypeError(`unsupported Sec-WebSocket-Version ${version}; only version 13 is supported.`);
|
|
143
|
+
let protocol = "";
|
|
144
|
+
if (options.protocol != null) {
|
|
145
|
+
if (typeof options.protocol !== "string") throw new TypeError("options.protocol must be a string.");
|
|
146
|
+
const offered = headerValue(req, "sec-websocket-protocol").split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
147
|
+
if (!offered.includes(options.protocol)) throw new TypeError(`the client did not offer the subprotocol "${options.protocol}" (offered: ${offered.length ? offered.join(", ") : "none"}).`);
|
|
148
|
+
protocol = options.protocol;
|
|
149
|
+
}
|
|
150
|
+
record.claimed = true;
|
|
151
|
+
record.protocol = protocol;
|
|
152
|
+
record.accept = acceptKey(key);
|
|
153
|
+
record.ws = new DevServerWebSocket({
|
|
154
|
+
socket: record.socket,
|
|
155
|
+
url: socketUrl(req),
|
|
156
|
+
protocol
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
socket: record.ws,
|
|
160
|
+
response: new UpgradeResponse(record)
|
|
161
|
+
};
|
|
214
162
|
};
|
|
215
163
|
const socketUrl = (req) => {
|
|
216
|
-
|
|
217
|
-
const target = typeof req.url === "string" && req.url.startsWith("/") ? req.url : "/";
|
|
218
|
-
return `ws://${host}${target}`;
|
|
164
|
+
return `ws://${headerValue(req, "host") || "localhost"}${typeof req.url === "string" && req.url.startsWith("/") ? req.url : "/"}`;
|
|
219
165
|
};
|
|
220
166
|
/** Publish the bridge so `@neon/functions` resolves under `neon dev`. */
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
167
|
+
const installWebSocketBridge = (target = globalThis) => {
|
|
168
|
+
Object.defineProperty(target, WS_BRIDGE_KEY, {
|
|
169
|
+
value: {
|
|
170
|
+
version: 1,
|
|
171
|
+
upgrade: upgradeWebSocket
|
|
172
|
+
},
|
|
173
|
+
enumerable: false,
|
|
174
|
+
configurable: true,
|
|
175
|
+
writable: true
|
|
176
|
+
});
|
|
228
177
|
};
|
|
229
|
-
// -- Handshake completion ----------------------------------------------------
|
|
230
178
|
/** Headers the handshake owns; a customer value must not override them. */
|
|
231
|
-
const RESERVED_HANDSHAKE_HEADERS = new Set([
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
179
|
+
const RESERVED_HANDSHAKE_HEADERS = /* @__PURE__ */ new Set([
|
|
180
|
+
"connection",
|
|
181
|
+
"upgrade",
|
|
182
|
+
"sec-websocket-accept",
|
|
183
|
+
"sec-websocket-protocol",
|
|
184
|
+
"sec-websocket-extensions",
|
|
185
|
+
"content-length",
|
|
186
|
+
"content-type",
|
|
187
|
+
"transfer-encoding"
|
|
240
188
|
]);
|
|
241
189
|
const completeUpgrade = (record, response) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
// A response without usable headers is not a reason to drop the upgrade.
|
|
273
|
-
}
|
|
274
|
-
socket.write(`${head}\r\n`);
|
|
275
|
-
// A live WebSocket is a long-lived idle-tolerant pipe: no inactivity timeout, and
|
|
276
|
-
// no Nagle delay on small frames. `Duplex` does not declare either method, so
|
|
277
|
-
// feature-detect (a test double may implement neither).
|
|
278
|
-
try {
|
|
279
|
-
const tunable = socket;
|
|
280
|
-
tunable.setTimeout?.(0);
|
|
281
|
-
tunable.setNoDelay?.(true);
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
/* not a real socket */
|
|
285
|
-
}
|
|
286
|
-
ws.openWithHead(record.head);
|
|
190
|
+
const socket = record.socket;
|
|
191
|
+
const ws = record.ws;
|
|
192
|
+
if (!ws) return;
|
|
193
|
+
if (ws.readyState === CLOSED) {
|
|
194
|
+
try {
|
|
195
|
+
socket.end();
|
|
196
|
+
} catch {}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
let head = `HTTP/1.1 101 Switching Protocols\r
|
|
200
|
+
upgrade: websocket\r
|
|
201
|
+
connection: Upgrade\r
|
|
202
|
+
sec-websocket-accept: ${record.accept}\r\n`;
|
|
203
|
+
if (record.protocol) head += `sec-websocket-protocol: ${record.protocol}\r\n`;
|
|
204
|
+
try {
|
|
205
|
+
response.headers.forEach((value, name) => {
|
|
206
|
+
const lower = name.toLowerCase();
|
|
207
|
+
if (RESERVED_HANDSHAKE_HEADERS.has(lower)) return;
|
|
208
|
+
head += `${lower}: ${value}\r\n`;
|
|
209
|
+
});
|
|
210
|
+
} catch {}
|
|
211
|
+
socket.write(`${head}\r\n`);
|
|
212
|
+
try {
|
|
213
|
+
const tunable = socket;
|
|
214
|
+
tunable.setTimeout?.(0);
|
|
215
|
+
tunable.setNoDelay?.(true);
|
|
216
|
+
} catch {}
|
|
217
|
+
ws.openWithHead(record.head);
|
|
287
218
|
};
|
|
288
|
-
// -- Reject / relay helpers --------------------------------------------------
|
|
289
219
|
const STATUS_TEXT = {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
220
|
+
200: "OK",
|
|
221
|
+
400: "Bad Request",
|
|
222
|
+
401: "Unauthorized",
|
|
223
|
+
403: "Forbidden",
|
|
224
|
+
404: "Not Found",
|
|
225
|
+
405: "Method Not Allowed",
|
|
226
|
+
409: "Conflict",
|
|
227
|
+
426: "Upgrade Required",
|
|
228
|
+
429: "Too Many Requests",
|
|
229
|
+
500: "Internal Server Error",
|
|
230
|
+
501: "Not Implemented",
|
|
231
|
+
502: "Bad Gateway",
|
|
232
|
+
503: "Service Unavailable"
|
|
303
233
|
};
|
|
304
234
|
/**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
socket.destroy();
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
/* already destroyed */
|
|
327
|
-
}
|
|
328
|
-
}
|
|
235
|
+
* Answer an upgrade with a plain HTTP response and close. Node gives us a raw socket
|
|
236
|
+
* for `'upgrade'` (no ServerResponse), so the bytes are written by hand. Always ends
|
|
237
|
+
* the socket: an unanswered upgrade socket leaks and leaves the client hanging.
|
|
238
|
+
*/
|
|
239
|
+
const writeUpgradeRejection = (socket, status, reason, extraHeaders = {}) => {
|
|
240
|
+
const body = `${reason}\n`;
|
|
241
|
+
let head = `HTTP/1.1 ${status} ${STATUS_TEXT[status] ?? "Error"}\r\nconnection: close\r
|
|
242
|
+
content-type: text/plain\r
|
|
243
|
+
content-length: ${Buffer.byteLength(body)}\r\n`;
|
|
244
|
+
for (const [name, value] of Object.entries(extraHeaders)) head += `${name.toLowerCase()}: ${value}\r\n`;
|
|
245
|
+
try {
|
|
246
|
+
socket.end(`${head}\r\n${body}`);
|
|
247
|
+
} catch {
|
|
248
|
+
try {
|
|
249
|
+
socket.destroy();
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
329
252
|
};
|
|
330
253
|
/** Headers the raw writer owns; a handler value must not contradict them. */
|
|
331
|
-
const RESERVED_RESPONSE_HEADERS = new Set([
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
254
|
+
const RESERVED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
255
|
+
"connection",
|
|
256
|
+
"content-length",
|
|
257
|
+
"transfer-encoding",
|
|
258
|
+
"keep-alive",
|
|
259
|
+
"upgrade"
|
|
337
260
|
]);
|
|
338
261
|
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
262
|
+
* Write a `Response` the handler returned to decline the upgrade, verbatim: status,
|
|
263
|
+
* headers and body. Declining a handshake with a 401/403/404 is how a function refuses
|
|
264
|
+
* a connection, so the response has to reach the client instead of being replaced.
|
|
265
|
+
*/
|
|
343
266
|
const writeResponseToSocket = async (socket, response) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
socket.end(Buffer.concat([Buffer.from(head, "latin1"), body]));
|
|
267
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
268
|
+
const statusText = response.statusText || (STATUS_TEXT[response.status] ?? "");
|
|
269
|
+
let head = `HTTP/1.1 ${response.status} ${statusText}\r\n`;
|
|
270
|
+
response.headers.forEach((value, name) => {
|
|
271
|
+
if (RESERVED_RESPONSE_HEADERS.has(name.toLowerCase())) return;
|
|
272
|
+
head += `${name.toLowerCase()}: ${value}\r\n`;
|
|
273
|
+
});
|
|
274
|
+
head += "connection: close\r\n";
|
|
275
|
+
head += `content-length: ${body.length}\r\n\r\n`;
|
|
276
|
+
socket.end(Buffer.concat([Buffer.from(head, "latin1"), body]));
|
|
355
277
|
};
|
|
356
278
|
/** RFC 6455 §4.1: a 16-byte nonce, base64-encoded. */
|
|
357
|
-
const isValidWebSocketKey = (key) => /^[A-Za-z0-9+/]{22}==$/.test(key) &&
|
|
358
|
-
Buffer.from(key, "base64").length === 16;
|
|
279
|
+
const isValidWebSocketKey = (key) => /^[A-Za-z0-9+/]{22}==$/.test(key) && Buffer.from(key, "base64").length === 16;
|
|
359
280
|
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (!isValidWebSocketKey(headerValue(req, "sec-websocket-key"))) {
|
|
383
|
-
return {
|
|
384
|
-
status: 400,
|
|
385
|
-
reason: "Sec-WebSocket-Key must be a base64-encoded 16-byte value",
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
return null;
|
|
281
|
+
* Check the parts of the handshake the server owns before any user code runs, so a
|
|
282
|
+
* malformed client request is answered as the client error it is rather than
|
|
283
|
+
* surfacing later as a throw from `upgradeWebSocket()` (which the listener cannot
|
|
284
|
+
* tell apart from a genuine handler bug, and would report as a 502).
|
|
285
|
+
*/
|
|
286
|
+
const validateHandshake = (req) => {
|
|
287
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
288
|
+
if (method !== "GET") return {
|
|
289
|
+
status: 405,
|
|
290
|
+
reason: `a WebSocket handshake must use GET, got ${method}`
|
|
291
|
+
};
|
|
292
|
+
const version = headerValue(req, "sec-websocket-version");
|
|
293
|
+
if (version !== "13") return {
|
|
294
|
+
status: 426,
|
|
295
|
+
reason: `unsupported Sec-WebSocket-Version ${version === "" ? "(absent)" : version}; only version 13 is supported`,
|
|
296
|
+
headers: { "sec-websocket-version": "13" }
|
|
297
|
+
};
|
|
298
|
+
if (!isValidWebSocketKey(headerValue(req, "sec-websocket-key"))) return {
|
|
299
|
+
status: 400,
|
|
300
|
+
reason: "Sec-WebSocket-Key must be a base64-encoded 16-byte value"
|
|
301
|
+
};
|
|
302
|
+
return null;
|
|
389
303
|
};
|
|
390
304
|
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
/* socket already gone */
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
};
|
|
305
|
+
* Build the `'upgrade'` listener for the dev server.
|
|
306
|
+
*
|
|
307
|
+
* Precedence matches the deployed runtime exactly: a legacy `upgrade` export wins
|
|
308
|
+
* unconditionally, and only in its absence is the upgrade routed through `fetch` so
|
|
309
|
+
* `upgradeWebSocket()` can claim it. A function that does neither gets a clean 501
|
|
310
|
+
* rather than the misleading 200 Node produces with no listener at all.
|
|
311
|
+
*/
|
|
312
|
+
const createUpgradeListener = ({ fetch: fetchHandler, upgrade, log = (message) => process.stderr.write(`${message}\n`) }) => {
|
|
313
|
+
return (req, socket, head) => {
|
|
314
|
+
handleUpgrade(req, socket, head, {
|
|
315
|
+
fetch: fetchHandler,
|
|
316
|
+
upgrade,
|
|
317
|
+
log
|
|
318
|
+
}).catch((err) => {
|
|
319
|
+
log(`WebSocket upgrade failed:\n${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
320
|
+
try {
|
|
321
|
+
writeUpgradeRejection(socket, 500, "upgrade error");
|
|
322
|
+
} catch {}
|
|
323
|
+
});
|
|
324
|
+
};
|
|
415
325
|
};
|
|
416
|
-
const handleUpgrade = async (req, socket, head, { fetch: fetchHandler, upgrade, log
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"both discard the upgrade. Return the `response` from upgradeWebSocket() " +
|
|
457
|
-
"unchanged.");
|
|
458
|
-
abortClaimedUpgrade(record);
|
|
459
|
-
writeUpgradeRejection(socket, 500, "websocket_upgrade_response_lost");
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
// The handler declined the upgrade and answered with an ordinary response.
|
|
463
|
-
// Relay it verbatim: returning a 401/403/404 is how a function refuses a
|
|
464
|
-
// handshake, and replacing it with a status of our own would throw away the
|
|
465
|
-
// only answer the client was given.
|
|
466
|
-
await writeResponseToSocket(socket, response);
|
|
326
|
+
const handleUpgrade = async (req, socket, head, { fetch: fetchHandler, upgrade, log }) => {
|
|
327
|
+
if (!isWebSocketHandshake(req) && !upgrade) {
|
|
328
|
+
writeUpgradeRejection(socket, 426, "expected a websocket upgrade");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (upgrade) {
|
|
332
|
+
await upgrade(req, socket, head);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const rejection = validateHandshake(req);
|
|
336
|
+
if (rejection) {
|
|
337
|
+
writeUpgradeRejection(socket, rejection.status, rejection.reason, rejection.headers ?? {});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const request = toWebRequest(req);
|
|
341
|
+
const record = registerUpgrade(request, {
|
|
342
|
+
req,
|
|
343
|
+
socket,
|
|
344
|
+
head
|
|
345
|
+
});
|
|
346
|
+
let response;
|
|
347
|
+
try {
|
|
348
|
+
response = await upgradeStore.run(record, () => fetchHandler(request));
|
|
349
|
+
} catch (err) {
|
|
350
|
+
log(`Request handler threw an error while upgrading:\n${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
351
|
+
abortClaimedUpgrade(record);
|
|
352
|
+
writeUpgradeRejection(socket, 502, "handler error");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (readUpgradeRecord(response) === record) {
|
|
356
|
+
completeUpgrade(record, response);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (record.claimed) {
|
|
360
|
+
log("websocket_upgrade_response_lost: upgradeWebSocket() was called for this request but the response returned from the handler is not the one it produced. `Response.clone()` and rebuilding a response (`new Response(res.body, res)`, as response-rewriting middleware does) both discard the upgrade. Return the `response` from upgradeWebSocket() unchanged.");
|
|
361
|
+
abortClaimedUpgrade(record);
|
|
362
|
+
writeUpgradeRejection(socket, 500, "websocket_upgrade_response_lost");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
await writeResponseToSocket(socket, response);
|
|
467
366
|
};
|
|
468
367
|
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
368
|
+
* The handler claimed the connection but we cannot honour it. Move the socket object
|
|
369
|
+
* to CLOSED so a customer holding it sees a close event instead of one stuck at
|
|
370
|
+
* CONNECTING — without touching the underlying socket, which still needs to carry an
|
|
371
|
+
* HTTP error response.
|
|
372
|
+
*/
|
|
474
373
|
const abortClaimedUpgrade = (record) => {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
catch {
|
|
479
|
-
/* best effort */
|
|
480
|
-
}
|
|
374
|
+
try {
|
|
375
|
+
record.ws?.abort(1011, "upgrade failed");
|
|
376
|
+
} catch {}
|
|
481
377
|
};
|
|
482
378
|
/**
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
379
|
+
* Build a Web `Request` from the Node upgrade. A handshake is a GET with no body, so
|
|
380
|
+
* there is nothing to stream — which is just as well, since the socket has already been
|
|
381
|
+
* detached from the HTTP parser.
|
|
382
|
+
*/
|
|
487
383
|
const toWebRequest = (req) => {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
headers.set(name, value);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return new Request(`http://${host}${target}`, {
|
|
504
|
-
method: req.method ?? "GET",
|
|
505
|
-
headers,
|
|
506
|
-
});
|
|
384
|
+
const host = headerValue(req, "host") || "localhost";
|
|
385
|
+
const target = typeof req.url === "string" && req.url.startsWith("/") ? req.url : "/";
|
|
386
|
+
const headers = new Headers();
|
|
387
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
388
|
+
if (name.startsWith(":")) continue;
|
|
389
|
+
if (Array.isArray(value)) for (const entry of value) headers.append(name, entry);
|
|
390
|
+
else if (value != null) headers.set(name, value);
|
|
391
|
+
}
|
|
392
|
+
return new Request(`http://${host}${target}`, {
|
|
393
|
+
method: req.method ?? "GET",
|
|
394
|
+
headers
|
|
395
|
+
});
|
|
507
396
|
};
|
|
508
397
|
/**
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_closeTimer, null, "f");
|
|
912
|
-
}
|
|
913
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_chunks, [], "f");
|
|
914
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_queued, 0, "f");
|
|
915
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_fragments, [], "f");
|
|
916
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_fragmentBytes, 0, "f");
|
|
917
|
-
try {
|
|
918
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_socket, "f").end();
|
|
919
|
-
}
|
|
920
|
-
catch {
|
|
921
|
-
try {
|
|
922
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_socket, "f").destroy();
|
|
923
|
-
}
|
|
924
|
-
catch {
|
|
925
|
-
/* already gone */
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
this.dispatchEvent(new CloseEventCtor("close", { code, reason, wasClean }));
|
|
929
|
-
}, _DevServerWebSocket_emitError = function _DevServerWebSocket_emitError(err) {
|
|
930
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
931
|
-
this.dispatchEvent(new ErrorEventCtor("error", { message, error: err }));
|
|
932
|
-
}, _DevServerWebSocket_writeFrame = function _DevServerWebSocket_writeFrame(opcode, payload) {
|
|
933
|
-
const frame = encodeFrame(opcode, payload);
|
|
934
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_bufferedAmount, __classPrivateFieldGet(this, _DevServerWebSocket_bufferedAmount, "f") + frame.length, "f");
|
|
935
|
-
try {
|
|
936
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_socket, "f").write(frame, () => {
|
|
937
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_bufferedAmount, __classPrivateFieldGet(this, _DevServerWebSocket_bufferedAmount, "f") - frame.length, "f");
|
|
938
|
-
if (__classPrivateFieldGet(this, _DevServerWebSocket_bufferedAmount, "f") < 0)
|
|
939
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_bufferedAmount, 0, "f");
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
catch (err) {
|
|
943
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_bufferedAmount, __classPrivateFieldGet(this, _DevServerWebSocket_bufferedAmount, "f") - frame.length, "f");
|
|
944
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_instances, "m", _DevServerWebSocket_emitError).call(this, err);
|
|
945
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_instances, "m", _DevServerWebSocket_finishClose).call(this, 1006, "", false);
|
|
946
|
-
}
|
|
947
|
-
}, _DevServerWebSocket_peek = function _DevServerWebSocket_peek(index) {
|
|
948
|
-
let remaining = index;
|
|
949
|
-
for (const chunk of __classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f")) {
|
|
950
|
-
if (remaining < chunk.length)
|
|
951
|
-
return chunk[remaining];
|
|
952
|
-
remaining -= chunk.length;
|
|
953
|
-
}
|
|
954
|
-
return 0;
|
|
955
|
-
}, _DevServerWebSocket_take = function _DevServerWebSocket_take(n) {
|
|
956
|
-
if (n === 0)
|
|
957
|
-
return EMPTY;
|
|
958
|
-
__classPrivateFieldSet(this, _DevServerWebSocket_queued, __classPrivateFieldGet(this, _DevServerWebSocket_queued, "f") - n, "f");
|
|
959
|
-
const first = __classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f")[0];
|
|
960
|
-
if (first.length === n)
|
|
961
|
-
return __classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f").shift();
|
|
962
|
-
if (first.length > n) {
|
|
963
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f")[0] = first.subarray(n);
|
|
964
|
-
// Copy: the caller unmasks in place and the remainder is still queued.
|
|
965
|
-
return Buffer.from(first.subarray(0, n));
|
|
966
|
-
}
|
|
967
|
-
const out = Buffer.allocUnsafe(n);
|
|
968
|
-
let offset = 0;
|
|
969
|
-
while (offset < n) {
|
|
970
|
-
const chunk = __classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f")[0];
|
|
971
|
-
const take = Math.min(chunk.length, n - offset);
|
|
972
|
-
chunk.copy(out, offset, 0, take);
|
|
973
|
-
offset += take;
|
|
974
|
-
if (take === chunk.length)
|
|
975
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f").shift();
|
|
976
|
-
else
|
|
977
|
-
__classPrivateFieldGet(this, _DevServerWebSocket_chunks, "f")[0] = chunk.subarray(take);
|
|
978
|
-
}
|
|
979
|
-
return out;
|
|
398
|
+
* A WHATWG `WebSocket` over an already-upgraded Node socket.
|
|
399
|
+
*
|
|
400
|
+
* Two deliberate divergences from a browser, both matching the deployed runtime:
|
|
401
|
+
* `binaryType` defaults to `"arraybuffer"` rather than `"blob"`, and `bufferedAmount`
|
|
402
|
+
* is what we have written and not yet seen flushed, which approximates rather than
|
|
403
|
+
* matches a browser's accounting.
|
|
404
|
+
*/
|
|
405
|
+
var DevServerWebSocket = class extends EventTarget {
|
|
406
|
+
static {
|
|
407
|
+
this.CONNECTING = CONNECTING;
|
|
408
|
+
}
|
|
409
|
+
static {
|
|
410
|
+
this.OPEN = OPEN;
|
|
411
|
+
}
|
|
412
|
+
static {
|
|
413
|
+
this.CLOSING = CLOSING;
|
|
414
|
+
}
|
|
415
|
+
static {
|
|
416
|
+
this.CLOSED = CLOSED;
|
|
417
|
+
}
|
|
418
|
+
#socket;
|
|
419
|
+
#protocol;
|
|
420
|
+
#readyState = CONNECTING;
|
|
421
|
+
#binaryType = "arraybuffer";
|
|
422
|
+
#bufferedAmount = 0;
|
|
423
|
+
#chunks = [];
|
|
424
|
+
#queued = 0;
|
|
425
|
+
#fragmentOpcode = 0;
|
|
426
|
+
#fragments = [];
|
|
427
|
+
#fragmentBytes = 0;
|
|
428
|
+
#closeSent = false;
|
|
429
|
+
#closeTimer = null;
|
|
430
|
+
#handlers = {
|
|
431
|
+
open: null,
|
|
432
|
+
message: null,
|
|
433
|
+
close: null,
|
|
434
|
+
error: null
|
|
435
|
+
};
|
|
436
|
+
constructor({ socket, url, protocol }) {
|
|
437
|
+
super();
|
|
438
|
+
this.#socket = socket;
|
|
439
|
+
this.url = url;
|
|
440
|
+
this.#protocol = protocol;
|
|
441
|
+
}
|
|
442
|
+
get CONNECTING() {
|
|
443
|
+
return CONNECTING;
|
|
444
|
+
}
|
|
445
|
+
get OPEN() {
|
|
446
|
+
return OPEN;
|
|
447
|
+
}
|
|
448
|
+
get CLOSING() {
|
|
449
|
+
return CLOSING;
|
|
450
|
+
}
|
|
451
|
+
get CLOSED() {
|
|
452
|
+
return CLOSED;
|
|
453
|
+
}
|
|
454
|
+
get protocol() {
|
|
455
|
+
return this.#protocol;
|
|
456
|
+
}
|
|
457
|
+
get extensions() {
|
|
458
|
+
return "";
|
|
459
|
+
}
|
|
460
|
+
get readyState() {
|
|
461
|
+
return this.#readyState;
|
|
462
|
+
}
|
|
463
|
+
get bufferedAmount() {
|
|
464
|
+
return this.#bufferedAmount;
|
|
465
|
+
}
|
|
466
|
+
get binaryType() {
|
|
467
|
+
return this.#binaryType;
|
|
468
|
+
}
|
|
469
|
+
set binaryType(value) {
|
|
470
|
+
if (value !== "arraybuffer" && value !== "blob") throw new DOMException(`binaryType must be "arraybuffer" or "blob", got "${String(value)}"`, "SyntaxError");
|
|
471
|
+
this.#binaryType = value;
|
|
472
|
+
}
|
|
473
|
+
get onopen() {
|
|
474
|
+
return this.#handlers.open;
|
|
475
|
+
}
|
|
476
|
+
set onopen(fn) {
|
|
477
|
+
this.#setHandler("open", fn);
|
|
478
|
+
}
|
|
479
|
+
get onmessage() {
|
|
480
|
+
return this.#handlers.message;
|
|
481
|
+
}
|
|
482
|
+
set onmessage(fn) {
|
|
483
|
+
this.#setHandler("message", fn);
|
|
484
|
+
}
|
|
485
|
+
get onclose() {
|
|
486
|
+
return this.#handlers.close;
|
|
487
|
+
}
|
|
488
|
+
set onclose(fn) {
|
|
489
|
+
this.#setHandler("close", fn);
|
|
490
|
+
}
|
|
491
|
+
get onerror() {
|
|
492
|
+
return this.#handlers.error;
|
|
493
|
+
}
|
|
494
|
+
set onerror(fn) {
|
|
495
|
+
this.#setHandler("error", fn);
|
|
496
|
+
}
|
|
497
|
+
#setHandler(type, fn) {
|
|
498
|
+
const previous = this.#handlers[type];
|
|
499
|
+
if (previous) this.removeEventListener(type, previous);
|
|
500
|
+
this.#handlers[type] = typeof fn === "function" ? fn : null;
|
|
501
|
+
const next = this.#handlers[type];
|
|
502
|
+
if (next) this.addEventListener(type, next);
|
|
503
|
+
}
|
|
504
|
+
send(data) {
|
|
505
|
+
if (this.#readyState === CONNECTING) throw new DOMException("cannot send() while the WebSocket is still CONNECTING; wait for the open event (the socket opens once the handler returns the upgrade response)", "InvalidStateError");
|
|
506
|
+
if (typeof data === "string") {
|
|
507
|
+
const payload = Buffer.from(data, "utf8");
|
|
508
|
+
if (this.#readyState !== OPEN) {
|
|
509
|
+
this.#bufferedAmount += payload.length;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this.#writeFrame(OP_TEXT, payload);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (data instanceof Blob) {
|
|
516
|
+
const size = data.size;
|
|
517
|
+
this.#bufferedAmount += size;
|
|
518
|
+
data.arrayBuffer().then((buf) => {
|
|
519
|
+
this.#bufferedAmount -= size;
|
|
520
|
+
if (this.#readyState === OPEN) this.#writeFrame(OP_BINARY, Buffer.from(buf));
|
|
521
|
+
}).catch(() => {
|
|
522
|
+
this.#bufferedAmount -= size;
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const payload = toBuffer(data);
|
|
527
|
+
if (payload === null) throw new TypeError("send() accepts a string, ArrayBuffer, TypedArray, DataView, or Blob");
|
|
528
|
+
if (this.#readyState !== OPEN) {
|
|
529
|
+
this.#bufferedAmount += payload.length;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.#writeFrame(OP_BINARY, payload);
|
|
533
|
+
}
|
|
534
|
+
close(code, reason) {
|
|
535
|
+
if (code !== void 0 && !isValidCloseCode(code)) throw new DOMException(`close code ${code} is not allowed; use 1000 or a code in 3000-4999`, "InvalidAccessError");
|
|
536
|
+
let reasonBuf = EMPTY;
|
|
537
|
+
if (reason !== void 0 && reason !== null) {
|
|
538
|
+
reasonBuf = Buffer.from(String(reason), "utf8");
|
|
539
|
+
if (reasonBuf.length > MAX_CLOSE_REASON_BYTES) throw new DOMException(`close reason must be at most ${MAX_CLOSE_REASON_BYTES} bytes of UTF-8`, "SyntaxError");
|
|
540
|
+
}
|
|
541
|
+
if (this.#readyState === CLOSING || this.#readyState === CLOSED) return;
|
|
542
|
+
if (this.#readyState === CONNECTING) {
|
|
543
|
+
this.abort(code ?? 1e3, reasonBuf.toString("utf8"));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.#readyState = CLOSING;
|
|
547
|
+
this.#sendClose(code ?? 1e3, reasonBuf);
|
|
548
|
+
this.#closeTimer = setTimeout(() => {
|
|
549
|
+
this.#finishClose(1006, "", false);
|
|
550
|
+
}, CLOSE_HANDSHAKE_TIMEOUT_MS);
|
|
551
|
+
this.#closeTimer.unref?.();
|
|
552
|
+
}
|
|
553
|
+
/** Called once the 101 is on the wire. */
|
|
554
|
+
openWithHead(head) {
|
|
555
|
+
if (this.#readyState !== CONNECTING) return;
|
|
556
|
+
const socket = this.#socket;
|
|
557
|
+
socket.on("data", (chunk) => this.#onData(chunk));
|
|
558
|
+
socket.on("error", (err) => {
|
|
559
|
+
this.#emitError(err);
|
|
560
|
+
this.#finishClose(1006, "", false);
|
|
561
|
+
});
|
|
562
|
+
socket.on("close", () => this.#finishClose(1006, "", false));
|
|
563
|
+
socket.on("end", () => this.#finishClose(1006, "", false));
|
|
564
|
+
this.#readyState = OPEN;
|
|
565
|
+
this.dispatchEvent(new Event("open"));
|
|
566
|
+
if (head.length) this.#onData(head);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Give up before the handshake was written. Fires `close` so a caller holding the
|
|
570
|
+
* socket is not left with something stuck at CONNECTING, but deliberately does not
|
|
571
|
+
* touch the underlying socket: an HTTP error response still has to go out on it.
|
|
572
|
+
*/
|
|
573
|
+
abort(code = 1006, reason = "") {
|
|
574
|
+
if (this.#readyState === CLOSED) return;
|
|
575
|
+
this.#readyState = CLOSED;
|
|
576
|
+
if (this.#closeTimer) {
|
|
577
|
+
clearTimeout(this.#closeTimer);
|
|
578
|
+
this.#closeTimer = null;
|
|
579
|
+
}
|
|
580
|
+
this.dispatchEvent(new CloseEventCtor("close", {
|
|
581
|
+
code,
|
|
582
|
+
reason,
|
|
583
|
+
wasClean: false
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
#onData(chunk) {
|
|
587
|
+
if (this.#readyState === CLOSED) return;
|
|
588
|
+
this.#chunks.push(chunk);
|
|
589
|
+
this.#queued += chunk.length;
|
|
590
|
+
try {
|
|
591
|
+
this.#parse();
|
|
592
|
+
} catch (err) {
|
|
593
|
+
this.#emitError(err);
|
|
594
|
+
const code = err.wsCode ?? 1002;
|
|
595
|
+
this.#fail(code, err instanceof Error ? err.message : String(err));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
#parse() {
|
|
599
|
+
for (;;) {
|
|
600
|
+
if (this.#queued < 2) return;
|
|
601
|
+
const b0 = this.#peek(0);
|
|
602
|
+
const b1 = this.#peek(1);
|
|
603
|
+
const fin = (b0 & 128) !== 0;
|
|
604
|
+
const rsv = b0 & 112;
|
|
605
|
+
const opcode = b0 & 15;
|
|
606
|
+
const masked = (b1 & 128) !== 0;
|
|
607
|
+
let length = b1 & 127;
|
|
608
|
+
let headerLen = 2;
|
|
609
|
+
if (length === 126) {
|
|
610
|
+
if (this.#queued < 4) return;
|
|
611
|
+
length = this.#peek(2) << 8 | this.#peek(3);
|
|
612
|
+
headerLen = 4;
|
|
613
|
+
} else if (length === 127) {
|
|
614
|
+
if (this.#queued < 10) return;
|
|
615
|
+
let big = 0n;
|
|
616
|
+
for (let i = 0; i < 8; i++) big = big << 8n | BigInt(this.#peek(2 + i));
|
|
617
|
+
if (big > BigInt(MAX_MESSAGE_BYTES)) throw protocolError(1009, "frame too large");
|
|
618
|
+
length = Number(big);
|
|
619
|
+
headerLen = 10;
|
|
620
|
+
}
|
|
621
|
+
if (masked) headerLen += 4;
|
|
622
|
+
if (this.#queued < headerLen + length) return;
|
|
623
|
+
if (rsv !== 0) throw protocolError(1002, "reserved bits must be clear (no extensions were negotiated)");
|
|
624
|
+
if (!masked) throw protocolError(1002, "client frames must be masked");
|
|
625
|
+
const isControl = (opcode & 8) !== 0;
|
|
626
|
+
if (isControl) {
|
|
627
|
+
if (!fin) throw protocolError(1002, "control frames must not be fragmented");
|
|
628
|
+
if (length > MAX_CONTROL_PAYLOAD) throw protocolError(1002, "control frame payload exceeds 125 bytes");
|
|
629
|
+
}
|
|
630
|
+
if (opcode !== OP_CONTINUATION && opcode !== OP_TEXT && opcode !== OP_BINARY && opcode !== OP_CLOSE && opcode !== OP_PING && opcode !== OP_PONG) throw protocolError(1002, `reserved opcode 0x${opcode.toString(16)}`);
|
|
631
|
+
const header = this.#take(headerLen);
|
|
632
|
+
const payload = this.#take(length);
|
|
633
|
+
if (masked) {
|
|
634
|
+
const mask = header.subarray(headerLen - 4, headerLen);
|
|
635
|
+
for (let i = 0; i < payload.length; i++) payload[i] = payload[i] ^ mask[i & 3];
|
|
636
|
+
}
|
|
637
|
+
if (isControl) {
|
|
638
|
+
this.#handleControl(opcode, payload);
|
|
639
|
+
if (this.#readyState === CLOSED) return;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (opcode === OP_CONTINUATION) {
|
|
643
|
+
if (this.#fragmentOpcode === 0) throw protocolError(1002, "continuation frame without a start frame");
|
|
644
|
+
this.#pushFragment(payload);
|
|
645
|
+
} else {
|
|
646
|
+
if (this.#fragmentOpcode !== 0) throw protocolError(1002, "new data frame while a fragmented message was open");
|
|
647
|
+
if (!fin) {
|
|
648
|
+
this.#fragmentOpcode = opcode;
|
|
649
|
+
this.#pushFragment(payload);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
this.#deliver(opcode, payload);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (fin) {
|
|
656
|
+
const messageOpcode = this.#fragmentOpcode;
|
|
657
|
+
const full = this.#fragments.length === 1 ? this.#fragments[0] : Buffer.concat(this.#fragments);
|
|
658
|
+
this.#fragmentOpcode = 0;
|
|
659
|
+
this.#fragments = [];
|
|
660
|
+
this.#fragmentBytes = 0;
|
|
661
|
+
this.#deliver(messageOpcode, full);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
#pushFragment(payload) {
|
|
666
|
+
this.#fragmentBytes += payload.length;
|
|
667
|
+
if (this.#fragmentBytes > MAX_MESSAGE_BYTES) throw protocolError(1009, `message exceeds the ${MAX_MESSAGE_BYTES} byte limit`);
|
|
668
|
+
if (payload.length === 0) return;
|
|
669
|
+
this.#fragments.push(payload);
|
|
670
|
+
}
|
|
671
|
+
#deliver(opcode, payload) {
|
|
672
|
+
if (this.#readyState !== OPEN) return;
|
|
673
|
+
let data;
|
|
674
|
+
if (opcode === OP_TEXT) try {
|
|
675
|
+
data = new TextDecoder("utf-8", { fatal: true }).decode(payload);
|
|
676
|
+
} catch {
|
|
677
|
+
throw protocolError(1007, "text frame is not valid UTF-8");
|
|
678
|
+
}
|
|
679
|
+
else if (this.#binaryType === "blob") data = new Blob([new Uint8Array(payload)]);
|
|
680
|
+
else data = payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength);
|
|
681
|
+
this.dispatchEvent(new MessageEvent("message", {
|
|
682
|
+
data,
|
|
683
|
+
origin: this.url
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
#handleControl(opcode, payload) {
|
|
687
|
+
if (opcode === OP_PING) {
|
|
688
|
+
if (this.#readyState === OPEN) this.#writeFrame(OP_PONG, payload);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (opcode === OP_PONG) return;
|
|
692
|
+
let code = 1005;
|
|
693
|
+
let reason = "";
|
|
694
|
+
if (payload.length === 1) throw protocolError(1002, "close frame payload must be empty or at least 2 bytes");
|
|
695
|
+
if (payload.length >= 2) {
|
|
696
|
+
code = payload[0] << 8 | payload[1];
|
|
697
|
+
if (!isEchoableCloseCode(code)) throw protocolError(1002, `invalid close code ${code}`);
|
|
698
|
+
try {
|
|
699
|
+
reason = new TextDecoder("utf-8", { fatal: true }).decode(payload.subarray(2));
|
|
700
|
+
} catch {
|
|
701
|
+
throw protocolError(1007, "close reason is not valid UTF-8");
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!this.#closeSent) this.#sendClose(code === 1005 ? 1e3 : code, EMPTY);
|
|
705
|
+
this.#finishClose(code, reason, true);
|
|
706
|
+
}
|
|
707
|
+
#sendClose(code, reasonBuf) {
|
|
708
|
+
if (this.#closeSent) return;
|
|
709
|
+
this.#closeSent = true;
|
|
710
|
+
const payload = Buffer.allocUnsafe(2 + reasonBuf.length);
|
|
711
|
+
payload.writeUInt16BE(code, 0);
|
|
712
|
+
if (reasonBuf.length) reasonBuf.copy(payload, 2);
|
|
713
|
+
this.#writeFrame(OP_CLOSE, payload);
|
|
714
|
+
}
|
|
715
|
+
#fail(code, reason) {
|
|
716
|
+
if (this.#readyState === OPEN) {
|
|
717
|
+
const reasonBuf = Buffer.from(reason.slice(0, MAX_CLOSE_REASON_BYTES), "utf8");
|
|
718
|
+
try {
|
|
719
|
+
this.#sendClose(code, reasonBuf);
|
|
720
|
+
} catch {}
|
|
721
|
+
}
|
|
722
|
+
this.#finishClose(code, reason, false);
|
|
723
|
+
}
|
|
724
|
+
#finishClose(code, reason, wasClean) {
|
|
725
|
+
if (this.#readyState === CLOSED) return;
|
|
726
|
+
this.#readyState = CLOSED;
|
|
727
|
+
if (this.#closeTimer) {
|
|
728
|
+
clearTimeout(this.#closeTimer);
|
|
729
|
+
this.#closeTimer = null;
|
|
730
|
+
}
|
|
731
|
+
this.#chunks = [];
|
|
732
|
+
this.#queued = 0;
|
|
733
|
+
this.#fragments = [];
|
|
734
|
+
this.#fragmentBytes = 0;
|
|
735
|
+
try {
|
|
736
|
+
this.#socket.end();
|
|
737
|
+
} catch {
|
|
738
|
+
try {
|
|
739
|
+
this.#socket.destroy();
|
|
740
|
+
} catch {}
|
|
741
|
+
}
|
|
742
|
+
this.dispatchEvent(new CloseEventCtor("close", {
|
|
743
|
+
code,
|
|
744
|
+
reason,
|
|
745
|
+
wasClean
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
#emitError(err) {
|
|
749
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
750
|
+
this.dispatchEvent(new ErrorEventCtor("error", {
|
|
751
|
+
message,
|
|
752
|
+
error: err
|
|
753
|
+
}));
|
|
754
|
+
}
|
|
755
|
+
#writeFrame(opcode, payload) {
|
|
756
|
+
const frame = encodeFrame(opcode, payload);
|
|
757
|
+
this.#bufferedAmount += frame.length;
|
|
758
|
+
try {
|
|
759
|
+
this.#socket.write(frame, () => {
|
|
760
|
+
this.#bufferedAmount -= frame.length;
|
|
761
|
+
if (this.#bufferedAmount < 0) this.#bufferedAmount = 0;
|
|
762
|
+
});
|
|
763
|
+
} catch (err) {
|
|
764
|
+
this.#bufferedAmount -= frame.length;
|
|
765
|
+
this.#emitError(err);
|
|
766
|
+
this.#finishClose(1006, "", false);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/** Read a byte without consuming it. */
|
|
770
|
+
#peek(index) {
|
|
771
|
+
let remaining = index;
|
|
772
|
+
for (const chunk of this.#chunks) {
|
|
773
|
+
if (remaining < chunk.length) return chunk[remaining];
|
|
774
|
+
remaining -= chunk.length;
|
|
775
|
+
}
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
/** Remove `n` bytes from the queue and return them as one owned Buffer. */
|
|
779
|
+
#take(n) {
|
|
780
|
+
if (n === 0) return EMPTY;
|
|
781
|
+
this.#queued -= n;
|
|
782
|
+
const first = this.#chunks[0];
|
|
783
|
+
if (first.length === n) return this.#chunks.shift();
|
|
784
|
+
if (first.length > n) {
|
|
785
|
+
this.#chunks[0] = first.subarray(n);
|
|
786
|
+
return Buffer.from(first.subarray(0, n));
|
|
787
|
+
}
|
|
788
|
+
const out = Buffer.allocUnsafe(n);
|
|
789
|
+
let offset = 0;
|
|
790
|
+
while (offset < n) {
|
|
791
|
+
const chunk = this.#chunks[0];
|
|
792
|
+
const take = Math.min(chunk.length, n - offset);
|
|
793
|
+
chunk.copy(out, offset, 0, take);
|
|
794
|
+
offset += take;
|
|
795
|
+
if (take === chunk.length) this.#chunks.shift();
|
|
796
|
+
else this.#chunks[0] = chunk.subarray(take);
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
980
800
|
};
|
|
981
|
-
DevServerWebSocket.CONNECTING = CONNECTING;
|
|
982
|
-
DevServerWebSocket.OPEN = OPEN;
|
|
983
|
-
DevServerWebSocket.CLOSING = CLOSING;
|
|
984
|
-
DevServerWebSocket.CLOSED = CLOSED;
|
|
985
801
|
const encodeFrame = (opcode, payload) => {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
header[0] = 0x80 | opcode;
|
|
1003
|
-
return Buffer.concat([header, payload]);
|
|
802
|
+
const len = payload.length;
|
|
803
|
+
let header;
|
|
804
|
+
if (len < 126) {
|
|
805
|
+
header = Buffer.allocUnsafe(2);
|
|
806
|
+
header[1] = len;
|
|
807
|
+
} else if (len < 65536) {
|
|
808
|
+
header = Buffer.allocUnsafe(4);
|
|
809
|
+
header[1] = 126;
|
|
810
|
+
header.writeUInt16BE(len, 2);
|
|
811
|
+
} else {
|
|
812
|
+
header = Buffer.allocUnsafe(10);
|
|
813
|
+
header[1] = 127;
|
|
814
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
815
|
+
}
|
|
816
|
+
header[0] = 128 | opcode;
|
|
817
|
+
return Buffer.concat([header, payload]);
|
|
1004
818
|
};
|
|
1005
819
|
const toBuffer = (data) => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
return Buffer.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
820
|
+
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
|
|
821
|
+
if (ArrayBuffer.isView(data)) return Buffer.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
822
|
+
return null;
|
|
1012
823
|
};
|
|
1013
824
|
/** Codes a server may send (§7.4): 1000, or the private 3000-4999 range. */
|
|
1014
|
-
const isValidCloseCode = (code) => code ===
|
|
825
|
+
const isValidCloseCode = (code) => code === 1e3 || Number.isInteger(code) && code >= 3e3 && code <= 4999;
|
|
1015
826
|
/** Codes that may legally appear in a close frame we receive — wider than the above. */
|
|
1016
827
|
const isEchoableCloseCode = (code) => {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
return true;
|
|
1021
|
-
return (code >= 1000 &&
|
|
1022
|
-
code <= 1014 &&
|
|
1023
|
-
code !== 1004 &&
|
|
1024
|
-
code !== 1005 &&
|
|
1025
|
-
code !== 1006);
|
|
828
|
+
if (!Number.isInteger(code)) return false;
|
|
829
|
+
if (code >= 3e3 && code <= 4999) return true;
|
|
830
|
+
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
|
|
1026
831
|
};
|
|
1027
832
|
const protocolError = (code, message) => {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
833
|
+
const err = new Error(message);
|
|
834
|
+
err.wsCode = code;
|
|
835
|
+
return err;
|
|
1031
836
|
};
|
|
837
|
+
//#endregion
|
|
838
|
+
export { CloseEventCtor, acceptKey, createUpgradeListener, installWebSocketBridge, isWebSocketHandshake, upgradeWebSocket, validateHandshake, writeUpgradeRejection };
|