@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) {
@@ -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 {};
@@ -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
- try {
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
- if (event.source && typeof event.source.id === "string") runtimeClientId = event.source.id;
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: { id, method: req.method, path, headers: headersToPairs(req.headers), body }
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;
@@ -12,6 +12,7 @@ export type HttpRequestMetaV1 = Readonly<{
12
12
  method: string;
13
13
  path: string;
14
14
  headers: Header[];
15
+ external_origin?: string;
15
16
  timeout_ms?: number;
16
17
  }>;
17
18
  export type HttpResponseMetaV1 = Readonly<{
@@ -13,6 +13,7 @@ export type ProxyWindowFetchRequest = Readonly<{
13
13
  method: string;
14
14
  path: string;
15
15
  headers: readonly Header[];
16
+ external_origin?: string;
16
17
  body?: ArrayBuffer;
17
18
  }>;
18
19
  export type ProxyWindowFetchForwardMsg = Readonly<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {