@floegence/flowersec-core 0.19.1 → 0.19.3
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
|
+
}
|
|
@@ -13,6 +13,28 @@ function normalizePathPrefix(name, v) {
|
|
|
13
13
|
throw new Error(`${name} must not include scheme/host`);
|
|
14
14
|
return s;
|
|
15
15
|
}
|
|
16
|
+
function normalizeRootRelativeScriptURL(name, v) {
|
|
17
|
+
if (typeof v !== "string")
|
|
18
|
+
throw new Error(`${name} must be a string`);
|
|
19
|
+
const s = v.trim();
|
|
20
|
+
if (s === "")
|
|
21
|
+
throw new Error(`${name} must be non-empty`);
|
|
22
|
+
if (s !== v)
|
|
23
|
+
throw new Error(`${name} must not contain leading or trailing whitespace`);
|
|
24
|
+
if (!s.startsWith("/"))
|
|
25
|
+
throw new Error(`${name} must start with "/"`);
|
|
26
|
+
if (s.startsWith("//"))
|
|
27
|
+
throw new Error(`${name} must not start with "//"`);
|
|
28
|
+
if (s.includes("://"))
|
|
29
|
+
throw new Error(`${name} must not include scheme/host`);
|
|
30
|
+
if (s.includes("\\"))
|
|
31
|
+
throw new Error(`${name} must not contain backslash`);
|
|
32
|
+
if (/[\s\u0000-\u001f\u007f]/.test(s))
|
|
33
|
+
throw new Error(`${name} must not contain whitespace or control characters`);
|
|
34
|
+
if (/[<>"'`]/.test(s))
|
|
35
|
+
throw new Error(`${name} must not contain HTML attribute delimiters`);
|
|
36
|
+
return s;
|
|
37
|
+
}
|
|
16
38
|
function normalizePathList(name, input) {
|
|
17
39
|
const out = [];
|
|
18
40
|
if (input == null || input.length === 0)
|
|
@@ -116,13 +138,9 @@ export function createProxyServiceWorkerScript(opts = {}) {
|
|
|
116
138
|
}
|
|
117
139
|
}
|
|
118
140
|
else {
|
|
119
|
-
|
|
120
|
-
"scriptUrl" in injectHTML && typeof injectHTML.scriptUrl === "string"
|
|
121
|
-
? injectHTML.scriptUrl.trim()
|
|
122
|
-
: "";
|
|
123
|
-
if (injectScriptUrl === "") {
|
|
141
|
+
if (!("scriptUrl" in injectHTML))
|
|
124
142
|
throw new Error("injectHTML.scriptUrl must be non-empty");
|
|
125
|
-
|
|
143
|
+
injectScriptUrl = normalizeRootRelativeScriptURL("injectHTML.scriptUrl", injectHTML.scriptUrl);
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
146
|
return `// Generated by @floegence/flowersec-core/proxy
|
|
@@ -168,7 +186,13 @@ self.addEventListener("message", (event) => {
|
|
|
168
186
|
if (!data || typeof data !== "object") return;
|
|
169
187
|
const msgType = typeof data.type === "string" ? data.type : "";
|
|
170
188
|
if (msgType === "flowersec-proxy:register-runtime") {
|
|
171
|
-
|
|
189
|
+
const ok = Boolean(event.source && typeof event.source.id === "string");
|
|
190
|
+
if (ok) runtimeClientId = event.source.id;
|
|
191
|
+
const port = event.ports && event.ports[0];
|
|
192
|
+
if (port) {
|
|
193
|
+
try { port.postMessage({ type: "flowersec-proxy:register-runtime-ack", ok }); } catch {}
|
|
194
|
+
try { port.close(); } catch {}
|
|
195
|
+
}
|
|
172
196
|
return;
|
|
173
197
|
}
|
|
174
198
|
if (!FORWARD_FETCH_MESSAGE_TYPES.has(msgType)) return;
|
|
@@ -309,16 +333,16 @@ function injectBootstrap(html) {
|
|
|
309
333
|
} else if (INJECT_MODE === "external_module") {
|
|
310
334
|
snippet =
|
|
311
335
|
'<script type="module" src="' +
|
|
312
|
-
INJECT_SCRIPT_URL +
|
|
336
|
+
escapeHTMLAttributeValue(INJECT_SCRIPT_URL) +
|
|
313
337
|
'"' +
|
|
314
|
-
(RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + RUNTIME_GLOBAL + '"' : "") +
|
|
338
|
+
(RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + escapeHTMLAttributeValue(RUNTIME_GLOBAL) + '"' : "") +
|
|
315
339
|
"></script>";
|
|
316
340
|
} else if (INJECT_MODE === "external_script") {
|
|
317
341
|
snippet =
|
|
318
342
|
'<script src="' +
|
|
319
|
-
INJECT_SCRIPT_URL +
|
|
343
|
+
escapeHTMLAttributeValue(INJECT_SCRIPT_URL) +
|
|
320
344
|
'"' +
|
|
321
|
-
(RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + RUNTIME_GLOBAL + '"' : "") +
|
|
345
|
+
(RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + escapeHTMLAttributeValue(RUNTIME_GLOBAL) + '"' : "") +
|
|
322
346
|
"></script>";
|
|
323
347
|
}
|
|
324
348
|
|
|
@@ -335,6 +359,21 @@ function injectBootstrap(html) {
|
|
|
335
359
|
return snippet + html;
|
|
336
360
|
}
|
|
337
361
|
|
|
362
|
+
function escapeHTMLAttributeValue(value) {
|
|
363
|
+
return String(value).replace(/[&<>"'\`=]/g, (ch) => {
|
|
364
|
+
switch (ch) {
|
|
365
|
+
case "&": return "&";
|
|
366
|
+
case "<": return "<";
|
|
367
|
+
case ">": return ">";
|
|
368
|
+
case '"': return """;
|
|
369
|
+
case "'": return "'";
|
|
370
|
+
case "\`": return "`";
|
|
371
|
+
case "=": return "=";
|
|
372
|
+
default: return ch;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
338
377
|
self.addEventListener("fetch", (event) => {
|
|
339
378
|
const url = new URL(event.request.url);
|
|
340
379
|
|
|
@@ -362,6 +401,7 @@ async function handleFetch(event) {
|
|
|
362
401
|
const req = event.request;
|
|
363
402
|
const url = new URL(req.url);
|
|
364
403
|
const id = Math.random().toString(16).slice(2) + Date.now().toString(16);
|
|
404
|
+
const externalOrigin = url.origin === self.location.origin ? url.origin : "";
|
|
365
405
|
|
|
366
406
|
let body;
|
|
367
407
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
@@ -483,7 +523,14 @@ async function handleFetch(event) {
|
|
|
483
523
|
|
|
484
524
|
target.postMessage({
|
|
485
525
|
type: WINDOW_CLIENT_MESSAGE_TYPE,
|
|
486
|
-
req: {
|
|
526
|
+
req: {
|
|
527
|
+
id,
|
|
528
|
+
method: req.method,
|
|
529
|
+
path,
|
|
530
|
+
headers: headersToPairs(req.headers),
|
|
531
|
+
...(externalOrigin ? { external_origin: externalOrigin } : {}),
|
|
532
|
+
body
|
|
533
|
+
}
|
|
487
534
|
}, [port2]);
|
|
488
535
|
|
|
489
536
|
const meta = await metaPromise;
|
package/dist/proxy/types.d.ts
CHANGED