@floegence/flowersec-core 0.21.1 → 0.22.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.
@@ -0,0 +1,9 @@
1
+ import type { WebSocketLike } from "../ws-client/binaryTransport.js";
2
+ import { type DirectAcceptOptions, type DirectCredentialResolver, type DirectHandshakeCredential, type Session, type Suite, type TunnelEndpointOptions } from "./index.js";
3
+ export declare function adaptNodeWebSocket(websocket: unknown): WebSocketLike;
4
+ export declare function acceptDirectNode(websocket: unknown, handshake: Readonly<{
5
+ channelId: string;
6
+ suite: Suite;
7
+ }> & DirectHandshakeCredential, options?: DirectAcceptOptions): Promise<Session>;
8
+ export declare function acceptDirectResolvedNode(websocket: unknown, resolver: DirectCredentialResolver, options?: DirectAcceptOptions): Promise<Session>;
9
+ export declare function connectTunnelEndpointNode(grant: unknown, options: Omit<TunnelEndpointOptions, "wsFactory">): Promise<Session>;
@@ -0,0 +1,51 @@
1
+ import { createNodeWsFactory } from "../node/wsFactory.js";
2
+ import { acceptDirect, acceptDirectResolved, connectTunnel, } from "./index.js";
3
+ export function adaptNodeWebSocket(websocket) {
4
+ const raw = websocket;
5
+ if (raw == null || typeof raw.send !== "function" || typeof raw.close !== "function") {
6
+ throw new TypeError("a Node WebSocket is required");
7
+ }
8
+ if (typeof raw.addEventListener === "function" && typeof raw.removeEventListener === "function") {
9
+ return raw;
10
+ }
11
+ const listeners = new Map();
12
+ return {
13
+ get binaryType() { return String(raw.binaryType ?? "nodebuffer"); },
14
+ set binaryType(value) { raw.binaryType = value; },
15
+ get readyState() { return Number(raw.readyState); },
16
+ get bufferedAmount() { return Number(raw.bufferedAmount ?? 0); },
17
+ send(data) { raw.send(data); },
18
+ close(code, reason) { raw.close(code, reason); },
19
+ addEventListener(type, listener) {
20
+ const wrapped = (...args) => {
21
+ if (type === "message")
22
+ listener({ data: args[0] });
23
+ else if (type === "close")
24
+ listener({ code: args[0], reason: args[1]?.toString?.() ?? "" });
25
+ else
26
+ listener(args[0]);
27
+ };
28
+ const byListener = listeners.get(type) ?? new Map();
29
+ byListener.set(listener, wrapped);
30
+ listeners.set(type, byListener);
31
+ raw.on(type, wrapped);
32
+ },
33
+ removeEventListener(type, listener) {
34
+ const byListener = listeners.get(type);
35
+ const wrapped = byListener?.get(listener);
36
+ if (wrapped == null)
37
+ return;
38
+ byListener.delete(listener);
39
+ raw.off(type, wrapped);
40
+ },
41
+ };
42
+ }
43
+ export function acceptDirectNode(websocket, handshake, options = {}) {
44
+ return acceptDirect(adaptNodeWebSocket(websocket), handshake, options);
45
+ }
46
+ export function acceptDirectResolvedNode(websocket, resolver, options = {}) {
47
+ return acceptDirectResolved(adaptNodeWebSocket(websocket), resolver, options);
48
+ }
49
+ export function connectTunnelEndpointNode(grant, options) {
50
+ return connectTunnel(grant, { ...options, wsFactory: createNodeWsFactory() });
51
+ }
package/dist/facade.d.ts CHANGED
@@ -17,8 +17,8 @@ export type { WebSocketLimits } from "./ws-client/binaryTransport.js";
17
17
  export type { YamuxLimits } from "./yamux/session.js";
18
18
  export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
19
19
  export { FlowersecError } from "./utils/errors.js";
20
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
21
- export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
20
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
21
+ export type { NetworkPlaintextPolicyOptions, TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
22
22
  export type { TunnelConnectOptions } from "./tunnel-client/connect.js";
23
23
  export type { DirectConnectOptions } from "./direct-client/connect.js";
24
24
  export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
package/dist/facade.js CHANGED
@@ -6,7 +6,7 @@ export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
6
6
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
7
7
  export { assertConnectArtifact } from "./connect/artifact.js";
8
8
  export { FlowersecError } from "./utils/errors.js";
9
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
9
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
10
10
  export async function connectTunnel(grant, opts) {
11
11
  return await connectTunnelInternal(grant, opts);
12
12
  }
@@ -1,5 +1,6 @@
1
1
  import { readU32be, u32be } from "../utils/bin.js";
2
- export const DEFAULT_MAX_JSON_FRAME_BYTES = 1 << 20;
2
+ import { SDK_DEFAULTS } from "../defaults.js";
3
+ export const DEFAULT_MAX_JSON_FRAME_BYTES = SDK_DEFAULTS.rpc.maxJsonFrameBytes;
3
4
  const te = new TextEncoder();
4
5
  const td = new TextDecoder();
5
6
  // JsonFramingError marks malformed or oversized frames.
@@ -1,8 +1,11 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
3
- export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "../client-connect/transportSecurity.js";
2
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
3
+ export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
4
4
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
5
5
  export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
6
6
  export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
7
+ export * from "../endpoint/index.js";
8
+ export * from "../endpoint/node.js";
9
+ export * from "../proxy/server.js";
7
10
  export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
8
11
  export { assertConnectArtifact } from "../connect/artifact.js";
@@ -1,5 +1,8 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
2
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
3
3
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
4
4
  export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
5
+ export * from "../endpoint/index.js";
6
+ export * from "../endpoint/node.js";
7
+ export * from "../proxy/server.js";
5
8
  export { assertConnectArtifact } from "../connect/artifact.js";
@@ -1,6 +1,7 @@
1
+ import { SDK_DEFAULTS } from "../defaults.js";
1
2
  export const PROXY_PROTOCOL_VERSION = 1;
2
3
  export const PROXY_KIND_HTTP1 = "flowersec-proxy/http1";
3
4
  export const PROXY_KIND_WS = "flowersec-proxy/ws";
4
- export const DEFAULT_MAX_CHUNK_BYTES = 256 * 1024;
5
- export const DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
6
- export const DEFAULT_MAX_WS_FRAME_BYTES = 1024 * 1024;
5
+ export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
6
+ export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
7
+ export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
@@ -7,6 +7,7 @@ const DEFAULT_REQUEST_HEADER_ALLOWLIST = new Set([
7
7
  "if-modified-since",
8
8
  "if-none-match",
9
9
  "if-unmodified-since",
10
+ "origin",
10
11
  "pragma",
11
12
  "range",
12
13
  "x-requested-with"
@@ -0,0 +1,27 @@
1
+ import type { Session } from "../endpoint/index.js";
2
+ import { RpcRouter, type RpcServerOptions } from "../rpc/server.js";
3
+ import type { YamuxStream } from "../yamux/stream.js";
4
+ import { PROXY_KIND_HTTP1, PROXY_KIND_WS } from "./constants.js";
5
+ export type ProxyServerOptions = Readonly<{
6
+ upstream: string;
7
+ upstreamOrigin?: string;
8
+ allowedUpstreamHosts?: readonly string[];
9
+ maxJsonFrameBytes?: number;
10
+ maxChunkBytes?: number;
11
+ maxBodyBytes?: number;
12
+ maxWsFrameBytes?: number;
13
+ defaultTimeoutMs?: number;
14
+ maxTimeoutMs?: number;
15
+ maxConcurrentStreams?: number;
16
+ extraRequestHeaders?: readonly string[];
17
+ extraResponseHeaders?: readonly string[];
18
+ blockedResponseHeaders?: readonly string[];
19
+ extraWsHeaders?: readonly string[];
20
+ forbiddenCookieNames?: readonly string[];
21
+ forbiddenCookieNamePrefixes?: readonly string[];
22
+ fetch?: typeof fetch;
23
+ rpcRouter?: RpcRouter;
24
+ rpcServerOptions?: RpcServerOptions;
25
+ }>;
26
+ export declare function serveProxySession(session: Session, options: ProxyServerOptions, signal?: AbortSignal): Promise<void>;
27
+ export declare function serveProxyStream(kind: typeof PROXY_KIND_HTTP1 | typeof PROXY_KIND_WS, stream: YamuxStream, options: ProxyServerOptions, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,480 @@
1
+ import { createRequire } from "node:module";
2
+ import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
3
+ import { RpcRouter, RpcServer } from "../rpc/server.js";
4
+ import { createByteReader } from "../streamio/index.js";
5
+ import { readU32be, u32be } from "../utils/bin.js";
6
+ import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION, } from "./constants.js";
7
+ import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders, isSafeHeaderValue, isValidHeaderName, normalizeHeaderName } from "./headerPolicy.js";
8
+ export async function serveProxySession(session, options, signal) {
9
+ const compiled = compileOptions(options);
10
+ const active = new Set();
11
+ try {
12
+ while (!signal?.aborted) {
13
+ const accepted = await session.acceptStream(signal === undefined ? {} : { signal });
14
+ if (accepted.kind === "rpc") {
15
+ if (active.size >= compiled.maxConcurrentStreams) {
16
+ await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
17
+ continue;
18
+ }
19
+ const task = serveRPCStream(accepted.stream, options.rpcRouter ?? new RpcRouter(), options.rpcServerOptions, signal)
20
+ .catch(() => { })
21
+ .finally(() => active.delete(task));
22
+ active.add(task);
23
+ continue;
24
+ }
25
+ if (accepted.kind !== PROXY_KIND_HTTP1 && accepted.kind !== PROXY_KIND_WS) {
26
+ await accepted.stream.reset(new Error(`unsupported proxy stream kind ${accepted.kind}`));
27
+ continue;
28
+ }
29
+ if (active.size >= compiled.maxConcurrentStreams) {
30
+ await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
31
+ continue;
32
+ }
33
+ const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
34
+ .catch(() => { })
35
+ .finally(() => active.delete(task));
36
+ active.add(task);
37
+ }
38
+ }
39
+ finally {
40
+ await Promise.allSettled(active);
41
+ }
42
+ }
43
+ async function serveRPCStream(stream, router, options, signal) {
44
+ const reader = createByteReader(stream, signal === undefined ? {} : { signal });
45
+ const server = new RpcServer({
46
+ readExactly: (length) => reader.readExactly(length),
47
+ write: (bytes) => stream.write(bytes),
48
+ close: (error) => { void stream.reset(asError(error)); },
49
+ }, options, router);
50
+ await server.serve(signal);
51
+ }
52
+ export function serveProxyStream(kind, stream, options, signal) {
53
+ return serveProxyStreamCompiled(kind, stream, compileOptions(options), signal);
54
+ }
55
+ async function serveProxyStreamCompiled(kind, stream, options, signal) {
56
+ try {
57
+ if (kind === PROXY_KIND_HTTP1)
58
+ await serveHTTP(stream, options, signal);
59
+ else
60
+ await serveWebSocket(stream, options, signal);
61
+ }
62
+ finally {
63
+ try {
64
+ await stream.close();
65
+ }
66
+ catch { /* The peer may already have reset the stream. */ }
67
+ }
68
+ }
69
+ async function serveHTTP(stream, options, signal) {
70
+ const reader = createByteReader(stream, signal === undefined ? {} : { signal });
71
+ let requestId = "unknown";
72
+ try {
73
+ const meta = assertHTTPRequestMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
74
+ requestId = meta.request_id;
75
+ const path = parsePath(meta.path);
76
+ const body = await readBody(reader, options.maxChunkBytes, options.maxBodyBytes);
77
+ const headers = requestHeaders(meta.headers, options);
78
+ applyExternalOrigin(headers, meta.external_origin);
79
+ const target = new URL(options.upstream);
80
+ target.pathname = path.pathname;
81
+ target.search = path.search;
82
+ target.hash = "";
83
+ const timeoutMs = resolveTimeout(meta.timeout_ms, options);
84
+ const controller = new AbortController();
85
+ const onAbort = () => controller.abort(signal?.reason);
86
+ signal?.addEventListener("abort", onAbort, { once: true });
87
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new Error("upstream request timeout")), timeoutMs) : undefined;
88
+ try {
89
+ const method = meta.method.trim().toUpperCase();
90
+ const response = await options.fetch(target, {
91
+ method,
92
+ headers,
93
+ redirect: "manual",
94
+ signal: controller.signal,
95
+ ...((method === "GET" || method === "HEAD") ? {} : { body: body.slice().buffer }),
96
+ });
97
+ const responseHeaders = collectResponseHeaders(response.headers, options);
98
+ const responseMeta = {
99
+ v: PROXY_PROTOCOL_VERSION,
100
+ request_id: requestId,
101
+ ok: true,
102
+ status: response.status,
103
+ headers: responseHeaders,
104
+ };
105
+ await writeJsonFrame(stream, responseMeta);
106
+ if (response.body == null) {
107
+ await stream.write(u32be(0));
108
+ return;
109
+ }
110
+ const bodyReader = response.body.getReader();
111
+ let total = 0;
112
+ while (true) {
113
+ const next = await bodyReader.read();
114
+ if (next.done)
115
+ break;
116
+ const bytes = next.value;
117
+ total += bytes.length;
118
+ if (total > options.maxBodyBytes)
119
+ throw new ProxyServerError("response_body_too_large", "response body too large");
120
+ for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
121
+ const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
122
+ await stream.write(u32be(chunk.length));
123
+ await stream.write(chunk);
124
+ }
125
+ }
126
+ await stream.write(u32be(0));
127
+ }
128
+ finally {
129
+ if (timer != null)
130
+ clearTimeout(timer);
131
+ signal?.removeEventListener("abort", onAbort);
132
+ }
133
+ }
134
+ catch (error) {
135
+ await writeHTTPError(stream, requestId, classifyHTTPError(error), error);
136
+ }
137
+ }
138
+ async function serveWebSocket(stream, options, signal) {
139
+ const reader = createByteReader(stream, signal === undefined ? {} : { signal });
140
+ let connId = "unknown";
141
+ let raw;
142
+ try {
143
+ const meta = assertWSOpenMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
144
+ connId = meta.conn_id;
145
+ const path = parsePath(meta.path);
146
+ const target = new URL(options.upstream);
147
+ target.protocol = target.protocol === "https:" ? "wss:" : "ws:";
148
+ target.pathname = path.pathname;
149
+ target.search = path.search;
150
+ target.hash = "";
151
+ const filtered = serverWSHeaders(meta.headers, options);
152
+ const protocolsHeader = filtered.find((header) => header.name === "sec-websocket-protocol")?.value ?? "";
153
+ const protocols = protocolsHeader.split(",").map((value) => value.trim()).filter((value) => value !== "");
154
+ const headers = { Origin: options.upstreamOrigin };
155
+ for (const header of filtered) {
156
+ if (header.name === "sec-websocket-protocol")
157
+ continue;
158
+ headers[header.name] = headers[header.name] == null ? header.value : `${headers[header.name]}, ${header.value}`;
159
+ }
160
+ const require = createRequire(import.meta.url);
161
+ const module = require("ws");
162
+ const WebSocketCtor = module.WebSocket ?? module;
163
+ raw = new WebSocketCtor(target.toString(), protocols, {
164
+ headers,
165
+ maxPayload: options.maxWsFrameBytes,
166
+ perMessageDeflate: false,
167
+ handshakeTimeout: Math.min(options.defaultTimeoutMs, options.maxTimeoutMs),
168
+ });
169
+ await waitForUpstreamOpen(raw, signal);
170
+ const response = {
171
+ v: PROXY_PROTOCOL_VERSION,
172
+ conn_id: connId,
173
+ ok: true,
174
+ protocol: String(raw.protocol ?? ""),
175
+ };
176
+ await writeJsonFrame(stream, response);
177
+ let writeChain = Promise.resolve();
178
+ let terminalResolve;
179
+ const terminal = new Promise((resolve) => { terminalResolve = resolve; });
180
+ const writeFrame = (op, payload) => {
181
+ const write = writeChain.then(() => writeWSFrame(stream, op, payload, options.maxWsFrameBytes));
182
+ writeChain = write.catch(() => { });
183
+ void write.catch((error) => terminalResolve(asError(error)));
184
+ };
185
+ raw.on("message", (data, isBinary) => writeFrame(isBinary ? 2 : 1, toBytes(data)));
186
+ raw.on("ping", (data) => writeFrame(9, toBytes(data)));
187
+ raw.on("pong", (data) => writeFrame(10, toBytes(data)));
188
+ raw.on("close", (code, reason) => {
189
+ const reasonBytes = toBytes(reason);
190
+ const payload = new Uint8Array(2 + reasonBytes.length);
191
+ payload[0] = (code >>> 8) & 0xff;
192
+ payload[1] = code & 0xff;
193
+ payload.set(reasonBytes, 2);
194
+ writeFrame(8, payload);
195
+ terminalResolve();
196
+ });
197
+ raw.on("error", (error) => terminalResolve(error));
198
+ const inbound = (async () => {
199
+ while (true) {
200
+ const frame = await readWSFrame(reader, options.maxWsFrameBytes);
201
+ switch (frame.op) {
202
+ case 1:
203
+ raw.send(frame.payload, { binary: false });
204
+ break;
205
+ case 2:
206
+ raw.send(frame.payload, { binary: true });
207
+ break;
208
+ case 8:
209
+ raw.close(frame.payload.length >= 2 ? (frame.payload[0] << 8) | frame.payload[1] : 1000, new TextDecoder().decode(frame.payload.subarray(2)));
210
+ return;
211
+ case 9:
212
+ raw.ping(frame.payload);
213
+ break;
214
+ case 10:
215
+ raw.pong(frame.payload);
216
+ break;
217
+ default: throw new Error("invalid websocket frame operation");
218
+ }
219
+ }
220
+ })();
221
+ const result = await Promise.race([inbound.then(() => undefined), terminal]);
222
+ if (result != null)
223
+ throw result;
224
+ await writeChain;
225
+ }
226
+ catch (error) {
227
+ if (raw == null || raw.readyState !== 1)
228
+ await writeWSOpenError(stream, connId, classifyWSError(error), error);
229
+ }
230
+ finally {
231
+ try {
232
+ raw?.close();
233
+ }
234
+ catch { /* Best effort. */ }
235
+ }
236
+ }
237
+ function compileOptions(input) {
238
+ const upstream = new URL(input.upstream);
239
+ if ((upstream.protocol !== "http:" && upstream.protocol !== "https:") || upstream.username !== "" || upstream.password !== "") {
240
+ throw new Error("upstream must be an http(s) URL without credentials");
241
+ }
242
+ const allowedHosts = new Set((input.allowedUpstreamHosts?.length ? input.allowedUpstreamHosts : ["127.0.0.1"]).map((host) => host.trim().toLowerCase()));
243
+ if (!allowedHosts.has(upstream.hostname.toLowerCase()))
244
+ throw new Error("upstream host is not allowed");
245
+ const upstreamOrigin = normalizeOrigin(input.upstreamOrigin ?? upstream.origin);
246
+ const maxJsonFrameBytes = positive(input.maxJsonFrameBytes, DEFAULT_MAX_JSON_FRAME_BYTES, "maxJsonFrameBytes");
247
+ const maxChunkBytes = positive(input.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES, "maxChunkBytes");
248
+ const maxBodyBytes = positive(input.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes");
249
+ const maxWsFrameBytes = positive(input.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES, "maxWsFrameBytes");
250
+ const defaultTimeoutMs = nonNegative(input.defaultTimeoutMs, 30_000, "defaultTimeoutMs");
251
+ const maxTimeoutMs = nonNegative(input.maxTimeoutMs, 300_000, "maxTimeoutMs");
252
+ if (maxTimeoutMs > 0 && defaultTimeoutMs > maxTimeoutMs)
253
+ throw new Error("defaultTimeoutMs exceeds maxTimeoutMs");
254
+ return {
255
+ upstream,
256
+ upstreamOrigin,
257
+ maxJsonFrameBytes,
258
+ maxChunkBytes,
259
+ maxBodyBytes,
260
+ maxWsFrameBytes,
261
+ defaultTimeoutMs,
262
+ maxTimeoutMs,
263
+ maxConcurrentStreams: positive(input.maxConcurrentStreams, 64, "maxConcurrentStreams"),
264
+ extraRequestHeaders: input.extraRequestHeaders ?? [],
265
+ extraResponseHeaders: input.extraResponseHeaders ?? [],
266
+ blockedResponseHeaders: normalizeNames(input.blockedResponseHeaders),
267
+ extraWsHeaders: input.extraWsHeaders ?? [],
268
+ forbiddenCookieNames: normalizeNames(input.forbiddenCookieNames),
269
+ forbiddenCookieNamePrefixes: [...normalizeNames(input.forbiddenCookieNamePrefixes)],
270
+ fetch: input.fetch ?? globalThis.fetch.bind(globalThis),
271
+ };
272
+ }
273
+ function assertHTTPRequestMeta(input) {
274
+ if (typeof input !== "object" || input == null || Array.isArray(input))
275
+ throw new ProxyServerError("invalid_request_meta", "invalid HTTP request meta");
276
+ const meta = input;
277
+ if (meta.v !== PROXY_PROTOCOL_VERSION || typeof meta.request_id !== "string" || meta.request_id.trim() === "")
278
+ throw new ProxyServerError("invalid_request_meta", "invalid request ID");
279
+ if (typeof meta.method !== "string" || meta.method.trim() === "" || typeof meta.path !== "string" || !Array.isArray(meta.headers))
280
+ throw new ProxyServerError("invalid_request_meta", "invalid HTTP request meta");
281
+ if (meta.timeout_ms !== undefined && (!Number.isSafeInteger(meta.timeout_ms) || meta.timeout_ms < 0))
282
+ throw new ProxyServerError("invalid_request_meta", "invalid timeout_ms");
283
+ return { ...meta, request_id: meta.request_id.trim(), method: meta.method.trim() };
284
+ }
285
+ function assertWSOpenMeta(input) {
286
+ if (typeof input !== "object" || input == null || Array.isArray(input))
287
+ throw new ProxyServerError("invalid_ws_open_meta", "invalid websocket open meta");
288
+ const meta = input;
289
+ if (meta.v !== PROXY_PROTOCOL_VERSION || typeof meta.conn_id !== "string" || meta.conn_id.trim() === "" || typeof meta.path !== "string" || !Array.isArray(meta.headers)) {
290
+ throw new ProxyServerError("invalid_ws_open_meta", "invalid websocket open meta");
291
+ }
292
+ return { ...meta, conn_id: meta.conn_id.trim() };
293
+ }
294
+ function parsePath(input) {
295
+ const path = input.trim();
296
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("://") || /[\r\n\t ]/.test(path))
297
+ throw new ProxyServerError("invalid_request_meta", "invalid path");
298
+ return new URL(path, "http://flowersec.invalid");
299
+ }
300
+ function requestHeaders(input, options) {
301
+ const filtered = filterRequestHeaders(input, { extraAllowed: options.extraRequestHeaders });
302
+ const headers = new Headers();
303
+ for (const header of filtered)
304
+ headers.append(header.name, header.value);
305
+ for (const header of input) {
306
+ if (normalizeHeaderName(header.name) !== "cookie" || !isSafeHeaderValue(header.value))
307
+ continue;
308
+ const cookie = filterCookie(header.value, options);
309
+ if (cookie !== "")
310
+ headers.append("cookie", cookie);
311
+ }
312
+ return headers;
313
+ }
314
+ function serverWSHeaders(input, options) {
315
+ const filtered = filterWsOpenHeaders(input, { extraAllowed: options.extraWsHeaders });
316
+ return filtered.flatMap((header) => header.name === "cookie" ? [{ ...header, value: filterCookie(header.value, options) }] : [header]).filter((header) => header.value !== "");
317
+ }
318
+ function filterCookie(input, options) {
319
+ return input.split(";").map((part) => part.trim()).filter((part) => {
320
+ const index = part.indexOf("=");
321
+ if (index <= 0)
322
+ return false;
323
+ const name = part.slice(0, index).trim().toLowerCase();
324
+ if (options.forbiddenCookieNames.has(name))
325
+ return false;
326
+ return !options.forbiddenCookieNamePrefixes.some((prefix) => name.startsWith(prefix));
327
+ }).join("; ");
328
+ }
329
+ function collectResponseHeaders(input, options) {
330
+ const raw = [];
331
+ const setCookies = input.getSetCookie?.() ?? [];
332
+ input.forEach((value, name) => {
333
+ if (name.toLowerCase() !== "set-cookie")
334
+ raw.push({ name, value });
335
+ });
336
+ for (const value of setCookies)
337
+ raw.push({ name: "set-cookie", value });
338
+ return filterResponseHeaders(raw, { extraAllowed: options.extraResponseHeaders }).passthrough
339
+ .concat(raw.filter((header) => normalizeHeaderName(header.name) === "set-cookie"))
340
+ .filter((header) => !options.blockedResponseHeaders.has(normalizeHeaderName(header.name)));
341
+ }
342
+ function applyExternalOrigin(headers, input) {
343
+ if (input == null || input.trim() === "")
344
+ return;
345
+ const external = normalizeOrigin(input);
346
+ const current = headers.get("origin");
347
+ if (current != null && normalizeOrigin(current) !== external)
348
+ throw new ProxyServerError("invalid_request_meta", "external_origin conflicts with origin header");
349
+ headers.set("x-forwarded-proto", new URL(external).protocol.slice(0, -1));
350
+ headers.set("host", new URL(external).host);
351
+ }
352
+ async function readBody(reader, maxChunkBytes, maxBodyBytes) {
353
+ const chunks = [];
354
+ let total = 0;
355
+ while (true) {
356
+ const length = readU32be(await reader.readExactly(4), 0);
357
+ if (length === 0)
358
+ break;
359
+ if (length > maxChunkBytes)
360
+ throw new ProxyServerError("request_body_too_large", "request chunk too large");
361
+ total += length;
362
+ if (total > maxBodyBytes)
363
+ throw new ProxyServerError("request_body_too_large", "request body too large");
364
+ chunks.push(await reader.readExactly(length));
365
+ }
366
+ const body = new Uint8Array(total);
367
+ let offset = 0;
368
+ for (const chunk of chunks) {
369
+ body.set(chunk, offset);
370
+ offset += chunk.length;
371
+ }
372
+ return body;
373
+ }
374
+ async function writeHTTPError(stream, requestId, code, error) {
375
+ const meta = { v: PROXY_PROTOCOL_VERSION, request_id: requestId.trim() || "unknown", ok: false, error: { code, message: asError(error).message } };
376
+ try {
377
+ await writeJsonFrame(stream, meta);
378
+ await stream.write(u32be(0));
379
+ }
380
+ catch { /* The peer may be gone. */ }
381
+ }
382
+ async function readWSFrame(reader, maxBytes) {
383
+ const header = await reader.readExactly(5);
384
+ const length = readU32be(header, 1);
385
+ if (length > maxBytes)
386
+ throw new Error("websocket frame too large");
387
+ return { op: header[0], payload: await reader.readExactly(length) };
388
+ }
389
+ async function writeWSFrame(stream, op, payload, maxBytes) {
390
+ if (payload.length > maxBytes)
391
+ throw new Error("websocket frame too large");
392
+ const header = new Uint8Array(5);
393
+ header[0] = op;
394
+ header.set(u32be(payload.length), 1);
395
+ await stream.write(header);
396
+ if (payload.length > 0)
397
+ await stream.write(payload);
398
+ }
399
+ async function writeWSOpenError(stream, connId, code, error) {
400
+ const response = { v: PROXY_PROTOCOL_VERSION, conn_id: connId.trim() || "unknown", ok: false, error: { code, message: asError(error).message } };
401
+ try {
402
+ await writeJsonFrame(stream, response);
403
+ }
404
+ catch { /* The peer may be gone. */ }
405
+ }
406
+ function waitForUpstreamOpen(websocket, signal) {
407
+ return new Promise((resolve, reject) => {
408
+ const cleanup = () => { websocket.off("open", onOpen); websocket.off("error", onError); signal?.removeEventListener("abort", onAbort); };
409
+ const onOpen = () => { cleanup(); resolve(); };
410
+ const onError = (error) => { cleanup(); reject(error); };
411
+ const onAbort = () => { cleanup(); reject(signal?.reason ?? new Error("aborted")); };
412
+ websocket.once("open", onOpen);
413
+ websocket.once("error", onError);
414
+ signal?.addEventListener("abort", onAbort, { once: true });
415
+ });
416
+ }
417
+ function resolveTimeout(input, options) {
418
+ const timeout = input == null || input === 0 ? options.defaultTimeoutMs : input;
419
+ return options.maxTimeoutMs > 0 ? Math.min(timeout, options.maxTimeoutMs) : timeout;
420
+ }
421
+ function normalizeOrigin(input) {
422
+ const url = new URL(input);
423
+ if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username !== "" || url.password !== "" || (url.pathname !== "" && url.pathname !== "/") || url.search !== "" || url.hash !== "")
424
+ throw new Error("invalid origin");
425
+ return url.origin;
426
+ }
427
+ function normalizeNames(input) {
428
+ const result = new Set();
429
+ for (const item of input ?? []) {
430
+ const name = normalizeHeaderName(item);
431
+ if (name === "" || !isValidHeaderName(name))
432
+ throw new Error("invalid policy name");
433
+ result.add(name);
434
+ }
435
+ return result;
436
+ }
437
+ function positive(input, fallback, name) {
438
+ const value = input ?? fallback;
439
+ if (!Number.isSafeInteger(value) || value <= 0)
440
+ throw new Error(`${name} must be a positive integer`);
441
+ return value;
442
+ }
443
+ function nonNegative(input, fallback, name) {
444
+ const value = input ?? fallback;
445
+ if (!Number.isSafeInteger(value) || value < 0)
446
+ throw new Error(`${name} must be a non-negative integer`);
447
+ return value;
448
+ }
449
+ function classifyHTTPError(error) {
450
+ if (error instanceof ProxyServerError)
451
+ return error.code;
452
+ if (asError(error).name === "AbortError")
453
+ return "timeout";
454
+ return "upstream_request_failed";
455
+ }
456
+ function classifyWSError(error) {
457
+ if (error instanceof ProxyServerError)
458
+ return error.code;
459
+ return "upstream_ws_dial_failed";
460
+ }
461
+ function toBytes(input) {
462
+ if (input instanceof Uint8Array)
463
+ return input;
464
+ if (input instanceof ArrayBuffer)
465
+ return new Uint8Array(input);
466
+ if (ArrayBuffer.isView(input))
467
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
468
+ return new TextEncoder().encode(String(input ?? ""));
469
+ }
470
+ function asError(error) {
471
+ return error instanceof Error ? error : new Error(String(error));
472
+ }
473
+ class ProxyServerError extends Error {
474
+ code;
475
+ constructor(code, message) {
476
+ super(message);
477
+ this.code = code;
478
+ this.name = "ProxyServerError";
479
+ }
480
+ }