@floegence/flowersec-core 0.10.0 → 0.10.2
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/dist/direct-client/connect.d.ts +6 -1
- package/dist/node/connect.js +1 -13
- package/dist/node/wsDefaults.d.ts +4 -0
- package/dist/node/wsDefaults.js +13 -0
- package/dist/node/wsFactory.js +5 -3
- package/dist/proxy/constants.d.ts +0 -2
- package/dist/proxy/constants.js +0 -2
- package/dist/proxy/serviceWorker.d.ts +2 -0
- package/dist/proxy/serviceWorker.js +217 -97
- package/dist/proxy/wsPatch.js +9 -4
- package/dist/tunnel-client/connect.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type ConnectOptionsBase } from "../client-connect/connectCore.js";
|
|
2
2
|
import type { ClientInternal } from "../client.js";
|
|
3
|
-
export type DirectConnectOptions = ConnectOptionsBase
|
|
3
|
+
export type DirectConnectOptions = ConnectOptionsBase & Readonly<{
|
|
4
|
+
/** Type-only marker to prevent mixing direct and tunnel option types. */
|
|
5
|
+
__mode?: "direct";
|
|
6
|
+
/** Reserved for tunnel connects; forbidden for direct connects. */
|
|
7
|
+
endpointInstanceId?: never;
|
|
8
|
+
}>;
|
|
4
9
|
export declare function connectDirect(info: unknown, opts: DirectConnectOptions): Promise<ClientInternal>;
|
package/dist/node/connect.js
CHANGED
|
@@ -2,19 +2,7 @@ import { connectDirect } from "../direct-client/connect.js";
|
|
|
2
2
|
import { connectTunnel } from "../tunnel-client/connect.js";
|
|
3
3
|
import { connect } from "../facade.js";
|
|
4
4
|
import { createNodeWsFactory } from "./wsFactory.js";
|
|
5
|
-
|
|
6
|
-
const defaultMaxRecordBytes = 1 << 20;
|
|
7
|
-
const handshakeFrameOverheadBytes = 4 + 1 + 1 + 4;
|
|
8
|
-
const wsMaxPayloadSlackBytes = 64;
|
|
9
|
-
function defaultWsMaxPayload(opts) {
|
|
10
|
-
const maxHandshakePayload = opts.maxHandshakePayload ?? 0;
|
|
11
|
-
const maxRecordBytes = opts.maxRecordBytes ?? 0;
|
|
12
|
-
const hp = Number.isSafeInteger(maxHandshakePayload) && maxHandshakePayload > 0 ? maxHandshakePayload : defaultMaxHandshakePayload;
|
|
13
|
-
const rb = Number.isSafeInteger(maxRecordBytes) && maxRecordBytes > 0 ? maxRecordBytes : defaultMaxRecordBytes;
|
|
14
|
-
const handshakeMax = Math.min(Number.MAX_SAFE_INTEGER, hp + handshakeFrameOverheadBytes);
|
|
15
|
-
const max = Math.max(rb, handshakeMax);
|
|
16
|
-
return Math.min(Number.MAX_SAFE_INTEGER, max + wsMaxPayloadSlackBytes);
|
|
17
|
-
}
|
|
5
|
+
import { defaultWsMaxPayload } from "./wsDefaults.js";
|
|
18
6
|
export async function connectNode(input, opts) {
|
|
19
7
|
const wsFactory = opts.wsFactory ??
|
|
20
8
|
createNodeWsFactory({
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const defaultMaxHandshakePayload = 8 * 1024;
|
|
2
|
+
const defaultMaxRecordBytes = 1 << 20;
|
|
3
|
+
const handshakeFrameOverheadBytes = 4 + 1 + 1 + 4;
|
|
4
|
+
const wsMaxPayloadSlackBytes = 64;
|
|
5
|
+
export function defaultWsMaxPayload(opts) {
|
|
6
|
+
const maxHandshakePayload = opts.maxHandshakePayload ?? 0;
|
|
7
|
+
const maxRecordBytes = opts.maxRecordBytes ?? 0;
|
|
8
|
+
const hp = Number.isSafeInteger(maxHandshakePayload) && maxHandshakePayload > 0 ? maxHandshakePayload : defaultMaxHandshakePayload;
|
|
9
|
+
const rb = Number.isSafeInteger(maxRecordBytes) && maxRecordBytes > 0 ? maxRecordBytes : defaultMaxRecordBytes;
|
|
10
|
+
const handshakeMax = Math.min(Number.MAX_SAFE_INTEGER, hp + handshakeFrameOverheadBytes);
|
|
11
|
+
const max = Math.max(rb, handshakeMax);
|
|
12
|
+
return Math.min(Number.MAX_SAFE_INTEGER, max + wsMaxPayloadSlackBytes);
|
|
13
|
+
}
|
package/dist/node/wsFactory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { defaultWsMaxPayload } from "./wsDefaults.js";
|
|
2
3
|
// createNodeWsFactory returns a wsFactory compatible with connectTunnel/connectDirect in Node.js.
|
|
3
4
|
//
|
|
4
5
|
// It uses the "ws" package to set the Origin header explicitly (browsers set Origin automatically).
|
|
@@ -6,15 +7,16 @@ export function createNodeWsFactory(opts = {}) {
|
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const wsMod = require("ws");
|
|
8
9
|
const WebSocketCtor = wsMod?.WebSocket ?? wsMod;
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
10
|
+
const maxPayloadRaw = opts.maxPayload ?? defaultWsMaxPayload({});
|
|
11
|
+
if (!Number.isSafeInteger(maxPayloadRaw) || maxPayloadRaw <= 0) {
|
|
11
12
|
throw new Error("maxPayload must be a positive integer");
|
|
12
13
|
}
|
|
14
|
+
const maxPayload = maxPayloadRaw;
|
|
13
15
|
const perMessageDeflate = opts.perMessageDeflate ?? false;
|
|
14
16
|
return (url, origin) => {
|
|
15
17
|
const raw = new WebSocketCtor(url, {
|
|
16
18
|
headers: { Origin: origin },
|
|
17
|
-
|
|
19
|
+
maxPayload,
|
|
18
20
|
perMessageDeflate,
|
|
19
21
|
});
|
|
20
22
|
// Map (type -> user listener -> wrapped listener) so removeEventListener works.
|
|
@@ -4,5 +4,3 @@ export declare const PROXY_KIND_WS: "flowersec-proxy/ws";
|
|
|
4
4
|
export declare const DEFAULT_MAX_CHUNK_BYTES: number;
|
|
5
5
|
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
6
6
|
export declare const DEFAULT_MAX_WS_FRAME_BYTES: number;
|
|
7
|
-
export declare const DEFAULT_DEFAULT_TIMEOUT_MS = 30000;
|
|
8
|
-
export declare const DEFAULT_MAX_TIMEOUT_MS: number;
|
package/dist/proxy/constants.js
CHANGED
|
@@ -4,5 +4,3 @@ export const PROXY_KIND_WS = "flowersec-proxy/ws";
|
|
|
4
4
|
export const DEFAULT_MAX_CHUNK_BYTES = 256 * 1024;
|
|
5
5
|
export const DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
|
|
6
6
|
export const DEFAULT_MAX_WS_FRAME_BYTES = 1024 * 1024;
|
|
7
|
-
export const DEFAULT_DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
-
export const DEFAULT_MAX_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -24,6 +24,8 @@ export type ProxyServiceWorkerInjectHTMLOptions = (ProxyServiceWorkerInjectHTMLI
|
|
|
24
24
|
}>;
|
|
25
25
|
export type ProxyServiceWorkerScriptOptions = Readonly<{
|
|
26
26
|
sameOriginOnly?: boolean;
|
|
27
|
+
maxRequestBodyBytes?: number;
|
|
28
|
+
maxInjectHTMLBytes?: number;
|
|
27
29
|
passthrough?: ProxyServiceWorkerPassthroughOptions;
|
|
28
30
|
proxyPathPrefix?: string;
|
|
29
31
|
stripProxyPathPrefix?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_MAX_BODY_BYTES } from "./constants.js";
|
|
1
2
|
function normalizePathPrefix(name, v) {
|
|
2
3
|
const s = typeof v === "string" ? v.trim() : "";
|
|
3
4
|
if (s === "")
|
|
@@ -24,6 +25,21 @@ function normalizePathList(name, input) {
|
|
|
24
25
|
}
|
|
25
26
|
return Array.from(new Set(out));
|
|
26
27
|
}
|
|
28
|
+
const defaultMaxInjectHTMLBytes = 2 * 1024 * 1024;
|
|
29
|
+
function normalizeMaxBytes(name, v, defaultValue) {
|
|
30
|
+
if (v == null)
|
|
31
|
+
return defaultValue;
|
|
32
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
33
|
+
throw new Error(`${name} must be a finite number`);
|
|
34
|
+
const n = Math.floor(v);
|
|
35
|
+
if (!Number.isSafeInteger(n))
|
|
36
|
+
throw new Error(`${name} must be a safe integer`);
|
|
37
|
+
if (n < 0)
|
|
38
|
+
throw new Error(`${name} must be >= 0`);
|
|
39
|
+
if (n === 0)
|
|
40
|
+
return defaultValue;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
27
43
|
// createProxyServiceWorkerScript returns a Service Worker script that forwards fetches to a runtime
|
|
28
44
|
// in a controlled window via postMessage + MessageChannel.
|
|
29
45
|
//
|
|
@@ -33,6 +49,8 @@ export function createProxyServiceWorkerScript(opts = {}) {
|
|
|
33
49
|
if (typeof sameOriginOnly !== "boolean") {
|
|
34
50
|
throw new Error("sameOriginOnly must be a boolean");
|
|
35
51
|
}
|
|
52
|
+
const maxRequestBodyBytes = normalizeMaxBytes("maxRequestBodyBytes", opts.maxRequestBodyBytes, DEFAULT_MAX_BODY_BYTES);
|
|
53
|
+
const maxInjectHTMLBytes = normalizeMaxBytes("maxInjectHTMLBytes", opts.maxInjectHTMLBytes, defaultMaxInjectHTMLBytes);
|
|
36
54
|
const proxyPathPrefix = normalizePathPrefix("proxyPathPrefix", opts.proxyPathPrefix);
|
|
37
55
|
const stripProxyPathPrefix = opts.stripProxyPathPrefix ?? false;
|
|
38
56
|
if (typeof stripProxyPathPrefix !== "boolean") {
|
|
@@ -96,6 +114,9 @@ const INJECT_EXCLUDE_PREFIXES = ${JSON.stringify(excludeInjectPrefixes)};
|
|
|
96
114
|
const INJECT_STRIP_VALIDATOR_HEADERS = ${JSON.stringify(stripValidatorHeaders)};
|
|
97
115
|
const INJECT_SET_NO_STORE = ${JSON.stringify(setNoStore)};
|
|
98
116
|
|
|
117
|
+
const MAX_REQUEST_BODY_BYTES = ${JSON.stringify(maxRequestBodyBytes)};
|
|
118
|
+
const MAX_INJECT_HTML_BYTES = ${JSON.stringify(maxInjectHTMLBytes)};
|
|
119
|
+
|
|
99
120
|
const INJECT_STRIP_HEADER_NAMES = new Set(["content-length", "etag", "last-modified", "content-md5"]);
|
|
100
121
|
|
|
101
122
|
let runtimeClientId = null;
|
|
@@ -163,6 +184,54 @@ function concatChunks(chunks) {
|
|
|
163
184
|
return out;
|
|
164
185
|
}
|
|
165
186
|
|
|
187
|
+
function makeError(status, message) {
|
|
188
|
+
const s = Math.max(0, Math.floor(status));
|
|
189
|
+
const e = new Error(String(message || "proxy error"));
|
|
190
|
+
e.status = s > 0 ? s : 502;
|
|
191
|
+
return e;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getErrorStatus(err, fallback) {
|
|
195
|
+
const raw =
|
|
196
|
+
err && typeof err === "object" && typeof err.status === "number" && Number.isFinite(err.status)
|
|
197
|
+
? Math.floor(err.status)
|
|
198
|
+
: fallback;
|
|
199
|
+
return raw > 0 ? raw : 502;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readRequestBody(req) {
|
|
203
|
+
const clRaw = req.headers.get("content-length");
|
|
204
|
+
const cl = clRaw ? Number(clRaw) : 0;
|
|
205
|
+
if (MAX_REQUEST_BODY_BYTES > 0 && Number.isFinite(cl) && cl > MAX_REQUEST_BODY_BYTES) {
|
|
206
|
+
throw makeError(413, "request body too large");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Prefer streaming reads so we can enforce MAX_REQUEST_BODY_BYTES without allocating unbounded buffers.
|
|
210
|
+
if (!req.body) {
|
|
211
|
+
const ab = await req.arrayBuffer();
|
|
212
|
+
if (MAX_REQUEST_BODY_BYTES > 0 && ab.byteLength > MAX_REQUEST_BODY_BYTES) {
|
|
213
|
+
throw makeError(413, "request body too large");
|
|
214
|
+
}
|
|
215
|
+
return ab;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const reader = req.body.getReader();
|
|
219
|
+
const chunks = [];
|
|
220
|
+
let total = 0;
|
|
221
|
+
while (true) {
|
|
222
|
+
const r = await reader.read();
|
|
223
|
+
if (r.done) break;
|
|
224
|
+
const b = r.value;
|
|
225
|
+
total += b.length;
|
|
226
|
+
if (MAX_REQUEST_BODY_BYTES > 0 && total > MAX_REQUEST_BODY_BYTES) {
|
|
227
|
+
try { reader.cancel(); } catch {}
|
|
228
|
+
throw makeError(413, "request body too large");
|
|
229
|
+
}
|
|
230
|
+
chunks.push(b);
|
|
231
|
+
}
|
|
232
|
+
return concatChunks(chunks).buffer;
|
|
233
|
+
}
|
|
234
|
+
|
|
166
235
|
function injectBootstrap(html) {
|
|
167
236
|
let snippet = "";
|
|
168
237
|
|
|
@@ -215,116 +284,167 @@ self.addEventListener("fetch", (event) => {
|
|
|
215
284
|
});
|
|
216
285
|
|
|
217
286
|
async function handleFetch(event) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const queued = [];
|
|
235
|
-
const htmlChunks = [];
|
|
236
|
-
let shouldInjectHTML = false;
|
|
237
|
-
const injectAllowed = INJECT_HTML && !shouldSkipInject(url.pathname);
|
|
238
|
-
|
|
239
|
-
let doneResolve;
|
|
240
|
-
let doneReject;
|
|
241
|
-
const donePromise = new Promise((resolve, reject) => { doneResolve = resolve; doneReject = reject; });
|
|
242
|
-
|
|
243
|
-
let controller = null;
|
|
244
|
-
|
|
245
|
-
const stream = new ReadableStream({
|
|
246
|
-
start(c) { controller = c; },
|
|
247
|
-
cancel() {
|
|
248
|
-
try { port.postMessage({ type: "flowersec-proxy:abort" }); } catch {}
|
|
249
|
-
try { port.close(); } catch {}
|
|
287
|
+
let lastErrorStatus = 502;
|
|
288
|
+
let lastErrorMessage = "proxy error";
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const runtime = await getRuntimeClient();
|
|
292
|
+
if (!runtime) return new Response("flowersec-proxy runtime not available", { status: 503 });
|
|
293
|
+
|
|
294
|
+
const req = event.request;
|
|
295
|
+
const url = new URL(req.url);
|
|
296
|
+
const id = Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
297
|
+
|
|
298
|
+
let body;
|
|
299
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
300
|
+
body = undefined;
|
|
301
|
+
} else {
|
|
302
|
+
body = await readRequestBody(req);
|
|
250
303
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
304
|
+
|
|
305
|
+
const ch = new MessageChannel();
|
|
306
|
+
const port = ch.port1;
|
|
307
|
+
const port2 = ch.port2;
|
|
308
|
+
|
|
309
|
+
let metaResolve;
|
|
310
|
+
let metaReject;
|
|
311
|
+
const metaPromise = new Promise((resolve, reject) => { metaResolve = resolve; metaReject = reject; });
|
|
312
|
+
|
|
313
|
+
const queued = [];
|
|
314
|
+
const htmlChunks = [];
|
|
315
|
+
let htmlBytes = 0;
|
|
316
|
+
let shouldInjectHTML = false;
|
|
317
|
+
const injectAllowed = INJECT_HTML && !shouldSkipInject(url.pathname);
|
|
318
|
+
|
|
319
|
+
let doneResolve;
|
|
320
|
+
let doneReject;
|
|
321
|
+
const donePromise = new Promise((resolve, reject) => { doneResolve = resolve; doneReject = reject; });
|
|
322
|
+
|
|
323
|
+
let controller = null;
|
|
324
|
+
|
|
325
|
+
const stream = new ReadableStream({
|
|
326
|
+
start(c) { controller = c; },
|
|
327
|
+
cancel() {
|
|
328
|
+
try { port.postMessage({ type: "flowersec-proxy:abort" }); } catch {}
|
|
329
|
+
try { port.close(); } catch {}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
function pushInjectChunk(b) {
|
|
334
|
+
htmlBytes += b.length;
|
|
335
|
+
if (MAX_INJECT_HTML_BYTES > 0 && htmlBytes > MAX_INJECT_HTML_BYTES) {
|
|
336
|
+
const err = makeError(502, "html response too large to inject");
|
|
337
|
+
lastErrorStatus = err.status;
|
|
338
|
+
lastErrorMessage = err.message;
|
|
339
|
+
metaReject(err);
|
|
340
|
+
doneReject(err);
|
|
341
|
+
controller?.error(err);
|
|
342
|
+
try { port.close(); } catch {}
|
|
343
|
+
return false;
|
|
260
344
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (shouldInjectHTML) for (const q of queued) htmlChunks.push(q);
|
|
264
|
-
queued.length = 0;
|
|
265
|
-
return;
|
|
345
|
+
htmlChunks.push(b);
|
|
346
|
+
return true;
|
|
266
347
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
348
|
+
|
|
349
|
+
port.onmessage = (ev) => {
|
|
350
|
+
const m = ev.data;
|
|
351
|
+
if (!m || typeof m.type !== "string") return;
|
|
352
|
+
if (m.type === "flowersec-proxy:response_meta") {
|
|
353
|
+
if (injectAllowed) {
|
|
354
|
+
const ct = String((m.headers || []).find((h) => (h.name || "").toLowerCase() === "content-type")?.value || "");
|
|
355
|
+
shouldInjectHTML = ct.toLowerCase().includes("text/html");
|
|
356
|
+
|
|
357
|
+
if (shouldInjectHTML && MAX_INJECT_HTML_BYTES > 0) {
|
|
358
|
+
const cl = Number(
|
|
359
|
+
String((m.headers || []).find((h) => (h.name || "").toLowerCase() === "content-length")?.value || "")
|
|
360
|
+
);
|
|
361
|
+
if (Number.isFinite(cl) && cl > MAX_INJECT_HTML_BYTES) {
|
|
362
|
+
const err = makeError(502, "html response too large to inject");
|
|
363
|
+
lastErrorStatus = err.status;
|
|
364
|
+
lastErrorMessage = err.message;
|
|
365
|
+
metaReject(err);
|
|
366
|
+
doneReject(err);
|
|
367
|
+
controller?.error(err);
|
|
368
|
+
try { port.close(); } catch {}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
metaResolve(m);
|
|
374
|
+
if (controller && !shouldInjectHTML) for (const q of queued) controller.enqueue(q);
|
|
375
|
+
if (shouldInjectHTML) for (const q of queued) if (!pushInjectChunk(q)) return;
|
|
376
|
+
queued.length = 0;
|
|
271
377
|
return;
|
|
272
378
|
}
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
379
|
+
if (m.type === "flowersec-proxy:response_chunk") {
|
|
380
|
+
const b = new Uint8Array(m.data);
|
|
381
|
+
if (shouldInjectHTML) {
|
|
382
|
+
pushInjectChunk(b);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (controller) controller.enqueue(b); else queued.push(b);
|
|
279
386
|
return;
|
|
280
387
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
388
|
+
if (m.type === "flowersec-proxy:response_end") {
|
|
389
|
+
if (shouldInjectHTML) {
|
|
390
|
+
doneResolve(htmlChunks);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
controller?.close();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (m.type === "flowersec-proxy:response_error") {
|
|
397
|
+
const status = typeof m.status === "number" && Number.isFinite(m.status) ? Math.floor(m.status) : 502;
|
|
398
|
+
lastErrorStatus = status > 0 ? status : 502;
|
|
399
|
+
lastErrorMessage = String(m.message || "proxy error");
|
|
400
|
+
const err = makeError(lastErrorStatus, lastErrorMessage);
|
|
401
|
+
metaReject(err);
|
|
402
|
+
doneReject(err);
|
|
403
|
+
controller?.error(err);
|
|
404
|
+
try { port.close(); } catch {}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
let path = url.pathname + url.search;
|
|
410
|
+
if (PROXY_PATH_PREFIX && STRIP_PROXY_PATH_PREFIX) {
|
|
411
|
+
let rest = url.pathname.slice(PROXY_PATH_PREFIX.length);
|
|
412
|
+
if (rest.startsWith("/")) rest = rest.slice(1);
|
|
413
|
+
path = "/" + rest + url.search;
|
|
291
414
|
}
|
|
292
|
-
};
|
|
293
415
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
416
|
+
runtime.postMessage({
|
|
417
|
+
type: "flowersec-proxy:fetch",
|
|
418
|
+
req: { id, method: req.method, path, headers: headersToPairs(req.headers), body }
|
|
419
|
+
}, [port2]);
|
|
420
|
+
|
|
421
|
+
const meta = await metaPromise;
|
|
422
|
+
const headers = new Headers();
|
|
423
|
+
for (const h of (meta.headers || [])) {
|
|
424
|
+
const name = String(h.name || "");
|
|
425
|
+
const lower = name.toLowerCase();
|
|
426
|
+
if (shouldInjectHTML && INJECT_STRIP_VALIDATOR_HEADERS && INJECT_STRIP_HEADER_NAMES.has(lower)) continue;
|
|
427
|
+
headers.append(name, String(h.value || ""));
|
|
428
|
+
}
|
|
300
429
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}, [port2]);
|
|
305
|
-
|
|
306
|
-
const meta = await metaPromise;
|
|
307
|
-
const headers = new Headers();
|
|
308
|
-
for (const h of (meta.headers || [])) {
|
|
309
|
-
const name = String(h.name || "");
|
|
310
|
-
const lower = name.toLowerCase();
|
|
311
|
-
if (shouldInjectHTML && INJECT_STRIP_VALIDATOR_HEADERS && INJECT_STRIP_HEADER_NAMES.has(lower)) continue;
|
|
312
|
-
headers.append(name, String(h.value || ""));
|
|
313
|
-
}
|
|
430
|
+
if (shouldInjectHTML && INJECT_SET_NO_STORE && !headers.has("Cache-Control")) {
|
|
431
|
+
headers.set("Cache-Control", "no-store");
|
|
432
|
+
}
|
|
314
433
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
434
|
+
if (shouldInjectHTML) {
|
|
435
|
+
const chunks = await donePromise;
|
|
436
|
+
const raw = concatChunks(chunks);
|
|
437
|
+
const html = new TextDecoder().decode(raw);
|
|
438
|
+
const injected = injectBootstrap(html);
|
|
439
|
+
return new Response(new TextEncoder().encode(injected), { status: meta.status || 502, headers });
|
|
440
|
+
}
|
|
318
441
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
return new Response(new TextEncoder().encode(injected), { status: meta.status || 502, headers });
|
|
442
|
+
return new Response(stream, { status: meta.status || 502, headers });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const status = getErrorStatus(err, lastErrorStatus);
|
|
445
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
446
|
+
return new Response(msg || lastErrorMessage || "flowersec-proxy error", { status });
|
|
325
447
|
}
|
|
326
|
-
|
|
327
|
-
return new Response(stream, { status: meta.status || 502, headers });
|
|
328
448
|
}
|
|
329
449
|
`;
|
|
330
450
|
}
|
package/dist/proxy/wsPatch.js
CHANGED
|
@@ -119,14 +119,18 @@ export function installWebSocketPatch(opts) {
|
|
|
119
119
|
removeEventListener(type, listener) {
|
|
120
120
|
this.listeners.off(type, listener);
|
|
121
121
|
}
|
|
122
|
+
queueWriteFrame(stream, op, payload) {
|
|
123
|
+
this.writeChain = this.writeChain
|
|
124
|
+
.then(() => writeWSFrame(stream, op, payload, maxWsFrameBytes))
|
|
125
|
+
.catch((e) => this.fail(e));
|
|
126
|
+
}
|
|
122
127
|
send(data) {
|
|
123
128
|
if (this.readyState !== PatchedWebSocket.OPEN || this.stream == null) {
|
|
124
129
|
throw new Error("WebSocket is not open");
|
|
125
130
|
}
|
|
131
|
+
const s = this.stream;
|
|
126
132
|
const sendBytes = (op, payload) => {
|
|
127
|
-
this.
|
|
128
|
-
.then(() => writeWSFrame(this.stream, op, payload, maxWsFrameBytes))
|
|
129
|
-
.catch((e) => this.fail(e));
|
|
133
|
+
this.queueWriteFrame(s, op, payload);
|
|
130
134
|
};
|
|
131
135
|
if (typeof data === "string") {
|
|
132
136
|
sendBytes(1, te.encode(data));
|
|
@@ -216,7 +220,8 @@ export function installWebSocketPatch(opts) {
|
|
|
216
220
|
const { op, payload } = await readWSFrame(reader, maxWsFrameBytes);
|
|
217
221
|
if (op === 9) {
|
|
218
222
|
// Ping -> Pong (not exposed to WebSocket JS API).
|
|
219
|
-
|
|
223
|
+
// Must be serialized with user writes, otherwise concurrent writes can corrupt framing.
|
|
224
|
+
this.queueWriteFrame(stream, 10, payload);
|
|
220
225
|
continue;
|
|
221
226
|
}
|
|
222
227
|
if (op === 10)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ConnectOptionsBase } from "../client-connect/connectCore.js";
|
|
2
2
|
import type { ClientInternal } from "../client.js";
|
|
3
3
|
export type TunnelConnectOptions = ConnectOptionsBase & Readonly<{
|
|
4
|
+
/** Type-only marker to prevent mixing direct and tunnel option types. */
|
|
5
|
+
__mode?: "tunnel";
|
|
4
6
|
/** Optional caller-provided endpoint instance ID (base64url). */
|
|
5
7
|
endpointInstanceId?: string;
|
|
6
8
|
}>;
|