@floegence/flowersec-core 0.23.0 → 0.25.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,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,5 @@
1
1
  import type { YamuxStream } from "../yamux/stream.js";
2
2
  export declare function createMessagePortBackedStream(port: MessagePort, opts?: Readonly<{
3
- writeAcknowledgements?: boolean;
3
+ maxBufferedBytes?: number;
4
+ onTerminal?: () => void;
4
5
  }>): YamuxStream;