@floegence/flowersec-core 2.5.2 → 2.5.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.
@@ -443,12 +443,31 @@ export async function createAcceptor(options) {
443
443
  const abort = () => controller.abort(operation.signal?.reason);
444
444
  operation.signal?.addEventListener("abort", abort, { once: true });
445
445
  try {
446
- return await Promise.any(listeners.map(async (listener, index) => {
447
- const selected = listeners[(index + cursor) % listeners.length];
448
- return await selected.accept({ signal: controller.signal });
449
- })).finally(() => {
450
- cursor = (cursor + 1) % listeners.length;
451
- controller.abort();
446
+ return await new Promise((resolve, reject) => {
447
+ let settled = false;
448
+ let remaining = listeners.length;
449
+ const errors = [];
450
+ listeners.forEach((listener, index) => {
451
+ const selected = listeners[(index + cursor) % listeners.length];
452
+ void selected.accept({ signal: controller.signal }).then((carrier) => {
453
+ if (!settled) {
454
+ settled = true;
455
+ cursor = (cursor + 1) % listeners.length;
456
+ controller.abort();
457
+ resolve(carrier);
458
+ }
459
+ else {
460
+ carrier.abort({ code: 1000, reason: "accept race lost" });
461
+ }
462
+ }, (error) => {
463
+ errors.push(error);
464
+ remaining -= 1;
465
+ if (!settled && remaining === 0) {
466
+ settled = true;
467
+ reject(new AggregateError(errors, "all listeners failed to accept"));
468
+ }
469
+ });
470
+ });
452
471
  });
453
472
  }
454
473
  finally {
@@ -12,6 +12,7 @@ export declare class MessagePortByteStream implements ByteStream {
12
12
  private buffered;
13
13
  private readBuffered;
14
14
  private ended;
15
+ private writeClosed;
15
16
  private closed;
16
17
  constructor(port: MessagePort);
17
18
  read(options?: OperationOptions): Promise<Uint8Array | null>;
@@ -18,6 +18,7 @@ export class MessagePortByteStream {
18
18
  buffered = 0;
19
19
  readBuffered = 0;
20
20
  ended = false;
21
+ writeClosed = false;
21
22
  closed = false;
22
23
  constructor(port) {
23
24
  this.port = port;
@@ -52,6 +53,8 @@ export class MessagePortByteStream {
52
53
  async write(data, options = {}) {
53
54
  if (this.closed || this.terminalError !== undefined)
54
55
  throw this.terminalError ?? new SessionError("closed");
56
+ if (this.writeClosed)
57
+ throw new SessionError("operation_failed");
55
58
  if (options.signal?.aborted === true)
56
59
  throw new SessionError("canceled");
57
60
  if (data.length === 0)
@@ -79,8 +82,9 @@ export class MessagePortByteStream {
79
82
  return data.length;
80
83
  }
81
84
  async closeWrite() {
82
- if (this.closed)
85
+ if (this.closed || this.writeClosed)
83
86
  return;
87
+ this.writeClosed = true;
84
88
  this.port.postMessage({ type: "end" });
85
89
  }
86
90
  async reset() {
@@ -100,6 +104,10 @@ export class MessagePortByteStream {
100
104
  return;
101
105
  const message = value;
102
106
  if (message.type === "chunk") {
107
+ if (this.ended || this.closed) {
108
+ this.fail(new SessionError("operation_failed"));
109
+ return;
110
+ }
103
111
  if (!Number.isSafeInteger(message.id) || !(message.data instanceof ArrayBuffer) || message.data.byteLength > MAX_BRIDGE_CHUNK_BYTES) {
104
112
  this.fail(new SessionError("operation_failed"));
105
113
  return;
@@ -130,6 +138,8 @@ export class MessagePortByteStream {
130
138
  return;
131
139
  }
132
140
  if (message.type === "end") {
141
+ if (this.ended)
142
+ return;
133
143
  this.ended = true;
134
144
  for (const waiter of this.readWaiters.splice(0))
135
145
  waiter.resolve(null);
@@ -219,20 +229,39 @@ export function registerProxyAppWindow(options) {
219
229
  openWebSocketStream: async (path, openOptions = {}) => {
220
230
  if (disposed)
221
231
  throw new SessionError("closed");
232
+ if (openOptions.signal?.aborted === true)
233
+ throw new SessionError("canceled");
222
234
  const channel = new MessageChannel();
223
235
  const response = new Promise((resolve, reject) => {
224
- const timer = setTimeout(() => { channel.port1.close(); reject(new SessionError("timeout")); }, 10_000);
236
+ let settled = false;
237
+ const finish = (error, opened) => {
238
+ if (settled)
239
+ return;
240
+ settled = true;
241
+ clearTimeout(timer);
242
+ openOptions.signal?.removeEventListener("abort", abort);
243
+ if (error !== undefined) {
244
+ channel.port1.close();
245
+ reject(error);
246
+ }
247
+ else
248
+ resolve(opened);
249
+ };
250
+ const abort = () => {
251
+ channel.port1.postMessage({ type: "reset" });
252
+ finish(new SessionError("canceled"));
253
+ };
254
+ const timer = setTimeout(() => finish(new SessionError("timeout")), 10_000);
225
255
  channel.port1.onmessage = (event) => {
226
256
  if (event.data?.type !== WEBSOCKET_ACK_MESSAGE)
227
257
  return;
228
- clearTimeout(timer);
229
258
  if (event.data.ok !== true) {
230
- channel.port1.close();
231
- reject(new SessionError("operation_failed"));
259
+ finish(new SessionError("operation_failed"));
232
260
  return;
233
261
  }
234
- resolve(Object.freeze({ stream: new MessagePortByteStream(channel.port1), protocol: typeof event.data.protocol === "string" ? event.data.protocol : "" }));
262
+ finish(undefined, Object.freeze({ stream: new MessagePortByteStream(channel.port1), protocol: typeof event.data.protocol === "string" ? event.data.protocol : "" }));
235
263
  };
264
+ openOptions.signal?.addEventListener("abort", abort, { once: true });
236
265
  });
237
266
  controller.postMessage({
238
267
  type: WEBSOCKET_OPEN_MESSAGE,
@@ -241,9 +270,6 @@ export function registerProxyAppWindow(options) {
241
270
  protocols: openOptions.protocols ?? [],
242
271
  ...(nonce === undefined ? {} : { capabilityNonce: nonce }),
243
272
  }, origin, [channel.port2]);
244
- if (openOptions.signal !== undefined) {
245
- openOptions.signal.addEventListener("abort", () => channel.port1.close(), { once: true });
246
- }
247
273
  return await response;
248
274
  },
249
275
  dispose: () => { disposed = true; },
@@ -303,10 +329,25 @@ export function registerProxyControllerWindow(options) {
303
329
  }
304
330
  if (event.data?.type === WEBSOCKET_OPEN_MESSAGE && event.data.version === 2) {
305
331
  void (async () => {
332
+ let canceled = false;
333
+ const openController = new AbortController();
334
+ port.onmessage = (message) => {
335
+ if (message.data?.type === "reset" ||
336
+ message.data?.type === "close") {
337
+ canceled = true;
338
+ openController.abort();
339
+ port.close();
340
+ }
341
+ };
306
342
  try {
307
343
  const opened = await options.runtime.openWebSocketStream(String(event.data.path ?? ""), {
308
344
  protocols: Array.isArray(event.data.protocols) ? event.data.protocols.map(String) : [],
345
+ signal: openController.signal,
309
346
  });
347
+ if (canceled) {
348
+ await opened.stream.reset();
349
+ return;
350
+ }
310
351
  port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: true, protocol: opened.protocol });
311
352
  await bridgeStreams(opened.stream, port);
312
353
  }
@@ -327,6 +327,7 @@ class WebTransportDatagramAdapter {
327
327
  this.terminalError = error;
328
328
  this.incoming.fail(error);
329
329
  void this.writable.abort(error).catch(() => undefined);
330
+ void this.readable.cancel(error).catch(() => undefined);
330
331
  }
331
332
  assertOpen() {
332
333
  if (this.terminalError !== undefined)
@@ -416,15 +417,26 @@ class IncomingWebTransportStreamQueue {
416
417
  }
417
418
  }
418
419
  class IncomingDatagramQueue {
420
+ static MAX_VALUES = 256;
421
+ static MAX_BYTES = 4 * 1024 * 1024;
419
422
  values = [];
423
+ bufferedBytes = 0;
420
424
  waiters = new Set();
421
425
  terminalError;
422
426
  push(value) {
423
427
  if (this.terminalError !== undefined)
424
428
  return;
425
429
  const waiter = this.waiters.values().next().value;
426
- if (waiter === undefined)
430
+ if (waiter === undefined) {
431
+ // DATAGRAMs are lossy by contract. Drop an incoming value once the
432
+ // bounded queue budget is exhausted, while keeping the native reader
433
+ // drained so an unconsumed peer cannot grow memory without bound.
434
+ if (this.values.length >= IncomingDatagramQueue.MAX_VALUES ||
435
+ this.bufferedBytes + value.byteLength > IncomingDatagramQueue.MAX_BYTES)
436
+ return;
427
437
  this.values.push(value);
438
+ this.bufferedBytes += value.byteLength;
439
+ }
428
440
  else
429
441
  waiter.deliver(value);
430
442
  }
@@ -434,8 +446,10 @@ class IncomingDatagramQueue {
434
446
  if (this.terminalError !== undefined)
435
447
  return Promise.reject(this.terminalError);
436
448
  const value = this.values.shift();
437
- if (value !== undefined)
449
+ if (value !== undefined) {
450
+ this.bufferedBytes -= value.byteLength;
438
451
  return Promise.resolve(value);
452
+ }
439
453
  return new Promise((resolve, reject) => {
440
454
  let settled = false;
441
455
  const cleanup = () => {
@@ -470,6 +484,7 @@ class IncomingDatagramQueue {
470
484
  return;
471
485
  this.terminalError = error;
472
486
  this.values.length = 0;
487
+ this.bufferedBytes = 0;
473
488
  for (const waiter of [...this.waiters])
474
489
  waiter.fail(error);
475
490
  }
@@ -132,7 +132,7 @@ export declare class SessionV2 implements SessionV2Contract {
132
132
  private peerResponderFrozen;
133
133
  private responderChanged;
134
134
  private idleWatchdogStarted;
135
- private idleTimer;
135
+ private idleTimerCancel;
136
136
  private readonly terminationState;
137
137
  private readonly rpcRouter;
138
138
  constructor(carrier: CarrierSessionV2, control: CarrierStreamV2, controlReader: ExactReader, config: SessionConfigV2, material: HandshakeMaterial);
@@ -137,7 +137,7 @@ export class SessionV2 {
137
137
  peerResponderFrozen = false;
138
138
  responderChanged = deferred();
139
139
  idleWatchdogStarted = false;
140
- idleTimer;
140
+ idleTimerCancel;
141
141
  terminationState = deferred();
142
142
  rpcRouter;
143
143
  constructor(carrier, control, controlReader, config, material) {
@@ -1017,10 +1017,9 @@ export class SessionV2 {
1017
1017
  const timeoutMs = sessionIdleTimeoutMs(this.config);
1018
1018
  if (timeoutMs === 0)
1019
1019
  return;
1020
- if (this.idleTimer !== undefined)
1021
- clearTimeout(this.idleTimer);
1022
- this.idleTimer = setTimeout(() => {
1023
- this.idleTimer = undefined;
1020
+ this.idleTimerCancel?.();
1021
+ this.idleTimerCancel = scheduleLongTimeout(() => {
1022
+ this.idleTimerCancel = undefined;
1024
1023
  this.fail(new SessionV2Error("timeout", "Flowersec v2 session idle timeout exceeded"));
1025
1024
  }, timeoutMs);
1026
1025
  }
@@ -1034,10 +1033,8 @@ export class SessionV2 {
1034
1033
  this.terminalError = error;
1035
1034
  this.lifecycle = "closed";
1036
1035
  this.terminationState.resolve({ error });
1037
- if (this.idleTimer !== undefined) {
1038
- clearTimeout(this.idleTimer);
1039
- this.idleTimer = undefined;
1040
- }
1036
+ this.idleTimerCancel?.();
1037
+ this.idleTimerCancel = undefined;
1041
1038
  this.incoming.fail(error);
1042
1039
  this.outboundPermits.fail(error);
1043
1040
  this.inboundPermits.fail(error);
@@ -1988,12 +1985,38 @@ function createSessionDeadline(config, phase) {
1988
1985
  }
1989
1986
  function defaultDeadlineFactory(timeoutMs, phase) {
1990
1987
  const controller = new AbortController();
1991
- const timer = setTimeout(() => {
1988
+ const cancelTimer = scheduleLongTimeout(() => {
1992
1989
  controller.abort(new SessionV2Error("timeout", `${phase} deadline exceeded`));
1993
1990
  }, timeoutMs);
1994
1991
  return {
1995
1992
  signal: controller.signal,
1996
- cancel: () => clearTimeout(timer),
1993
+ cancel: cancelTimer,
1994
+ };
1995
+ }
1996
+ const MAX_NATIVE_TIMEOUT_MS = 2_147_483_647;
1997
+ // Node and browsers clamp setTimeout delays above a signed 32-bit integer.
1998
+ // Keep the public timeout range intact by scheduling the remaining duration in
1999
+ // bounded segments against a monotonic wall-clock deadline.
2000
+ function scheduleLongTimeout(callback, timeoutMs) {
2001
+ const clock = () => typeof performance !== "undefined" ? performance.now() : Date.now();
2002
+ const deadline = clock() + timeoutMs;
2003
+ let timer;
2004
+ let canceled = false;
2005
+ const schedule = () => {
2006
+ if (canceled)
2007
+ return;
2008
+ const remaining = deadline - clock();
2009
+ if (remaining <= 0) {
2010
+ callback();
2011
+ return;
2012
+ }
2013
+ timer = setTimeout(schedule, Math.min(remaining, MAX_NATIVE_TIMEOUT_MS));
2014
+ };
2015
+ schedule();
2016
+ return () => {
2017
+ canceled = true;
2018
+ if (timer !== undefined)
2019
+ clearTimeout(timer);
1997
2020
  };
1998
2021
  }
1999
2022
  function combineSignals(...signals) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "Flowersec core TypeScript library for carrier-neutral encrypted sessions and multiplexed streams.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,7 @@
77
77
  "ws": "^8.21.2"
78
78
  },
79
79
  "optionalDependencies": {
80
- "@floegence/flowersec-node-native": "2.5.2"
80
+ "@floegence/flowersec-node-native": "2.5.3"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@playwright/test": "1.62.1",
@@ -95,5 +95,5 @@
95
95
  "vite": "^8.2.1",
96
96
  "vitest": "4.1.10"
97
97
  },
98
- "flowersecSourceCommit": "2aca169b79bc65a1e1ff51ca5356d0ed5dcbf18a"
98
+ "flowersecSourceCommit": "aa50aab509e3519b2b9c8e49abccc407a831a741"
99
99
  }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:3a83166e-0cec-5e55-814a-79ffaf005d24",
4
+ "serialNumber": "urn:uuid:ec0f2db1-aea5-5835-89bb-52b08c4a4a1c",
5
5
  "version": 1,
6
6
  "metadata": {
7
7
  "component": {
8
8
  "type": "library",
9
9
  "name": "@floegence/flowersec-core",
10
- "version": "2.5.2",
11
- "purl": "pkg:npm/%40floegence/flowersec-core@2.5.2",
12
- "bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.2"
10
+ "version": "2.5.3",
11
+ "purl": "pkg:npm/%40floegence/flowersec-core@2.5.3",
12
+ "bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.3"
13
13
  },
14
14
  "properties": [
15
15
  {
16
16
  "name": "flowersec:source-inventory-sha256",
17
- "value": "8d179ba65d4598bdeb169b3dbcc8732a8e0aa8fc724f228eacbb117ad49ce149"
17
+ "value": "1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb"
18
18
  }
19
19
  ]
20
20
  },
@@ -190,7 +190,7 @@
190
190
  ],
191
191
  "dependencies": [
192
192
  {
193
- "ref": "pkg:npm/%40floegence/flowersec-core@2.5.2",
193
+ "ref": "pkg:npm/%40floegence/flowersec-core@2.5.3",
194
194
  "dependsOn": [
195
195
  "pkg:npm/%40noble/ciphers@2.3.0",
196
196
  "pkg:npm/%40noble/curves@2.3.0",
package/sbom/spdx.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
5
  "name": "flowersec-ts",
6
- "documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/8d179ba65d4598bdeb169b3dbcc8732a8e0aa8fc724f228eacbb117ad49ce149",
6
+ "documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb",
7
7
  "creationInfo": {
8
8
  "created": "1970-01-01T00:00:00Z",
9
9
  "creators": [
@@ -13,19 +13,19 @@
13
13
  "packages": [
14
14
  {
15
15
  "name": "@floegence/flowersec-core",
16
- "SPDXID": "SPDXRef-Package-75953903056055f5a6bd",
17
- "versionInfo": "2.5.2",
16
+ "SPDXID": "SPDXRef-Package-290762592d648256929a",
17
+ "versionInfo": "2.5.3",
18
18
  "downloadLocation": "NOASSERTION",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "NOASSERTION",
21
21
  "licenseDeclared": "NOASSERTION",
22
22
  "copyrightText": "NOASSERTION",
23
- "comment": "Flowersec source inventory SHA-256: 8d179ba65d4598bdeb169b3dbcc8732a8e0aa8fc724f228eacbb117ad49ce149",
23
+ "comment": "Flowersec source inventory SHA-256: 1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb",
24
24
  "externalRefs": [
25
25
  {
26
26
  "referenceCategory": "PACKAGE-MANAGER",
27
27
  "referenceType": "purl",
28
- "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.2"
28
+ "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.3"
29
29
  }
30
30
  ]
31
31
  },
@@ -136,30 +136,30 @@
136
136
  {
137
137
  "spdxElementId": "SPDXRef-DOCUMENT",
138
138
  "relationshipType": "DESCRIBES",
139
- "relatedSpdxElement": "SPDXRef-Package-75953903056055f5a6bd"
139
+ "relatedSpdxElement": "SPDXRef-Package-290762592d648256929a"
140
140
  },
141
141
  {
142
- "spdxElementId": "SPDXRef-Package-75953903056055f5a6bd",
142
+ "spdxElementId": "SPDXRef-Package-290762592d648256929a",
143
143
  "relationshipType": "DEPENDS_ON",
144
144
  "relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
145
145
  },
146
146
  {
147
- "spdxElementId": "SPDXRef-Package-75953903056055f5a6bd",
147
+ "spdxElementId": "SPDXRef-Package-290762592d648256929a",
148
148
  "relationshipType": "DEPENDS_ON",
149
149
  "relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
150
150
  },
151
151
  {
152
- "spdxElementId": "SPDXRef-Package-75953903056055f5a6bd",
152
+ "spdxElementId": "SPDXRef-Package-290762592d648256929a",
153
153
  "relationshipType": "DEPENDS_ON",
154
154
  "relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
155
155
  },
156
156
  {
157
- "spdxElementId": "SPDXRef-Package-75953903056055f5a6bd",
157
+ "spdxElementId": "SPDXRef-Package-290762592d648256929a",
158
158
  "relationshipType": "DEPENDS_ON",
159
159
  "relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
160
160
  },
161
161
  {
162
- "spdxElementId": "SPDXRef-Package-75953903056055f5a6bd",
162
+ "spdxElementId": "SPDXRef-Package-290762592d648256929a",
163
163
  "relationshipType": "DEPENDS_ON",
164
164
  "relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
165
165
  },