@floegence/flowersec-core 2.3.5 → 2.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +33 -42
  2. package/THIRD_PARTY_NOTICES.md +4 -79
  3. package/dist/cli.js +31 -31
  4. package/dist/connectionController.d.ts +1 -0
  5. package/dist/connectionController.js +3 -0
  6. package/dist/connector/adapters/rawQuicCandidate.d.ts +5 -0
  7. package/dist/connector/adapters/rawQuicCandidate.js +71 -0
  8. package/dist/connector/sessionAcceptor.d.ts +22 -2
  9. package/dist/connector/sessionAcceptor.js +51 -21
  10. package/dist/connector/sessionConnector.d.ts +2 -0
  11. package/dist/connector/sessionConnector.js +4 -1
  12. package/dist/interop/proxyServerPeer.d.ts +1 -0
  13. package/dist/interop/proxyServerPeer.js +83 -0
  14. package/dist/interop/serverParityPeer.d.ts +1 -0
  15. package/dist/interop/serverParityPeer.js +608 -0
  16. package/dist/node/acceptor.d.ts +31 -19
  17. package/dist/node/acceptor.js +360 -70
  18. package/dist/node/connectSession.d.ts +2 -1
  19. package/dist/node/connectSession.js +22 -12
  20. package/dist/node/controlplane.d.ts +137 -0
  21. package/dist/node/controlplane.js +439 -0
  22. package/dist/node/index.d.ts +8 -2
  23. package/dist/node/index.js +4 -1
  24. package/dist/node/nativeTransportAddon.d.ts +94 -0
  25. package/dist/node/nativeTransportAddon.js +166 -0
  26. package/dist/node/proxyServer.d.ts +31 -0
  27. package/dist/node/proxyServer.js +572 -0
  28. package/dist/node/rawQuicAdapter.d.ts +9 -0
  29. package/dist/node/rawQuicAdapter.js +107 -0
  30. package/dist/node/rawQuicServer.d.ts +24 -0
  31. package/dist/node/rawQuicServer.js +37 -0
  32. package/dist/node/runtimeCapability.d.ts +8 -1
  33. package/dist/node/runtimeCapability.js +20 -6
  34. package/dist/node/tunnelRuntime.d.ts +42 -0
  35. package/dist/node/tunnelRuntime.js +552 -0
  36. package/dist/node/webSocketServer.d.ts +24 -0
  37. package/dist/node/webSocketServer.js +135 -0
  38. package/dist/public/contract.d.ts +4 -3
  39. package/dist/transport/webSocketAdapter.d.ts +7 -0
  40. package/dist/transport/webSocketAdapter.js +136 -0
  41. package/dist/v2/artifact.js +1 -1
  42. package/dist/v2/capability.js +1 -1
  43. package/dist/v2/carrier.d.ts +4 -1
  44. package/dist/v2/carrier.js +2 -2
  45. package/dist/v2/handshake.js +7 -7
  46. package/dist/v2/protocol.js +5 -5
  47. package/dist/v2/publicSession.js +25 -7
  48. package/dist/v2/retryDisposition.js +4 -4
  49. package/dist/v2/session.d.ts +2 -2
  50. package/dist/v2/session.js +3 -3
  51. package/dist/v2/unreliableMessage.js +4 -4
  52. package/dist/vendor/tr46.js +11 -6
  53. package/dist/ws-client/binaryTransport.d.ts +3 -0
  54. package/dist/ws-client/binaryTransport.js +14 -0
  55. package/package.json +19 -16
  56. package/sbom/cyclonedx.json +52 -2585
  57. package/sbom/spdx.json +56 -1826
  58. package/dist/node/webTransportClient.d.ts +0 -9
  59. package/dist/node/webTransportClient.js +0 -56
  60. package/dist/node/webTransportServer.d.ts +0 -22
  61. package/dist/node/webTransportServer.js +0 -100
@@ -0,0 +1,572 @@
1
+ import { createRequire } from "node:module";
2
+ import { SessionError } from "../public/contract.js";
3
+ import { writeJsonFrame } from "../framing/jsonframe.js";
4
+ import { ProxyByteReader, writeAll } from "../proxy/stream.js";
5
+ import { registerSessionStreamsAtomically, SessionHandlers, } from "./acceptor.js";
6
+ const HTTP_KIND = "flowersec-proxy/http1";
7
+ const WS_KIND = "flowersec-proxy/ws";
8
+ const WIRE_VERSION = 1;
9
+ const DEFAULT_MAX_JSON = 1 << 20;
10
+ const DEFAULT_MAX_CHUNK = 256 * 1024;
11
+ const DEFAULT_MAX_BODY = 64 * 1024 * 1024;
12
+ const DEFAULT_MAX_WS = 1 << 20;
13
+ const DEFAULT_TIMEOUT = 30_000;
14
+ const MAX_TIMEOUT = 300_000;
15
+ const FORBIDDEN_HEADERS = new Set(["authorization", "connection", "host", "keep-alive", "proxy-authorization", "set-cookie", "transfer-encoding", "upgrade"]);
16
+ const REQUEST_HEADERS = new Set(["accept", "accept-language", "content-type", "if-match", "if-none-match", "range"]);
17
+ const RESPONSE_HEADERS = new Set(["accept-ranges", "cache-control", "content-disposition", "content-language", "content-range", "content-type", "etag", "expires", "last-modified", "location"]);
18
+ const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
19
+ export class ProxyServerError extends Error {
20
+ code;
21
+ constructor(code) {
22
+ super(`Flowersec proxy server failed (code=${code})`);
23
+ this.code = code;
24
+ this.name = "ProxyServerError";
25
+ }
26
+ }
27
+ export class ProxyServer {
28
+ #config;
29
+ #active = new Set();
30
+ #permits = new Set();
31
+ #completion;
32
+ #resolveCompletion;
33
+ #closed = false;
34
+ constructor(options) {
35
+ this.#config = compileConfig(options);
36
+ this.#completion = new Promise((resolve) => { this.#resolveCompletion = resolve; });
37
+ }
38
+ register(handlers) {
39
+ if (this.#closed)
40
+ throw new ProxyServerError("closed");
41
+ if (!(handlers instanceof SessionHandlers))
42
+ throw new ProxyServerError("handler_registration");
43
+ try {
44
+ registerSessionStreamsAtomically(handlers, [
45
+ [HTTP_KIND, this.#httpHandler()],
46
+ [WS_KIND, this.#webSocketHandler()],
47
+ ]);
48
+ }
49
+ catch (error) {
50
+ this.#report(error);
51
+ throw new ProxyServerError("handler_registration");
52
+ }
53
+ }
54
+ async close() {
55
+ if (!this.#closed) {
56
+ this.#closed = true;
57
+ for (const controller of this.#active)
58
+ controller.abort(new SessionError("closed"));
59
+ if (this.#active.size === 0)
60
+ this.#resolveCompletion();
61
+ }
62
+ await this.#completion;
63
+ }
64
+ /** @internal */
65
+ get activeCount() { return this.#active.size; }
66
+ #httpHandler() {
67
+ return async (incoming, options) => {
68
+ const release = this.#acquire(incoming.stream);
69
+ if (release === undefined)
70
+ return;
71
+ const controller = this.#track(options.signal);
72
+ try {
73
+ await this.#serveHTTP(incoming.stream, controller.signal);
74
+ }
75
+ catch (error) {
76
+ await incoming.stream.reset().catch(() => undefined);
77
+ this.#report(error);
78
+ }
79
+ finally {
80
+ this.#untrack(controller);
81
+ release();
82
+ }
83
+ };
84
+ }
85
+ #webSocketHandler() {
86
+ return async (incoming, options) => {
87
+ const release = this.#acquire(incoming.stream);
88
+ if (release === undefined)
89
+ return;
90
+ const controller = this.#track(options.signal);
91
+ try {
92
+ await this.#serveWebSocket(incoming.stream, controller.signal);
93
+ }
94
+ catch (error) {
95
+ await incoming.stream.reset().catch(() => undefined);
96
+ this.#report(error);
97
+ }
98
+ finally {
99
+ this.#untrack(controller);
100
+ release();
101
+ }
102
+ };
103
+ }
104
+ #acquire(stream) {
105
+ if (this.#closed || this.#permits.size >= this.#config.maxConcurrent) {
106
+ void stream.reset().catch(() => undefined);
107
+ this.#report(new ProxyServerError(this.#closed ? "closed" : "handler_registration"));
108
+ return undefined;
109
+ }
110
+ const token = Object.freeze({});
111
+ this.#permits.add(token);
112
+ let released = false;
113
+ return () => {
114
+ if (released)
115
+ return;
116
+ released = true;
117
+ this.#permits.delete(token);
118
+ };
119
+ }
120
+ #track(parent) {
121
+ const controller = new AbortController();
122
+ if (this.#closed) {
123
+ controller.abort(new SessionError("closed"));
124
+ return controller;
125
+ }
126
+ if (parent !== undefined) {
127
+ if (parent.aborted)
128
+ controller.abort(parent.reason);
129
+ else
130
+ parent.addEventListener("abort", () => controller.abort(parent.reason), { once: true });
131
+ }
132
+ this.#active.add(controller);
133
+ return controller;
134
+ }
135
+ #untrack(controller) {
136
+ this.#active.delete(controller);
137
+ if (this.#closed && this.#active.size === 0)
138
+ this.#resolveCompletion();
139
+ }
140
+ #report(error) { try {
141
+ this.#config.report?.(error);
142
+ }
143
+ catch { /* reporting is isolated */ } }
144
+ async #serveHTTP(stream, signal) {
145
+ const reader = new ProxyByteReader(stream, { signal });
146
+ let meta;
147
+ try {
148
+ meta = decodeHTTPMeta(await readFrame(reader, this.#config.maxJSON));
149
+ }
150
+ catch {
151
+ await writeHTTPError(stream, "unknown", "invalid_request_meta");
152
+ return;
153
+ }
154
+ const requestID = meta.request_id.trim();
155
+ const path = normalizePath(meta.path);
156
+ if (meta.v !== WIRE_VERSION || requestID === "" || meta.method.trim() === "" || path === undefined) {
157
+ await writeHTTPError(stream, requestID, "invalid_request_meta");
158
+ return;
159
+ }
160
+ const timeout = normalizeTimeout(meta.timeout_ms, this.#config.defaultTimeout, this.#config.maxTimeout);
161
+ if (timeout === undefined) {
162
+ await writeHTTPError(stream, requestID, "invalid_request_meta");
163
+ return;
164
+ }
165
+ const body = await readBody(reader, this.#config.maxChunk, this.#config.maxBody);
166
+ if (body === undefined) {
167
+ await writeHTTPError(stream, requestID, "request_body_invalid");
168
+ return;
169
+ }
170
+ const externalOrigin = validateOrigin(meta.external_origin, this.#config.allowedOrigins);
171
+ if (meta.external_origin !== undefined && externalOrigin === undefined) {
172
+ await writeHTTPError(stream, requestID, "invalid_request_meta");
173
+ return;
174
+ }
175
+ const requestHeaders = filterRequestHeaders(meta.headers, this.#config);
176
+ if (externalOrigin !== undefined) {
177
+ const explicitOrigin = requestHeaders.find((header) => header.name === "origin")?.value;
178
+ if (explicitOrigin !== undefined && explicitOrigin !== externalOrigin) {
179
+ await writeHTTPError(stream, requestID, "invalid_request_meta");
180
+ return;
181
+ }
182
+ const origin = new URL(externalOrigin);
183
+ requestHeaders.push({ name: "x-forwarded-proto", value: origin.protocol.slice(0, -1) });
184
+ }
185
+ const headers = Object.fromEntries(requestHeaders.map((header) => [header.name, header.value]));
186
+ const target = new URL(path, this.#config.upstream);
187
+ const requestInit = {
188
+ method: meta.method.trim().toUpperCase(), headers, redirect: "manual", signal,
189
+ ...(body.byteLength === 0 ? {} : { body: body.slice().buffer, duplex: "half" }),
190
+ };
191
+ const timeoutController = new AbortController();
192
+ const timer = setTimeout(() => timeoutController.abort(), timeout);
193
+ const linked = linkSignals(signal, timeoutController.signal);
194
+ try {
195
+ requestInit.signal = linked.signal;
196
+ const response = await fetch(target, requestInit);
197
+ const contentLength = response.headers.get("content-length");
198
+ if (contentLength !== null && Number(contentLength) > this.#config.maxBody) {
199
+ await writeHTTPError(stream, requestID, "response_body_too_large");
200
+ return;
201
+ }
202
+ await writeFrame(stream, {
203
+ v: WIRE_VERSION, request_id: requestID, ok: true, status: response.status,
204
+ headers: filterHeaders(responseHeaderList(response.headers), RESPONSE_HEADERS, this.#config.responseHeaders, this.#config.blockedResponseHeaders),
205
+ }, signal);
206
+ let total = 0;
207
+ const reader = response.body?.getReader();
208
+ if (reader !== undefined) {
209
+ while (true) {
210
+ const result = await reader.read();
211
+ if (result.done)
212
+ break;
213
+ total += result.value.length;
214
+ if (total > this.#config.maxBody)
215
+ throw new Error("upstream response body exceeds limit");
216
+ for (let offset = 0; offset < result.value.length; offset += this.#config.maxChunk) {
217
+ await writeChunk(stream, result.value.subarray(offset, offset + this.#config.maxChunk), signal);
218
+ }
219
+ }
220
+ }
221
+ await writeChunk(stream, new Uint8Array(), signal);
222
+ }
223
+ catch (error) {
224
+ const code = timeoutController.signal.aborted ? "timeout" : signal.aborted ? "canceled" : "upstream_request_failed";
225
+ await writeHTTPError(stream, requestID, code);
226
+ this.#report(error);
227
+ }
228
+ finally {
229
+ clearTimeout(timer);
230
+ linked.dispose();
231
+ }
232
+ }
233
+ async #serveWebSocket(stream, signal) {
234
+ const reader = new ProxyByteReader(stream, { signal });
235
+ let open;
236
+ try {
237
+ open = decodeWSOpen(await readFrame(reader, this.#config.maxJSON));
238
+ }
239
+ catch {
240
+ await writeWSError(stream, "unknown", "invalid_ws_open_meta");
241
+ return;
242
+ }
243
+ const path = normalizePath(open.path);
244
+ if (open.v !== WIRE_VERSION || open.conn_id.trim() === "" || path === undefined) {
245
+ await writeWSError(stream, open.conn_id, "invalid_ws_open_meta");
246
+ return;
247
+ }
248
+ const upstreamURL = new URL(path, this.#config.upstream);
249
+ upstreamURL.protocol = upstreamURL.protocol === "https:" ? "wss:" : "ws:";
250
+ const require = createRequire(import.meta.url);
251
+ const wsModule = require("ws");
252
+ const WebSocketCtor = wsModule?.WebSocket ?? wsModule;
253
+ const protocols = open.headers.find((header) => header.name.toLowerCase() === "sec-websocket-protocol")?.value;
254
+ const headers = filterHeaders(open.headers, new Set(["sec-websocket-protocol"]), this.#config.websocketHeaders)
255
+ .filter((header) => header.name !== "origin");
256
+ const socket = new WebSocketCtor(upstreamURL.toString(), protocols === undefined ? undefined : protocols.split(",").map((item) => item.trim()), {
257
+ headers: {
258
+ ...Object.fromEntries(headers.map((header) => [header.name, header.value])),
259
+ Origin: this.#config.upstreamOrigin,
260
+ },
261
+ maxPayload: this.#config.maxWS,
262
+ perMessageDeflate: false,
263
+ });
264
+ let opened = false;
265
+ try {
266
+ await onceEvent(socket, "open", signal);
267
+ await writeFrame(stream, { v: WIRE_VERSION, conn_id: open.conn_id.trim(), ok: true, protocol: socket.protocol ?? "" }, signal);
268
+ opened = true;
269
+ await relayWebSocket(socket, stream, reader, this.#config.maxWS, signal);
270
+ }
271
+ catch (error) {
272
+ if (!opened)
273
+ await writeWSError(stream, open.conn_id, signal.aborted ? "canceled" : "upstream_ws_dial_failed");
274
+ this.#report(error);
275
+ }
276
+ finally {
277
+ try {
278
+ socket.close();
279
+ }
280
+ catch { /* cleanup */ }
281
+ await stream.close().catch(() => undefined);
282
+ }
283
+ }
284
+ }
285
+ function compileConfig(options) {
286
+ let upstream;
287
+ let origin;
288
+ try {
289
+ upstream = new URL(options.upstream);
290
+ origin = new URL(options.upstreamOrigin);
291
+ }
292
+ catch {
293
+ throw new ProxyServerError("invalid_options");
294
+ }
295
+ if ((upstream.protocol !== "http:" && upstream.protocol !== "https:") || upstream.pathname !== "/" || upstream.search || upstream.hash || upstream.username || upstream.hostname === "")
296
+ throw new ProxyServerError("invalid_options");
297
+ if ((origin.protocol !== "http:" && origin.protocol !== "https:") || origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.origin !== origin.toString().replace(/\/$/u, ""))
298
+ throw new ProxyServerError("invalid_options");
299
+ const allowedHosts = new Set((options.allowedUpstreamHosts ?? ["127.0.0.1", "::1"]).map((value) => value.trim().toLowerCase()));
300
+ if (!allowedHosts.has(upstream.hostname.toLowerCase()))
301
+ throw new ProxyServerError("invalid_options");
302
+ const allowedOrigins = new Set(options.allowedOrigins ?? [origin.origin]);
303
+ for (const allowed of allowedOrigins) {
304
+ try {
305
+ if (new URL(allowed).origin !== allowed)
306
+ throw new Error();
307
+ }
308
+ catch {
309
+ throw new ProxyServerError("invalid_options");
310
+ }
311
+ }
312
+ const positive = (value, fallback) => value === undefined ? fallback : value;
313
+ const maxConcurrent = positive(options.maxConcurrentStreams, 64);
314
+ const maxJSON = positive(options.maxJsonFrameBytes, DEFAULT_MAX_JSON);
315
+ const maxChunk = positive(options.maxChunkBytes, DEFAULT_MAX_CHUNK);
316
+ const maxBody = positive(options.maxBodyBytes, DEFAULT_MAX_BODY);
317
+ const maxWS = positive(options.maxWebSocketFrameBytes, DEFAULT_MAX_WS);
318
+ const defaultTimeout = positive(options.defaultHTTPRequestTimeoutMs, DEFAULT_TIMEOUT);
319
+ const maxTimeout = positive(options.maxHTTPRequestTimeoutMs, MAX_TIMEOUT);
320
+ if ([maxConcurrent, maxJSON, maxChunk, maxBody, maxWS, defaultTimeout, maxTimeout].some((value) => !Number.isSafeInteger(value) || value < 1) || defaultTimeout > maxTimeout)
321
+ throw new ProxyServerError("invalid_options");
322
+ const normalizeHeaders = (values) => new Set((values ?? []).map((name) => {
323
+ const lower = name.trim().toLowerCase();
324
+ if (!HEADER_NAME.test(lower) || FORBIDDEN_HEADERS.has(lower))
325
+ throw new ProxyServerError("invalid_options");
326
+ return lower;
327
+ }));
328
+ const normalizeCookieValues = (values) => (values ?? []).map((value) => {
329
+ const normalized = value.trim().toLowerCase();
330
+ if (normalized === "" || /[;=\s]/u.test(normalized))
331
+ throw new ProxyServerError("invalid_options");
332
+ return normalized;
333
+ });
334
+ return {
335
+ upstream,
336
+ upstreamOrigin: origin.origin,
337
+ allowedOrigins,
338
+ requestHeaders: normalizeHeaders(options.extraRequestHeaders),
339
+ responseHeaders: normalizeHeaders(options.extraResponseHeaders),
340
+ blockedResponseHeaders: normalizeHeaders(options.blockedResponseHeaders),
341
+ websocketHeaders: normalizeHeaders(options.extraWebSocketHeaders),
342
+ forbiddenCookies: new Set(normalizeCookieValues(options.forbiddenCookieNames)),
343
+ forbiddenCookiePrefixes: normalizeCookieValues(options.forbiddenCookieNamePrefixes),
344
+ maxConcurrent, maxJSON, maxChunk, maxBody, maxWS, defaultTimeout, maxTimeout,
345
+ ...(options.onError === undefined ? {} : { report: options.onError }),
346
+ };
347
+ }
348
+ function filterRequestHeaders(input, config) {
349
+ const headers = filterHeaders(input, REQUEST_HEADERS, config.requestHeaders);
350
+ return headers.flatMap((header) => {
351
+ if (header.name !== "cookie")
352
+ return [header];
353
+ const value = header.value.split(";").flatMap((part) => {
354
+ const item = part.trim();
355
+ const separator = item.indexOf("=");
356
+ if (separator < 1)
357
+ return [];
358
+ const name = item.slice(0, separator).trim().toLowerCase();
359
+ if (config.forbiddenCookies.has(name) || config.forbiddenCookiePrefixes.some((prefix) => name.startsWith(prefix)))
360
+ return [];
361
+ return [item];
362
+ }).join("; ");
363
+ return value === "" ? [] : [{ name: header.name, value }];
364
+ });
365
+ }
366
+ function filterHeaders(input, base, extra, blocked) {
367
+ const result = [];
368
+ for (const header of input) {
369
+ const name = header.name.trim().toLowerCase();
370
+ if ((!base.has(name) && !extra.has(name)) || FORBIDDEN_HEADERS.has(name) || !HEADER_NAME.test(name) || /[\r\n]/u.test(header.value) || blocked?.has(name))
371
+ continue;
372
+ result.push({ name, value: header.value });
373
+ }
374
+ return result;
375
+ }
376
+ function responseHeaderList(headers) {
377
+ const values = [];
378
+ headers.forEach((value, name) => values.push({ name, value }));
379
+ return values;
380
+ }
381
+ async function readFrame(reader, maximum) {
382
+ const frame = await reader.readExactly(4);
383
+ const length = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(0, false);
384
+ if (length > maximum)
385
+ throw new Error("frame too large");
386
+ return JSON.parse(new TextDecoder().decode(await reader.readExactly(length)));
387
+ }
388
+ async function writeFrame(stream, value, signal) {
389
+ await writeJsonFrame({ write: async (data) => await writeAll(stream, data, signal === undefined ? {} : { signal }) }, value);
390
+ }
391
+ async function writeHTTPError(stream, requestID, code) {
392
+ await writeFrame(stream, { v: WIRE_VERSION, request_id: requestID.trim() || "unknown", ok: false, error: { code, message: "proxy operation failed" } }).catch(() => undefined);
393
+ await writeChunks(stream, new Uint8Array(), 1, undefined).catch(() => undefined);
394
+ }
395
+ async function writeWSError(stream, connID, code) {
396
+ await writeFrame(stream, { v: WIRE_VERSION, conn_id: connID.trim() || "unknown", ok: false, error: { code, message: "proxy operation failed" } }).catch(() => undefined);
397
+ }
398
+ async function writeChunks(stream, body, chunkSize, signal) {
399
+ for (let offset = 0; offset < body.length; offset += chunkSize) {
400
+ await writeChunk(stream, body.subarray(offset, Math.min(body.length, offset + chunkSize)), signal);
401
+ }
402
+ await writeChunk(stream, new Uint8Array(), signal);
403
+ }
404
+ async function writeChunk(stream, chunk, signal) {
405
+ const bytes = new Uint8Array(4 + chunk.length);
406
+ new DataView(bytes.buffer).setUint32(0, chunk.length, false);
407
+ bytes.set(chunk, 4);
408
+ await writeAll(stream, bytes, signal === undefined ? {} : { signal });
409
+ }
410
+ async function readBody(reader, maxChunk, maxBody) {
411
+ const chunks = [];
412
+ let total = 0;
413
+ try {
414
+ while (true) {
415
+ const header = await reader.readExactly(4);
416
+ const length = new DataView(header.buffer, header.byteOffset, 4).getUint32(0, false);
417
+ if (length === 0)
418
+ return concat(chunks);
419
+ if (length > maxChunk || total + length > maxBody)
420
+ return undefined;
421
+ const chunk = await reader.readExactly(length);
422
+ chunks.push(chunk);
423
+ total += length;
424
+ }
425
+ }
426
+ catch {
427
+ return undefined;
428
+ }
429
+ }
430
+ function concat(chunks) { const out = new Uint8Array(chunks.reduce((sum, item) => sum + item.length, 0)); let offset = 0; for (const chunk of chunks) {
431
+ out.set(chunk, offset);
432
+ offset += chunk.length;
433
+ } return out; }
434
+ function normalizePath(value) { if (value.trim() !== value || !value.startsWith("/") || value.startsWith("//") || value.includes("://") || /[\u0000-\u0020\u007f]/u.test(value))
435
+ return undefined; try {
436
+ const parsed = new URL(value, "http://flowersec.invalid");
437
+ return parsed.origin === "http://flowersec.invalid" && !parsed.hash ? `${parsed.pathname}${parsed.search}` : undefined;
438
+ }
439
+ catch {
440
+ return undefined;
441
+ } }
442
+ function normalizeTimeout(value, fallback, maximum) { if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
443
+ return undefined; return Math.min(value === undefined || value === 0 ? fallback : value, maximum); }
444
+ function validateOrigin(value, allowed) { if (value === undefined || value === "")
445
+ return undefined; try {
446
+ const origin = new URL(value);
447
+ return origin.pathname === "/" && !origin.search && !origin.hash && origin.origin === value && allowed.has(origin.origin) ? origin.origin : undefined;
448
+ }
449
+ catch {
450
+ return undefined;
451
+ } }
452
+ function linkSignals(parent, timeout) { const controller = new AbortController(); const abort = (event) => controller.abort(event.target.reason); parent.addEventListener("abort", abort, { once: true }); timeout.addEventListener("abort", abort, { once: true }); return { signal: controller.signal, dispose: () => { parent.removeEventListener("abort", abort); timeout.removeEventListener("abort", abort); } }; }
453
+ function decodeHTTPMeta(value) { if (!isRecord(value) || value.v !== WIRE_VERSION || typeof value.request_id !== "string" || typeof value.method !== "string" || typeof value.path !== "string" || !Array.isArray(value.headers) || (value.external_origin !== undefined && typeof value.external_origin !== "string") || (value.timeout_ms !== undefined && typeof value.timeout_ms !== "number"))
454
+ throw new Error("invalid HTTP metadata"); return { v: value.v, request_id: value.request_id, method: value.method, path: value.path, headers: value.headers.filter(isHeader), ...(value.external_origin === undefined ? {} : { external_origin: value.external_origin }), ...(value.timeout_ms === undefined ? {} : { timeout_ms: value.timeout_ms }) }; }
455
+ function decodeWSOpen(value) { if (!isRecord(value) || value.v !== WIRE_VERSION || typeof value.conn_id !== "string" || typeof value.path !== "string" || !Array.isArray(value.headers))
456
+ throw new Error("invalid WS metadata"); return { v: value.v, conn_id: value.conn_id, path: value.path, headers: value.headers.filter(isHeader) }; }
457
+ function isHeader(value) { return isRecord(value) && typeof value.name === "string" && typeof value.value === "string"; }
458
+ function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
459
+ function onceEvent(socket, event, signal) { return new Promise((resolve, reject) => { const onAbort = () => { cleanup(); reject(signal.reason ?? new Error("aborted")); }; const onOpen = () => { cleanup(); resolve(); }; const onError = (error) => { cleanup(); reject(error); }; const cleanup = () => { signal.removeEventListener("abort", onAbort); socket.off?.(event, onOpen); socket.off?.("error", onError); }; socket.once(event, onOpen); socket.once("error", onError); signal.addEventListener("abort", onAbort, { once: true }); }); }
460
+ async function relayWebSocket(socket, stream, reader, maximum, signal) {
461
+ let done = false;
462
+ let writeChain = Promise.resolve();
463
+ const enqueue = (task) => {
464
+ const current = writeChain.then(task);
465
+ writeChain = current.catch(() => undefined);
466
+ return current;
467
+ };
468
+ const upstream = new Promise((resolve, reject) => {
469
+ socket.on("message", (data, isBinary) => {
470
+ if (done)
471
+ return;
472
+ const payload = data instanceof Uint8Array ? data : new Uint8Array(data);
473
+ if (payload.length > maximum) {
474
+ reject(new Error("websocket frame too large"));
475
+ return;
476
+ }
477
+ void enqueue(async () => {
478
+ const frame = new Uint8Array(5 + payload.length);
479
+ frame[0] = isBinary ? 2 : 1;
480
+ new DataView(frame.buffer).setUint32(1, payload.length, false);
481
+ frame.set(payload, 5);
482
+ await writeAll(stream, frame, { signal });
483
+ }).catch(reject);
484
+ });
485
+ socket.once("close", (code, reason) => {
486
+ const closePayload = encodeWebSocketClose(code, reason);
487
+ void enqueue(async () => {
488
+ const frame = new Uint8Array(5 + closePayload.length);
489
+ frame[0] = 8;
490
+ new DataView(frame.buffer).setUint32(1, closePayload.length, false);
491
+ frame.set(closePayload, 5);
492
+ await writeAll(stream, frame, { signal });
493
+ }).then(() => resolve("upstream_close"), reject);
494
+ });
495
+ socket.once("error", reject);
496
+ });
497
+ const downstream = (async () => {
498
+ while (!done) {
499
+ const frame = await reader.readExactly(5);
500
+ const operation = frame[0];
501
+ const length = new DataView(frame.buffer, frame.byteOffset, 5).getUint32(1, false);
502
+ if (length > maximum || ![1, 2, 8, 9, 10].includes(operation))
503
+ throw new Error("invalid websocket frame");
504
+ const payload = await reader.readExactly(length);
505
+ if (operation === 8) {
506
+ const close = decodeWebSocketClose(payload);
507
+ socket.close(close.code, close.reason);
508
+ return "downstream_close";
509
+ }
510
+ if (operation === 9) {
511
+ await sendSocketControl(socket, "ping", payload);
512
+ continue;
513
+ }
514
+ if (operation === 10) {
515
+ await sendSocketControl(socket, "pong", payload);
516
+ continue;
517
+ }
518
+ if (operation === 1) {
519
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(payload);
520
+ await sendSocketFrame(socket, text, undefined);
521
+ }
522
+ else {
523
+ await sendSocketFrame(socket, payload, true);
524
+ }
525
+ }
526
+ return "downstream_close";
527
+ })();
528
+ try {
529
+ const winner = await Promise.race([upstream, downstream]);
530
+ if (winner === "downstream_close") {
531
+ await Promise.race([upstream, new Promise((resolve) => setTimeout(resolve, 1_000))]);
532
+ }
533
+ }
534
+ finally {
535
+ done = true;
536
+ try {
537
+ socket.close();
538
+ }
539
+ catch { /* cleanup */ }
540
+ }
541
+ }
542
+ async function sendSocketFrame(socket, payload, binary) {
543
+ await new Promise((resolve, reject) => {
544
+ const callback = (error) => error == null ? resolve() : reject(error);
545
+ socket.send(payload, { binary: binary === true }, callback);
546
+ });
547
+ }
548
+ async function sendSocketControl(socket, operation, payload) {
549
+ await new Promise((resolve, reject) => {
550
+ socket[operation](payload, (error) => error == null ? resolve() : reject(error));
551
+ });
552
+ }
553
+ function encodeWebSocketClose(code, reason) {
554
+ if (!Number.isInteger(code) || code < 0 || code > 65_535 || code === 1004 || code === 1005 || code === 1006)
555
+ return new Uint8Array();
556
+ const payload = new Uint8Array(2 + (reason?.length ?? 0));
557
+ new DataView(payload.buffer).setUint16(0, code, false);
558
+ if (reason !== undefined)
559
+ payload.set(reason, 2);
560
+ return payload;
561
+ }
562
+ function decodeWebSocketClose(payload) {
563
+ if (payload.length === 0)
564
+ return {};
565
+ if (payload.length < 2)
566
+ throw new Error("invalid websocket close frame");
567
+ const code = new DataView(payload.buffer, payload.byteOffset, 2).getUint16(0, false);
568
+ if (code === 1004 || code === 1005 || code === 1006)
569
+ throw new Error("invalid websocket close code");
570
+ const reason = new TextDecoder("utf-8", { fatal: true }).decode(payload.subarray(2));
571
+ return { code, reason };
572
+ }
@@ -0,0 +1,9 @@
1
+ import type { ArtifactV2, CanonicalArtifactCandidateV2 } from "../v2/artifact.js";
2
+ import type { NativeCarrierSessionV2 } from "../v2/carrier.js";
3
+ import type { NativeRawQuicDriver } from "./nativeTransportAddon.js";
4
+ export type RawQuicTLSOptions = Readonly<{
5
+ ca: string | Uint8Array | readonly (string | Uint8Array)[];
6
+ }>;
7
+ export declare function createNodeRawQuicClientV2(driver: NativeRawQuicDriver, candidate: CanonicalArtifactCandidateV2, artifact: ArtifactV2, tls: RawQuicTLSOptions, signal: AbortSignal, handshakeTimeoutMs?: number): Promise<NativeCarrierSessionV2>;
8
+ export declare function normalizeCertificateChain(input: string | Uint8Array | readonly (string | Uint8Array)[]): readonly Uint8Array[];
9
+ export declare function normalizePrivateKey(input: string | Uint8Array): Uint8Array;