@floegence/flowersec-core 0.19.1 → 0.19.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { profileToPresetManifest } from "./profiles.js";
|
|
2
2
|
import { resolveProxyPreset } from "./preset.js";
|
|
3
3
|
import { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.js";
|
|
4
|
-
import { createProxyRuntime } from "./runtime.js";
|
|
4
|
+
import { createProxyRuntime, ensureServiceWorkerRuntimeRegistered } from "./runtime.js";
|
|
5
5
|
import { createProxyServiceWorkerScript } from "./serviceWorker.js";
|
|
6
6
|
function dedupeStrings(values) {
|
|
7
7
|
const out = [];
|
|
@@ -252,6 +252,7 @@ export async function registerProxyIntegration(input) {
|
|
|
252
252
|
maxRepairAttempts,
|
|
253
253
|
controllerTimeoutMs,
|
|
254
254
|
});
|
|
255
|
+
await ensureServiceWorkerRuntimeRegistered({ timeoutMs: controllerTimeoutMs });
|
|
255
256
|
if (expectedScriptPathSuffix !== "") {
|
|
256
257
|
const ok = await waitForControllerSuffix(expectedScriptPathSuffix, controllerTimeoutMs);
|
|
257
258
|
if (!ok) {
|
package/dist/proxy/runtime.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ type ProxyFetchReq = Readonly<{
|
|
|
7
7
|
method: string;
|
|
8
8
|
path: string;
|
|
9
9
|
headers: readonly Header[];
|
|
10
|
+
external_origin?: string;
|
|
10
11
|
body?: ArrayBuffer;
|
|
11
12
|
}>;
|
|
12
13
|
export type ProxyRuntimeLimits = Readonly<{
|
|
@@ -39,5 +40,9 @@ export type ProxyRuntimeOptions = Readonly<{
|
|
|
39
40
|
extraWsHeaders?: readonly string[];
|
|
40
41
|
cookieJar?: CookieJar;
|
|
41
42
|
}>;
|
|
43
|
+
export type EnsureServiceWorkerRuntimeRegisteredOptions = Readonly<{
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
}>;
|
|
42
46
|
export declare function createProxyRuntime(opts: ProxyRuntimeOptions): ProxyRuntime;
|
|
47
|
+
export declare function ensureServiceWorkerRuntimeRegistered(opts?: EnsureServiceWorkerRuntimeRegisteredOptions): Promise<void>;
|
|
43
48
|
export {};
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -38,6 +38,27 @@ function normalizeTimeoutMs(timeoutMs) {
|
|
|
38
38
|
throw new Error("timeout_ms must be >= 0");
|
|
39
39
|
return v;
|
|
40
40
|
}
|
|
41
|
+
function normalizeExternalOrigin(externalOriginRaw) {
|
|
42
|
+
if (typeof externalOriginRaw !== "string")
|
|
43
|
+
return undefined;
|
|
44
|
+
const externalOrigin = externalOriginRaw.trim();
|
|
45
|
+
if (externalOrigin === "")
|
|
46
|
+
return undefined;
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = new URL(externalOrigin);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new Error("external_origin must be an http(s) origin");
|
|
53
|
+
}
|
|
54
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.host === "") {
|
|
55
|
+
throw new Error("external_origin must be an http(s) origin");
|
|
56
|
+
}
|
|
57
|
+
if (parsed.username !== "" || parsed.password !== "" || (parsed.pathname !== "" && parsed.pathname !== "/") || parsed.search !== "" || parsed.hash !== "") {
|
|
58
|
+
throw new Error("external_origin must be an origin without credentials, path, query, or fragment");
|
|
59
|
+
}
|
|
60
|
+
return parsed.origin;
|
|
61
|
+
}
|
|
41
62
|
function normalizeMaxBytes(name, v, defaultValue) {
|
|
42
63
|
if (v == null)
|
|
43
64
|
return defaultValue;
|
|
@@ -97,13 +118,9 @@ export function createProxyRuntime(opts) {
|
|
|
97
118
|
const extraResponseHeaders = opts.extraResponseHeaders ?? [];
|
|
98
119
|
const extraWsHeaders = opts.extraWsHeaders ?? [];
|
|
99
120
|
const registerRuntime = () => {
|
|
100
|
-
|
|
101
|
-
const ctl = globalThis.navigator?.serviceWorker?.controller;
|
|
102
|
-
ctl?.postMessage({ type: "flowersec-proxy:register-runtime" });
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
121
|
+
void ensureServiceWorkerRuntimeRegistered({ timeoutMs: 2_000 }).catch(() => {
|
|
105
122
|
// Best-effort: controllerchange will retry once the active Service Worker is ready.
|
|
106
|
-
}
|
|
123
|
+
});
|
|
107
124
|
};
|
|
108
125
|
const onMessage = (ev) => {
|
|
109
126
|
const data = ev.data;
|
|
@@ -134,6 +151,7 @@ export function createProxyRuntime(opts) {
|
|
|
134
151
|
try {
|
|
135
152
|
const path = pathOnly(req.path);
|
|
136
153
|
const requestID = req.id.trim() !== "" ? req.id : randomB64u(18);
|
|
154
|
+
const externalOrigin = normalizeExternalOrigin(req.external_origin);
|
|
137
155
|
stream = await client.openStream(PROXY_KIND_HTTP1, { signal: ac.signal });
|
|
138
156
|
const reader = createByteReader(stream, { signal: ac.signal });
|
|
139
157
|
const filteredReqHeaders = filterRequestHeaders(req.headers, { extraAllowed: extraRequestHeaders });
|
|
@@ -145,6 +163,7 @@ export function createProxyRuntime(opts) {
|
|
|
145
163
|
method: req.method,
|
|
146
164
|
path,
|
|
147
165
|
headers: reqHeaders,
|
|
166
|
+
...(externalOrigin === undefined ? {} : { external_origin: externalOrigin }),
|
|
148
167
|
timeout_ms: timeoutMs
|
|
149
168
|
});
|
|
150
169
|
const body = req.body != null ? new Uint8Array(req.body) : new Uint8Array();
|
|
@@ -237,3 +256,60 @@ export function createProxyRuntime(opts) {
|
|
|
237
256
|
}
|
|
238
257
|
};
|
|
239
258
|
}
|
|
259
|
+
export async function ensureServiceWorkerRuntimeRegistered(opts = {}) {
|
|
260
|
+
const ctl = globalThis.navigator?.serviceWorker?.controller;
|
|
261
|
+
if (!ctl || typeof ctl.postMessage !== "function")
|
|
262
|
+
return;
|
|
263
|
+
const timeoutMs = Math.max(0, Math.floor(opts.timeoutMs ?? 2_000));
|
|
264
|
+
const ch = new MessageChannel();
|
|
265
|
+
await new Promise((resolve, reject) => {
|
|
266
|
+
let done = false;
|
|
267
|
+
let timer = null;
|
|
268
|
+
const finish = (error) => {
|
|
269
|
+
if (done)
|
|
270
|
+
return;
|
|
271
|
+
done = true;
|
|
272
|
+
if (timer != null)
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
ch.port1.onmessage = null;
|
|
275
|
+
ch.port1.onmessageerror = null;
|
|
276
|
+
try {
|
|
277
|
+
ch.port1.close();
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Best-effort.
|
|
281
|
+
}
|
|
282
|
+
if (error == null) {
|
|
283
|
+
resolve();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
287
|
+
};
|
|
288
|
+
ch.port1.onmessage = (ev) => {
|
|
289
|
+
const data = ev.data;
|
|
290
|
+
if (data == null || typeof data !== "object")
|
|
291
|
+
return;
|
|
292
|
+
if (data.type !== "flowersec-proxy:register-runtime-ack")
|
|
293
|
+
return;
|
|
294
|
+
if (data.ok !== true) {
|
|
295
|
+
finish(new Error("service worker runtime registration was rejected"));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
finish();
|
|
299
|
+
};
|
|
300
|
+
ch.port1.onmessageerror = () => {
|
|
301
|
+
finish(new Error("service worker runtime registration failed"));
|
|
302
|
+
};
|
|
303
|
+
if (timeoutMs > 0) {
|
|
304
|
+
timer = setTimeout(() => {
|
|
305
|
+
finish(new Error("service worker runtime registration timed out"));
|
|
306
|
+
}, timeoutMs);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
ctl.postMessage({ type: "flowersec-proxy:register-runtime" }, [ch.port2]);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
finish(error);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
@@ -168,7 +168,13 @@ self.addEventListener("message", (event) => {
|
|
|
168
168
|
if (!data || typeof data !== "object") return;
|
|
169
169
|
const msgType = typeof data.type === "string" ? data.type : "";
|
|
170
170
|
if (msgType === "flowersec-proxy:register-runtime") {
|
|
171
|
-
|
|
171
|
+
const ok = Boolean(event.source && typeof event.source.id === "string");
|
|
172
|
+
if (ok) runtimeClientId = event.source.id;
|
|
173
|
+
const port = event.ports && event.ports[0];
|
|
174
|
+
if (port) {
|
|
175
|
+
try { port.postMessage({ type: "flowersec-proxy:register-runtime-ack", ok }); } catch {}
|
|
176
|
+
try { port.close(); } catch {}
|
|
177
|
+
}
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
180
|
if (!FORWARD_FETCH_MESSAGE_TYPES.has(msgType)) return;
|
|
@@ -362,6 +368,7 @@ async function handleFetch(event) {
|
|
|
362
368
|
const req = event.request;
|
|
363
369
|
const url = new URL(req.url);
|
|
364
370
|
const id = Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
371
|
+
const externalOrigin = url.origin === self.location.origin ? url.origin : "";
|
|
365
372
|
|
|
366
373
|
let body;
|
|
367
374
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
@@ -483,7 +490,14 @@ async function handleFetch(event) {
|
|
|
483
490
|
|
|
484
491
|
target.postMessage({
|
|
485
492
|
type: WINDOW_CLIENT_MESSAGE_TYPE,
|
|
486
|
-
req: {
|
|
493
|
+
req: {
|
|
494
|
+
id,
|
|
495
|
+
method: req.method,
|
|
496
|
+
path,
|
|
497
|
+
headers: headersToPairs(req.headers),
|
|
498
|
+
...(externalOrigin ? { external_origin: externalOrigin } : {}),
|
|
499
|
+
body
|
|
500
|
+
}
|
|
487
501
|
}, [port2]);
|
|
488
502
|
|
|
489
503
|
const meta = await metaPromise;
|
package/dist/proxy/types.d.ts
CHANGED