@floegence/flowersec-core 0.24.0 → 0.26.0

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,6 +1,6 @@
1
1
  import { createServiceWorkerControllerGuard, } from "./controllerGuard.js";
2
2
  import { createMessagePortBackedStream } from "./portStream.js";
3
- import { PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE, PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY, } from "./windowBridgeProtocol.js";
3
+ import { PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE, PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, } from "./windowBridgeProtocol.js";
4
4
  function resolveTargetWindow(raw) {
5
5
  const target = raw ?? globalThis.window;
6
6
  if (target == null)
@@ -59,6 +59,8 @@ export function registerProxyAppWindow(opts) {
59
59
  const targetWindow = resolveTargetWindow(opts.targetWindow);
60
60
  const controllerWindow = resolveControllerWindow(targetWindow, opts.controllerWindow);
61
61
  const capabilityNonce = normalizeCapabilityNonce(opts.capabilityNonce);
62
+ const activeWebSocketBridges = new Set();
63
+ let disposed = false;
62
64
  const sw = targetWindow.navigator?.serviceWorker;
63
65
  const onServiceWorkerMessage = (ev) => {
64
66
  const data = ev.data;
@@ -90,15 +92,32 @@ export function registerProxyAppWindow(opts) {
90
92
  : { maxWsBufferedAmountBytes: opts.maxWsBufferedAmountBytes }),
91
93
  },
92
94
  openWebSocketStream: async (path, wsOpts = {}) => {
95
+ if (disposed)
96
+ throw new Error("proxy app Window bridge is disposed");
93
97
  const channel = new MessageChannel();
94
98
  const port = channel.port1;
95
99
  port.start?.();
96
100
  return await new Promise((resolve, reject) => {
97
101
  let settled = false;
102
+ let terminal = false;
103
+ let stream = null;
104
+ const cleanup = () => {
105
+ activeWebSocketBridges.delete(bridge);
106
+ if (wsOpts.signal != null)
107
+ wsOpts.signal.removeEventListener("abort", onAbort);
108
+ };
109
+ const finishTerminal = () => {
110
+ if (terminal)
111
+ return false;
112
+ terminal = true;
113
+ cleanup();
114
+ return true;
115
+ };
98
116
  const finishReject = (error) => {
99
117
  if (settled)
100
118
  return;
101
119
  settled = true;
120
+ finishTerminal();
102
121
  try {
103
122
  port.close();
104
123
  }
@@ -107,16 +126,76 @@ export function registerProxyAppWindow(opts) {
107
126
  }
108
127
  reject(error instanceof Error ? error : new Error(String(error)));
109
128
  };
129
+ const disposeBridge = (error) => {
130
+ if (!finishTerminal())
131
+ return;
132
+ if (stream != null) {
133
+ try {
134
+ void Promise.resolve(stream.reset(error)).catch(() => {
135
+ // The bridge is already terminal.
136
+ });
137
+ }
138
+ catch {
139
+ // The bridge is already terminal.
140
+ }
141
+ return;
142
+ }
143
+ try {
144
+ port.postMessage({
145
+ type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
146
+ message: error.message,
147
+ });
148
+ }
149
+ catch {
150
+ // Best-effort.
151
+ }
152
+ if (!settled) {
153
+ settled = true;
154
+ reject(error);
155
+ }
156
+ try {
157
+ port.close();
158
+ }
159
+ catch {
160
+ // Best-effort.
161
+ }
162
+ };
163
+ const bridge = { dispose: disposeBridge };
164
+ const onAbort = () => {
165
+ const reason = wsOpts.signal?.reason;
166
+ disposeBridge(reason instanceof Error ? reason : new Error(String(reason ?? "aborted")));
167
+ };
168
+ activeWebSocketBridges.add(bridge);
110
169
  const finishResolve = (ack) => {
111
- if (settled)
170
+ if (settled || terminal)
112
171
  return;
113
- settled = true;
114
172
  const capabilities = Array.isArray(ack.capabilities)
115
173
  ? ack.capabilities.filter((value) => typeof value === "string")
116
174
  : [];
117
- const writeAcknowledgements = capabilities.includes(PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY);
175
+ if (!capabilities.includes(PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY)) {
176
+ try {
177
+ port.postMessage({
178
+ type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
179
+ message: "proxy Window bridge capability mismatch",
180
+ });
181
+ }
182
+ catch {
183
+ // The capability error remains authoritative.
184
+ }
185
+ finishReject(new Error("proxy Window bridge does not support bidirectional stream acknowledgements"));
186
+ return;
187
+ }
188
+ if (disposed) {
189
+ disposeBridge(new Error("proxy app Window bridge is disposed"));
190
+ return;
191
+ }
192
+ settled = true;
193
+ stream = createMessagePortBackedStream(port, {
194
+ maxBufferedBytes: opts.maxWsBufferedAmountBytes ?? 4 * (1 << 20),
195
+ onTerminal: finishTerminal,
196
+ });
118
197
  resolve({
119
- stream: createMessagePortBackedStream(port, { writeAcknowledgements }),
198
+ stream,
120
199
  protocol: String(ack.protocol ?? ""),
121
200
  });
122
201
  };
@@ -135,15 +214,16 @@ export function registerProxyAppWindow(opts) {
135
214
  };
136
215
  if (wsOpts.signal != null) {
137
216
  if (wsOpts.signal.aborted) {
138
- finishReject(wsOpts.signal.reason ?? new Error("aborted"));
217
+ onAbort();
139
218
  return;
140
219
  }
141
- wsOpts.signal.addEventListener("abort", () => finishReject(wsOpts.signal?.reason ?? new Error("aborted")), { once: true });
220
+ wsOpts.signal.addEventListener("abort", onAbort, { once: true });
142
221
  }
143
222
  try {
144
223
  controllerWindow.postMessage({
145
224
  type: PROXY_WINDOW_WS_OPEN_MSG_TYPE,
146
225
  path,
226
+ capabilities: [PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY],
147
227
  ...(wsOpts.protocols === undefined ? {} : { protocols: wsOpts.protocols }),
148
228
  ...(capabilityNonce === "" ? {} : { capabilityNonce }),
149
229
  }, controllerOrigin, [channel.port2]);
@@ -157,7 +237,13 @@ export function registerProxyAppWindow(opts) {
157
237
  return {
158
238
  runtime,
159
239
  dispose: () => {
240
+ if (disposed)
241
+ return;
242
+ disposed = true;
160
243
  sw?.removeEventListener("message", onServiceWorkerMessage);
244
+ const error = new Error("proxy app Window bridge is disposed");
245
+ for (const bridge of [...activeWebSocketBridges])
246
+ bridge.dispose(error);
161
247
  },
162
248
  };
163
249
  }
@@ -1,20 +1,11 @@
1
1
  import type { Client } from "../client.js";
2
2
  import type { ConnectArtifact } from "../connect/artifact.js";
3
- import type { ConnectBrowserOptions, TunnelConnectBrowserOptions } from "../browser/connect.js";
4
- import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
3
+ import type { ConnectBrowserOptions } from "../browser/connect.js";
5
4
  import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
6
5
  import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
7
6
  import type { ProxyPresetInput } from "./preset.js";
8
7
  import { type ProxyRuntime } from "./runtime.js";
9
- export type ConnectTunnelProxyBrowserOptions = Readonly<{
10
- connect?: TunnelConnectBrowserOptions;
11
- preset?: ProxyPresetInput;
12
- runtimeGlobalKey?: string;
13
- runtime?: RegisterProxyIntegrationOptions["runtime"];
14
- serviceWorker: ProxyIntegrationServiceWorkerOptions;
15
- plugins?: readonly ProxyIntegrationPlugin[];
16
- }>;
17
- export type ConnectTunnelProxyBrowserHandle = Readonly<{
8
+ export type ConnectArtifactProxyBrowserHandle = Readonly<{
18
9
  client: Client;
19
10
  runtime: ProxyRuntime;
20
11
  dispose: () => Promise<void>;
@@ -27,15 +18,7 @@ export type ConnectArtifactProxyBrowserOptions = Readonly<{
27
18
  serviceWorker?: ProxyIntegrationServiceWorkerOptions;
28
19
  plugins?: readonly ProxyIntegrationPlugin[];
29
20
  }>;
30
- export type ConnectTunnelProxyControllerBrowserOptions = Readonly<{
31
- connect?: TunnelConnectBrowserOptions;
32
- runtime?: RegisterProxyIntegrationOptions["runtime"];
33
- allowedOrigins: RegisterProxyControllerWindowOptions["allowedOrigins"];
34
- targetWindow?: RegisterProxyControllerWindowOptions["targetWindow"];
35
- expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
36
- capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
37
- }>;
38
- export type ConnectTunnelProxyControllerBrowserHandle = Readonly<{
21
+ export type ConnectArtifactProxyControllerBrowserHandle = Readonly<{
39
22
  client: Client;
40
23
  runtime: ProxyRuntime;
41
24
  dispose: () => void;
@@ -48,7 +31,5 @@ export type ConnectArtifactProxyControllerBrowserOptions = Readonly<{
48
31
  expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
49
32
  capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
50
33
  }>;
51
- export declare function connectTunnelProxyBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
52
- export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
53
- export declare function connectTunnelProxyControllerBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
54
- export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
34
+ export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectArtifactProxyBrowserHandle>;
35
+ export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectArtifactProxyControllerBrowserHandle>;
@@ -1,4 +1,4 @@
1
- import { connectBrowser, connectTunnelBrowser } from "../browser/connect.js";
1
+ import { connectBrowser } from "../browser/connect.js";
2
2
  import { registerProxyIntegration, } from "./integration.js";
3
3
  import { registerProxyControllerWindow } from "./controllerWindow.js";
4
4
  import { extractProxyRuntimeScopeV1, resolvePresetInputFromScope, resolveRuntimeLimitsFromScope, resolveRuntimePresetLimits, } from "./runtimeScope.js";
@@ -23,12 +23,10 @@ function scopeRuntimeToIntegrationOptions(scope, opts) {
23
23
  };
24
24
  }
25
25
  async function connectProxyBrowserClient(client, opts) {
26
- const compat = opts;
27
26
  const integrationInput = {
28
27
  client,
29
28
  serviceWorker: opts.serviceWorker,
30
29
  ...(opts.preset === undefined ? {} : { preset: opts.preset }),
31
- ...(compat.profile === undefined ? {} : { profile: compat.profile }),
32
30
  ...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
33
31
  ...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
34
32
  ...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
@@ -101,20 +99,12 @@ function connectProxyControllerClient(client, opts) {
101
99
  },
102
100
  };
103
101
  }
104
- export async function connectTunnelProxyBrowser(grant, opts) {
105
- const client = await connectTunnelBrowser(grant, opts.connect ?? {});
106
- return await connectProxyBrowserClient(client, opts);
107
- }
108
102
  export async function connectArtifactProxyBrowser(artifact, opts = {}) {
109
103
  const scope = extractProxyRuntimeScopeV1(artifact, "service_worker");
110
104
  const client = await connectBrowser(artifact, opts.connect ?? {});
111
105
  const nextOpts = scopeRuntimeToIntegrationOptions(scope, opts);
112
106
  return await connectProxyBrowserClient(client, nextOpts);
113
107
  }
114
- export async function connectTunnelProxyControllerBrowser(grant, opts) {
115
- const client = await connectTunnelBrowser(grant, opts.connect ?? {});
116
- return connectProxyControllerClient(client, opts);
117
- }
118
108
  export async function connectArtifactProxyControllerBrowser(artifact, opts = {}) {
119
109
  const scope = extractProxyRuntimeScopeV1(artifact, "controller_bridge");
120
110
  const client = await connectBrowser(artifact, opts.connect ?? {});
@@ -4,3 +4,4 @@ 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_MAX_CONCURRENT_STREAMS: 64;
@@ -5,3 +5,4 @@ export const PROXY_KIND_WS = "flowersec-proxy/ws";
5
5
  export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
6
6
  export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
7
7
  export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
8
+ export const DEFAULT_MAX_CONCURRENT_STREAMS = SDK_DEFAULTS.proxy.maxConcurrentStreams;
@@ -1,4 +1,4 @@
1
- import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY, } from "./windowBridgeProtocol.js";
1
+ import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, } from "./windowBridgeProtocol.js";
2
2
  function normalizeOrigins(origins) {
3
3
  const out = [];
4
4
  const seen = new Set();
@@ -56,14 +56,45 @@ function withTrustedExternalOrigin(req, rawOrigin) {
56
56
  ? sanitized
57
57
  : { ...sanitized, external_origin: externalOrigin };
58
58
  }
59
- function bridgeWebSocket(runtime, msg, port) {
59
+ function validWriteId(value) {
60
+ return Number.isSafeInteger(value) && Number(value) > 0;
61
+ }
62
+ function bridgeWebSocket(runtime, msg, port, onTerminal) {
63
+ const capabilities = Array.isArray(msg.capabilities)
64
+ ? msg.capabilities.filter((value) => typeof value === "string")
65
+ : [];
66
+ if (!capabilities.includes(PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY)) {
67
+ try {
68
+ port.postMessage({
69
+ type: PROXY_WINDOW_WS_ERROR_MSG_TYPE,
70
+ message: "proxy Window bridge requires bidirectional stream acknowledgements",
71
+ });
72
+ }
73
+ catch {
74
+ // Best-effort.
75
+ }
76
+ finally {
77
+ port.close();
78
+ }
79
+ onTerminal();
80
+ return { dispose: () => { }, isTerminal: () => true };
81
+ }
60
82
  const ac = new AbortController();
61
83
  let terminal = false;
84
+ let terminalError = null;
62
85
  let acceptingWrites = true;
63
86
  let stream = null;
64
- let pendingWriteBytes = 0;
65
- let writeChain = Promise.resolve();
87
+ let pendingInboundWriteId = null;
88
+ let pendingOutboundAcknowledgement = null;
89
+ let nextWriteId = 1;
66
90
  const maxBufferedBytes = runtime.limits.maxWsBufferedAmountBytes ?? 4 * (1 << 20);
91
+ let terminalNotified = false;
92
+ const notifyTerminal = () => {
93
+ if (terminalNotified)
94
+ return;
95
+ terminalNotified = true;
96
+ onTerminal();
97
+ };
67
98
  const closePort = () => {
68
99
  try {
69
100
  port.close();
@@ -72,12 +103,16 @@ function bridgeWebSocket(runtime, msg, port) {
72
103
  // Best-effort.
73
104
  }
74
105
  };
75
- const failBridge = (error) => {
106
+ const failBridge = (error, notifyPeer = true) => {
76
107
  if (terminal)
77
108
  return;
78
109
  terminal = true;
79
110
  acceptingWrites = false;
80
111
  const err = error instanceof Error ? error : new Error(String(error));
112
+ terminalError = err;
113
+ pendingInboundWriteId = null;
114
+ pendingOutboundAcknowledgement?.reject(err);
115
+ pendingOutboundAcknowledgement = null;
81
116
  if (stream != null) {
82
117
  void Promise.resolve(stream.reset(err)).catch(() => {
83
118
  // The bridge error is already delivered through the terminal response.
@@ -89,17 +124,146 @@ function bridgeWebSocket(runtime, msg, port) {
89
124
  catch {
90
125
  // Best-effort.
91
126
  }
92
- try {
93
- port.postMessage({
94
- type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
95
- message: err.message,
96
- });
97
- }
98
- catch {
99
- // Best-effort.
127
+ if (notifyPeer) {
128
+ try {
129
+ port.postMessage({
130
+ type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
131
+ message: err.message,
132
+ });
133
+ }
134
+ catch {
135
+ // Best-effort.
136
+ }
100
137
  }
101
138
  closePort();
139
+ notifyTerminal();
140
+ };
141
+ const failProtocol = (message) => {
142
+ failBridge(new Error(`proxy Window stream protocol error: ${message}`));
143
+ };
144
+ const postChunkAndWait = async (chunk) => {
145
+ if (chunk.byteLength > maxBufferedBytes) {
146
+ throw new Error("proxy WebSocket inbound chunk exceeded the buffer limit");
147
+ }
148
+ if (!Number.isSafeInteger(nextWriteId)) {
149
+ throw new Error("proxy Window stream write identifier space exhausted");
150
+ }
151
+ if (pendingOutboundAcknowledgement != null) {
152
+ throw new Error("proxy Window stream already has an unacknowledged outbound chunk");
153
+ }
154
+ const writeId = nextWriteId++;
155
+ const ab = cloneChunk(chunk);
156
+ await new Promise((resolve, reject) => {
157
+ pendingOutboundAcknowledgement = { writeId, resolve, reject };
158
+ try {
159
+ port.postMessage({
160
+ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE,
161
+ data: ab,
162
+ writeId,
163
+ }, [ab]);
164
+ }
165
+ catch (error) {
166
+ pendingOutboundAcknowledgement = null;
167
+ reject(error instanceof Error ? error : new Error(String(error)));
168
+ }
169
+ });
170
+ };
171
+ port.onmessage = (ev) => {
172
+ const data = ev.data;
173
+ if (data == null || typeof data !== "object")
174
+ return;
175
+ const type = typeof data.type === "string" ? data.type : "";
176
+ if (stream == null) {
177
+ if (type === PROXY_WINDOW_STREAM_RESET_MSG_TYPE) {
178
+ const message = String(data.message ?? "stream reset");
179
+ failBridge(new Error(message), false);
180
+ }
181
+ else if (type === PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE
182
+ || type === PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE
183
+ || type === PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE) {
184
+ failProtocol("received stream traffic before the websocket opened");
185
+ }
186
+ return;
187
+ }
188
+ switch (type) {
189
+ case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
190
+ if (!acceptingWrites)
191
+ return;
192
+ const chunkMessage = data;
193
+ const raw = chunkMessage.data;
194
+ if (!(raw instanceof ArrayBuffer) || !validWriteId(chunkMessage.writeId)) {
195
+ failProtocol("invalid stream chunk");
196
+ return;
197
+ }
198
+ if (pendingInboundWriteId != null) {
199
+ failProtocol("received more than one unacknowledged chunk");
200
+ return;
201
+ }
202
+ if (raw.byteLength > maxBufferedBytes) {
203
+ failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
204
+ return;
205
+ }
206
+ const writeId = chunkMessage.writeId;
207
+ const chunk = new Uint8Array(raw);
208
+ pendingInboundWriteId = writeId;
209
+ void stream.write(chunk)
210
+ .then(() => {
211
+ if (terminal)
212
+ return;
213
+ if (pendingInboundWriteId !== writeId) {
214
+ failProtocol("completed write does not match the pending chunk");
215
+ return;
216
+ }
217
+ pendingInboundWriteId = null;
218
+ port.postMessage({
219
+ type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
220
+ writeId,
221
+ });
222
+ })
223
+ .catch((error) => failBridge(error));
224
+ return;
225
+ }
226
+ case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE: {
227
+ if (!acceptingWrites)
228
+ return;
229
+ if (pendingInboundWriteId != null) {
230
+ failProtocol("stream closed before the pending chunk was acknowledged");
231
+ return;
232
+ }
233
+ acceptingWrites = false;
234
+ terminal = true;
235
+ const closeError = new Error("stream is closed");
236
+ pendingInboundWriteId = null;
237
+ pendingOutboundAcknowledgement?.reject(closeError);
238
+ pendingOutboundAcknowledgement = null;
239
+ void Promise.resolve(stream.close()).catch(() => {
240
+ // The peer already closed its bridge endpoint.
241
+ });
242
+ closePort();
243
+ notifyTerminal();
244
+ return;
245
+ }
246
+ case PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE: {
247
+ const writeId = data.writeId;
248
+ const pending = pendingOutboundAcknowledgement;
249
+ if (!validWriteId(writeId) || pending == null || pending.writeId !== writeId) {
250
+ failProtocol("unexpected stream write acknowledgement");
251
+ return;
252
+ }
253
+ pendingOutboundAcknowledgement = null;
254
+ pending.resolve();
255
+ return;
256
+ }
257
+ case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
258
+ const message = String(data.message ?? "stream reset");
259
+ failBridge(new Error(message), false);
260
+ return;
261
+ }
262
+ default:
263
+ return;
264
+ }
102
265
  };
266
+ port.start?.();
103
267
  void (async () => {
104
268
  try {
105
269
  const wsOpts = {
@@ -107,69 +271,15 @@ function bridgeWebSocket(runtime, msg, port) {
107
271
  ...(msg.protocols === undefined ? {} : { protocols: msg.protocols }),
108
272
  };
109
273
  const opened = await runtime.openWebSocketStream(msg.path, wsOpts);
274
+ if (terminal) {
275
+ await Promise.resolve(opened.stream.reset(terminalError ?? new Error("proxy controller Window bridge is closed")));
276
+ return;
277
+ }
110
278
  stream = opened.stream;
111
- port.onmessage = (ev) => {
112
- const data = ev.data;
113
- if (data == null || typeof data !== "object")
114
- return;
115
- const type = typeof data.type === "string" ? data.type : "";
116
- switch (type) {
117
- case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
118
- if (!acceptingWrites || stream == null)
119
- return;
120
- const raw = data.data;
121
- if (!(raw instanceof ArrayBuffer))
122
- return;
123
- if (pendingWriteBytes + raw.byteLength > maxBufferedBytes) {
124
- failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
125
- return;
126
- }
127
- const rawWriteId = data.writeId;
128
- const writeId = Number.isSafeInteger(rawWriteId) && Number(rawWriteId) > 0
129
- ? Number(rawWriteId)
130
- : undefined;
131
- const chunk = new Uint8Array(raw);
132
- pendingWriteBytes += chunk.byteLength;
133
- writeChain = writeChain
134
- .then(async () => {
135
- if (terminal || stream == null)
136
- throw new Error("stream is closed");
137
- await stream.write(chunk);
138
- if (terminal || writeId === undefined)
139
- return;
140
- port.postMessage({
141
- type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
142
- writeId,
143
- });
144
- })
145
- .catch((error) => failBridge(error))
146
- .finally(() => {
147
- pendingWriteBytes = Math.max(0, pendingWriteBytes - chunk.byteLength);
148
- });
149
- return;
150
- }
151
- case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE:
152
- if (!acceptingWrites || stream == null)
153
- return;
154
- acceptingWrites = false;
155
- writeChain = writeChain
156
- .then(() => stream?.close())
157
- .catch((error) => failBridge(error));
158
- return;
159
- case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
160
- const message = String(data.message ?? "stream reset");
161
- failBridge(new Error(message));
162
- return;
163
- }
164
- default:
165
- return;
166
- }
167
- };
168
- port.start?.();
169
279
  port.postMessage({
170
280
  type: PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE,
171
281
  protocol: opened.protocol,
172
- capabilities: [PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY],
282
+ capabilities: [PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY],
173
283
  });
174
284
  while (!terminal) {
175
285
  const chunk = await stream.read();
@@ -178,22 +288,36 @@ function bridgeWebSocket(runtime, msg, port) {
178
288
  acceptingWrites = false;
179
289
  port.postMessage({ type: PROXY_WINDOW_STREAM_END_MSG_TYPE });
180
290
  closePort();
291
+ notifyTerminal();
181
292
  return;
182
293
  }
183
- const ab = cloneChunk(chunk);
184
- port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab }, [ab]);
294
+ await postChunkAndWait(chunk);
185
295
  }
186
296
  }
187
297
  catch (error) {
298
+ if (terminal)
299
+ return;
188
300
  const message = error instanceof Error ? error.message : String(error);
189
301
  if (stream == null) {
190
- port.postMessage({ type: PROXY_WINDOW_WS_ERROR_MSG_TYPE, message });
191
- closePort();
302
+ terminal = true;
303
+ acceptingWrites = false;
304
+ terminalError = error instanceof Error ? error : new Error(message);
305
+ try {
306
+ port.postMessage({ type: PROXY_WINDOW_WS_ERROR_MSG_TYPE, message });
307
+ }
308
+ finally {
309
+ closePort();
310
+ notifyTerminal();
311
+ }
192
312
  return;
193
313
  }
194
314
  failBridge(error);
195
315
  }
196
316
  })();
317
+ return {
318
+ dispose: (error) => failBridge(error),
319
+ isTerminal: () => terminal,
320
+ };
197
321
  }
198
322
  export function registerProxyControllerWindow(opts) {
199
323
  const allowedOrigins = normalizeOrigins(opts.allowedOrigins);
@@ -202,6 +326,8 @@ export function registerProxyControllerWindow(opts) {
202
326
  }
203
327
  const capabilityNonce = normalizeCapabilityNonce(opts.capabilityNonce);
204
328
  requireBridgeCapability(opts.expectedSource, capabilityNonce);
329
+ const activeWebSocketBridges = new Set();
330
+ let disposed = false;
205
331
  const targetWindow = opts.targetWindow ?? globalThis.window;
206
332
  if (targetWindow == null) {
207
333
  throw new Error("targetWindow is not available");
@@ -227,9 +353,18 @@ export function registerProxyControllerWindow(opts) {
227
353
  opts.runtime.dispatchFetch(withTrustedExternalOrigin(msg.req, eventOrigin), port);
228
354
  return;
229
355
  }
230
- case PROXY_WINDOW_WS_OPEN_MSG_TYPE:
231
- bridgeWebSocket(opts.runtime, data, port);
356
+ case PROXY_WINDOW_WS_OPEN_MSG_TYPE: {
357
+ if (disposed)
358
+ return;
359
+ let bridge = null;
360
+ bridge = bridgeWebSocket(opts.runtime, data, port, () => {
361
+ if (bridge != null)
362
+ activeWebSocketBridges.delete(bridge);
363
+ });
364
+ if (!bridge.isTerminal())
365
+ activeWebSocketBridges.add(bridge);
232
366
  return;
367
+ }
233
368
  default:
234
369
  return;
235
370
  }
@@ -237,7 +372,13 @@ export function registerProxyControllerWindow(opts) {
237
372
  targetWindow.addEventListener("message", onMessage);
238
373
  return {
239
374
  dispose: () => {
375
+ if (disposed)
376
+ return;
377
+ disposed = true;
240
378
  targetWindow.removeEventListener("message", onMessage);
379
+ const error = new Error("proxy controller Window bridge is disposed");
380
+ for (const bridge of [...activeWebSocketBridges])
381
+ bridge.dispose(error);
241
382
  },
242
383
  };
243
384
  }
@@ -1,4 +1,3 @@
1
- import { profileToPresetManifest } from "./profiles.js";
2
1
  import { resolveProxyPreset } from "./preset.js";
3
2
  import { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.js";
4
3
  import { createProxyRuntime, ensureServiceWorkerRuntimeRegistered } from "./runtime.js";
@@ -165,13 +164,8 @@ function shouldIgnoreMismatch(plugins, ctx) {
165
164
  return false;
166
165
  }
167
166
  function resolveIntegrationPreset(opts) {
168
- if (opts.preset !== undefined && opts.profile !== undefined) {
169
- throw new Error("preset and deprecated profile cannot be used together");
170
- }
171
167
  if (opts.preset !== undefined)
172
168
  return resolveProxyPreset(opts.preset);
173
- if (opts.profile !== undefined)
174
- return resolveProxyPreset(profileToPresetManifest(opts.profile));
175
169
  return resolveProxyPreset();
176
170
  }
177
171
  function buildRuntimeOptions(preset, runtime) {