@floegence/flowersec-core 2.5.3 → 2.5.4

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.
@@ -9,6 +9,6 @@ type ReadExactlyFn = (n: number) => Promise<Uint8Array>;
9
9
  type ReadExactlyLike = Readonly<{
10
10
  readExactly: (n: number) => Promise<Uint8Array>;
11
11
  }>;
12
- export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown): Promise<void>;
12
+ export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown, maxBytes?: number): Promise<void>;
13
13
  export declare function readJsonFrame(readExactly: ReadExactlyFn | ReadExactlyLike, maxBytes: number): Promise<unknown>;
14
14
  export {};
@@ -13,8 +13,10 @@ function normalizeReadExactly(readExactly) {
13
13
  return typeof readExactly === "function" ? readExactly : (n) => readExactly.readExactly(n);
14
14
  }
15
15
  // writeJsonFrame encodes a JSON payload with a 4-byte length prefix.
16
- export async function writeJsonFrame(write, v) {
16
+ export async function writeJsonFrame(write, v, maxBytes = 0) {
17
17
  const json = te.encode(JSON.stringify(v));
18
+ if (maxBytes > 0 && json.length > maxBytes)
19
+ throw new JsonFramingError("frame too large");
18
20
  const hdr = u32be(json.length);
19
21
  const out = new Uint8Array(4 + json.length);
20
22
  out.set(hdr, 0);
@@ -1,5 +1,5 @@
1
1
  import { RpcRouter } from "../rpc/server.js";
2
- import { assertRpcError } from "../rpc/validate.js";
2
+ import { assertRpcError, assertRpcTypeId } from "../rpc/validate.js";
3
3
  import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
4
4
  import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
5
5
  import { startNodeWebSocketServer, } from "./webSocketServer.js";
@@ -85,10 +85,13 @@ function registerNotification(state, typeId, handler) {
85
85
  state.notifications.set(typeId, handler);
86
86
  }
87
87
  function validateRPCRegistration(typeId, handler) {
88
- if (!Number.isSafeInteger(typeId)
89
- || typeId < 1
90
- || typeId > 0xffff_ffff
91
- || typeof handler !== "function") {
88
+ try {
89
+ assertRpcTypeId(typeId);
90
+ }
91
+ catch {
92
+ throw new HandlerRegistrationError("invalid_handler");
93
+ }
94
+ if (typeof handler !== "function") {
92
95
  throw new HandlerRegistrationError("invalid_handler");
93
96
  }
94
97
  }
@@ -45,11 +45,34 @@ function normalizeTimeout(input) {
45
45
  }
46
46
  return input;
47
47
  }
48
+ class InvalidProxyPathError extends TypeError {
49
+ }
48
50
  function normalizePath(input) {
49
- if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://")) {
50
- throw new TypeError("proxy path must be an origin-relative path");
51
+ if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://") || input.includes("#")) {
52
+ throw new InvalidProxyPathError("proxy path must be an origin-relative path");
51
53
  }
52
- return input;
54
+ let parsed;
55
+ try {
56
+ parsed = new URL(input, "https://flowersec.invalid/");
57
+ }
58
+ catch {
59
+ throw new InvalidProxyPathError("proxy path must be an origin-relative path");
60
+ }
61
+ const pathname = normalizePercentEscapes(parsed.pathname, true).replace(/\/{2,}/gu, "/");
62
+ const search = normalizePercentEscapes(parsed.search, false);
63
+ return pathname + search;
64
+ }
65
+ function normalizePercentEscapes(input, rejectEncodedSeparators) {
66
+ if (/%(?![0-9a-f]{2})/iu.test(input))
67
+ throw new InvalidProxyPathError("proxy path contains invalid percent encoding");
68
+ return input.replace(/%([0-9a-f]{2})/giu, (_match, hex) => {
69
+ const value = Number.parseInt(hex, 16);
70
+ if (rejectEncodedSeparators && (value === 0x2f || value === 0x5c)) {
71
+ throw new InvalidProxyPathError("proxy path contains an encoded separator");
72
+ }
73
+ const character = String.fromCharCode(value);
74
+ return /[A-Za-z0-9\-._~]/u.test(character) ? character : `%${hex.toUpperCase()}`;
75
+ });
53
76
  }
54
77
  function pathName(path) {
55
78
  const query = path.indexOf("?");
@@ -176,11 +199,19 @@ class StreamAdmission {
176
199
  return Promise.reject(new SessionError("resource_exhausted"));
177
200
  }
178
201
  return new Promise((resolve, reject) => {
202
+ let cleaned = false;
203
+ const cleanup = () => {
204
+ if (cleaned)
205
+ return;
206
+ cleaned = true;
207
+ signal?.removeEventListener("abort", onAbort);
208
+ };
179
209
  const entry = {
180
210
  bytes,
181
211
  ...(signal === undefined ? {} : { signal }),
182
- resolve,
183
- reject: (error) => reject(error),
212
+ cleanup,
213
+ resolve: (release) => { cleanup(); resolve(release); },
214
+ reject: (error) => { cleanup(); reject(error); },
184
215
  };
185
216
  const onAbort = () => {
186
217
  const index = this.queue.indexOf(entry);
@@ -188,7 +219,7 @@ class StreamAdmission {
188
219
  return;
189
220
  this.queue.splice(index, 1);
190
221
  this.queuedBytes -= bytes;
191
- reject(new SessionError("canceled"));
222
+ entry.reject(new SessionError("canceled"));
192
223
  };
193
224
  signal?.addEventListener("abort", onAbort, { once: true });
194
225
  this.queue.push(entry);
@@ -251,6 +282,8 @@ async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
251
282
  function publicFailure(error) {
252
283
  if (error instanceof ProxyPolicyError)
253
284
  return { status: 403, code: "policy_denied", message: "proxy request denied" };
285
+ if (error instanceof InvalidProxyPathError)
286
+ return { status: 400, code: "invalid_request", message: "invalid proxy request" };
254
287
  if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
255
288
  return { status: 503, code: error.code, message: "proxy service unavailable" };
256
289
  }
@@ -15,6 +15,8 @@ export type ProxyServiceWorkerScriptOptions = Readonly<{
15
15
  sameOriginOnly?: boolean;
16
16
  maxRequestBodyBytes?: number;
17
17
  maxInjectHTMLBytes?: number;
18
+ responseMetadataTimeoutMs?: number;
19
+ responseBodyInactivityTimeoutMs?: number;
18
20
  passthrough?: ProxyServiceWorkerPassthroughOptions;
19
21
  proxyPathPrefix?: string;
20
22
  stripProxyPathPrefix?: boolean;
@@ -17,6 +17,12 @@ function bounded(name, value, fallback, maximum) {
17
17
  throw new TypeError(`${name} is invalid`);
18
18
  return result;
19
19
  }
20
+ function optionalBounded(name, value, maximum) {
21
+ const result = value ?? 0;
22
+ if (!Number.isSafeInteger(result) || result < 0 || result > maximum)
23
+ throw new TypeError(`${name} is invalid`);
24
+ return result;
25
+ }
20
26
  function token(name, value, fallback = "") {
21
27
  const result = value ?? fallback;
22
28
  if (result !== result.trim() || /[\u0000-\u0020\u007f]/u.test(result) || result.length > 512)
@@ -42,6 +48,8 @@ function normalizeOptions(options) {
42
48
  sameOriginOnly: options.sameOriginOnly ?? true,
43
49
  maxRequestBodyBytes: bounded("maxRequestBodyBytes", options.maxRequestBodyBytes, 64 * 1024 * 1024, 256 * 1024 * 1024),
44
50
  maxInjectHTMLBytes: bounded("maxInjectHTMLBytes", options.maxInjectHTMLBytes, 8 * 1024 * 1024, 32 * 1024 * 1024),
51
+ responseMetadataTimeoutMs: bounded("responseMetadataTimeoutMs", options.responseMetadataTimeoutMs, 10_000, 300_000),
52
+ responseBodyInactivityTimeoutMs: optionalBounded("responseBodyInactivityTimeoutMs", options.responseBodyInactivityTimeoutMs, 300_000),
45
53
  passthroughPaths: strings("passthrough.paths", options.passthrough?.paths),
46
54
  passthroughPrefixes: strings("passthrough.prefixes", options.passthrough?.prefixes),
47
55
  proxyPathPrefix,
@@ -127,6 +135,8 @@ function serviceWorkerMain(config) {
127
135
  return new Response("proxy runtime unavailable", { status: 503 });
128
136
  }
129
137
  let body;
138
+ if (request.signal.aborted)
139
+ return new Response("proxy request canceled", { status: 499 });
130
140
  if (request.method !== "GET" && request.method !== "HEAD") {
131
141
  const requestBody = await request.clone().arrayBuffer();
132
142
  if (requestBody.byteLength > config.maxRequestBodyBytes)
@@ -138,16 +148,54 @@ function serviceWorkerMain(config) {
138
148
  let metadata = null;
139
149
  let controller = null;
140
150
  let finished = false;
141
- const finishError = (status, message) => {
151
+ let remoteAbortSent = false;
152
+ let bodyInactivityTimer;
153
+ const abortRemote = () => {
154
+ if (remoteAbortSent)
155
+ return;
156
+ remoteAbortSent = true;
157
+ try {
158
+ channel.port1.postMessage({ type: "flowersec-proxy:abort" });
159
+ }
160
+ catch { /* Port already failed. */ }
161
+ };
162
+ const cleanup = () => {
163
+ clearTimeout(metadataTimer);
164
+ clearTimeout(bodyInactivityTimer);
165
+ clearInterval(runtimeWatchdog);
166
+ request.signal.removeEventListener("abort", requestAborted);
167
+ channel.port1.close();
168
+ };
169
+ const finishError = (status, message, cancelRemote = false) => {
142
170
  if (finished)
143
171
  return;
144
172
  finished = true;
173
+ if (cancelRemote)
174
+ abortRemote();
145
175
  if (controller !== null)
146
176
  controller.error(new Error(message));
147
177
  else
148
178
  resolve(new Response(message, { status }));
149
- channel.port1.close();
179
+ cleanup();
180
+ };
181
+ const requestAborted = () => finishError(499, "proxy request canceled", true);
182
+ const armBodyInactivityTimer = () => {
183
+ clearTimeout(bodyInactivityTimer);
184
+ if (config.responseBodyInactivityTimeoutMs === 0)
185
+ return;
186
+ bodyInactivityTimer = setTimeout(() => finishError(504, "proxy response body timed out", true), config.responseBodyInactivityTimeoutMs);
150
187
  };
188
+ const metadataTimer = setTimeout(() => finishError(504, "proxy response timed out", true), config.responseMetadataTimeoutMs);
189
+ const runtimeWatchdog = setInterval(() => {
190
+ void worker.clients.get(target.id).then((current) => {
191
+ if (current !== null || finished)
192
+ return;
193
+ if (config.windowTarget === "registered_runtime")
194
+ runtimeClientId = "";
195
+ finishError(503, "proxy runtime unavailable", true);
196
+ }).catch(() => finishError(503, "proxy runtime unavailable", true));
197
+ }, 250);
198
+ request.signal.addEventListener("abort", requestAborted, { once: true });
151
199
  channel.port1.onmessage = (message) => {
152
200
  const value = message.data;
153
201
  if (value === null || typeof value !== "object" || finished)
@@ -162,13 +210,21 @@ function serviceWorkerMain(config) {
162
210
  headers.append(entry.name, entry.value);
163
211
  }
164
212
  metadata = { status: value.status, headers };
213
+ clearTimeout(metadataTimer);
214
+ armBodyInactivityTimer();
165
215
  const stream = new ReadableStream({
166
216
  start(valueController) {
167
217
  controller = valueController;
168
218
  channel.port1.postMessage({ type: "flowersec-proxy:response_credit" });
169
219
  },
170
220
  pull() { channel.port1.postMessage({ type: "flowersec-proxy:response_credit" }); },
171
- cancel() { channel.port1.postMessage({ type: "flowersec-proxy:abort" }); channel.port1.close(); },
221
+ cancel() {
222
+ if (finished)
223
+ return;
224
+ finished = true;
225
+ abortRemote();
226
+ cleanup();
227
+ },
172
228
  });
173
229
  resolve(new Response(stream, { status: metadata.status, headers: metadata.headers }));
174
230
  return;
@@ -177,19 +233,20 @@ function serviceWorkerMain(config) {
177
233
  if (controller === null || !(value.data instanceof ArrayBuffer))
178
234
  return finishError(502, "invalid proxy response");
179
235
  controller.enqueue(new Uint8Array(value.data));
236
+ armBodyInactivityTimer();
180
237
  return;
181
238
  }
182
239
  if (value.type === "flowersec-proxy:response_end") {
183
240
  finished = true;
184
241
  controller?.close();
185
- channel.port1.close();
242
+ cleanup();
186
243
  return;
187
244
  }
188
245
  if (value.type === "flowersec-proxy:response_error") {
189
246
  finishError(Number.isInteger(value.status) ? value.status : 502, typeof value.message === "string" ? value.message : "proxy request failed");
190
247
  }
191
248
  };
192
- channel.port1.onmessageerror = () => finishError(502, "proxy request failed");
249
+ channel.port1.onmessageerror = () => finishError(502, "proxy request failed", true);
193
250
  const headers = Array.from(request.headers.entries()).map(([name, value]) => ({ name, value }));
194
251
  try {
195
252
  target.postMessage({
@@ -207,8 +264,7 @@ function serviceWorkerMain(config) {
207
264
  catch {
208
265
  if (config.windowTarget === "registered_runtime")
209
266
  runtimeClientId = "";
210
- channel.port1.close();
211
- resolve(new Response("proxy runtime unavailable", { status: 503 }));
267
+ finishError(503, "proxy runtime unavailable");
212
268
  }
213
269
  });
214
270
  const injection = config.injectHTML;
@@ -23,6 +23,7 @@ export declare class MessagePortByteStream implements ByteStream {
23
23
  private handle;
24
24
  private finish;
25
25
  private fail;
26
+ private settleWrite;
26
27
  }
27
28
  export type RegisterProxyAppWindowOptions = Readonly<{
28
29
  controllerOrigin: string;
@@ -39,12 +39,17 @@ export class MessagePortByteStream {
39
39
  if (this.terminalError !== undefined)
40
40
  throw this.terminalError;
41
41
  return await new Promise((resolve, reject) => {
42
- const waiter = { resolve, reject };
42
+ const cleanup = () => options.signal?.removeEventListener("abort", onAbort);
43
+ const waiter = {
44
+ resolve: (value) => { cleanup(); resolve(value); },
45
+ reject: (error) => { cleanup(); reject(error); },
46
+ };
43
47
  const onAbort = () => {
44
48
  const index = this.readWaiters.indexOf(waiter);
45
- if (index >= 0)
46
- this.readWaiters.splice(index, 1);
47
- reject(new SessionError("canceled"));
49
+ if (index < 0)
50
+ return;
51
+ this.readWaiters.splice(index, 1);
52
+ waiter.reject(new SessionError("canceled"));
48
53
  };
49
54
  options.signal?.addEventListener("abort", onAbort, { once: true });
50
55
  this.readWaiters.push(waiter);
@@ -66,17 +71,19 @@ export class MessagePortByteStream {
66
71
  const copy = data.slice();
67
72
  this.buffered += copy.length;
68
73
  await new Promise((resolve, reject) => {
74
+ const onAbort = () => this.settleWrite(id, new SessionError("canceled"));
69
75
  this.writeWaiters.set(id, {
70
- resolve: () => { this.buffered -= copy.length; resolve(); },
71
- reject: (error) => { this.buffered -= copy.length; reject(error); },
76
+ bytes: copy.length,
77
+ ...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
78
+ resolve,
79
+ reject,
72
80
  });
81
+ options.signal?.addEventListener("abort", onAbort, { once: true });
73
82
  try {
74
83
  this.port.postMessage({ type: "chunk", id, data: copy.buffer }, [copy.buffer]);
75
84
  }
76
85
  catch {
77
- this.writeWaiters.delete(id);
78
- this.buffered -= copy.length;
79
- reject(new SessionError("operation_failed"));
86
+ this.settleWrite(id, new SessionError("operation_failed"));
80
87
  }
81
88
  });
82
89
  return data.length;
@@ -130,11 +137,7 @@ export class MessagePortByteStream {
130
137
  return;
131
138
  }
132
139
  if (message.type === "ack" && Number.isSafeInteger(message.id)) {
133
- const waiter = this.writeWaiters.get(message.id);
134
- if (waiter !== undefined) {
135
- this.writeWaiters.delete(message.id);
136
- waiter.resolve();
137
- }
140
+ this.settleWrite(message.id);
138
141
  return;
139
142
  }
140
143
  if (message.type === "end") {
@@ -159,9 +162,8 @@ export class MessagePortByteStream {
159
162
  this.readBuffered = 0;
160
163
  for (const waiter of this.readWaiters.splice(0))
161
164
  waiter.resolve(null);
162
- for (const waiter of this.writeWaiters.values())
163
- waiter.reject(new SessionError("closed"));
164
- this.writeWaiters.clear();
165
+ for (const id of [...this.writeWaiters.keys()])
166
+ this.settleWrite(id, new SessionError("closed"));
165
167
  this.port.close();
166
168
  }
167
169
  fail(error) {
@@ -173,11 +175,22 @@ export class MessagePortByteStream {
173
175
  this.readBuffered = 0;
174
176
  for (const waiter of this.readWaiters.splice(0))
175
177
  waiter.reject(error);
176
- for (const waiter of this.writeWaiters.values())
177
- waiter.reject(error);
178
- this.writeWaiters.clear();
178
+ for (const id of [...this.writeWaiters.keys()])
179
+ this.settleWrite(id, error);
179
180
  this.port.close();
180
181
  }
182
+ settleWrite(id, error) {
183
+ const waiter = this.writeWaiters.get(id);
184
+ if (waiter === undefined)
185
+ return;
186
+ this.writeWaiters.delete(id);
187
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
188
+ this.buffered = Math.max(0, this.buffered - waiter.bytes);
189
+ if (error === undefined)
190
+ waiter.resolve();
191
+ else
192
+ waiter.reject(error);
193
+ }
181
194
  }
182
195
  function bridgeLimits(maxWsFrameBytes, maxWsBufferedAmountBytes) {
183
196
  const wsFrame = maxWsFrameBytes ?? SDK_DEFAULTS.proxy.maxWsFrameBytes;
@@ -279,33 +292,65 @@ export function registerProxyAppWindow(options) {
279
292
  dispose: () => { disposed = true; },
280
293
  });
281
294
  }
282
- async function bridgeStreams(runtimeStream, port) {
295
+ async function bridgeStreams(runtimeStream, port, signal) {
283
296
  const bridge = new MessagePortByteStream(port);
297
+ const controller = new AbortController();
298
+ let resetTask;
299
+ const resetBoth = () => {
300
+ resetTask ??= Promise.allSettled([
301
+ Promise.resolve().then(async () => await runtimeStream.reset()),
302
+ Promise.resolve().then(async () => await bridge.reset()),
303
+ ]);
304
+ return resetTask;
305
+ };
306
+ const abort = () => {
307
+ controller.abort(signal?.reason ?? new SessionError("canceled"));
308
+ void resetBoth();
309
+ };
310
+ if (signal?.aborted === true)
311
+ abort();
312
+ else
313
+ signal?.addEventListener("abort", abort, { once: true });
284
314
  const left = (async () => {
285
315
  while (true) {
286
- const chunk = await runtimeStream.read();
316
+ const chunk = await runtimeStream.read({ signal: controller.signal });
287
317
  if (chunk === null) {
288
318
  await bridge.closeWrite();
289
319
  return;
290
320
  }
291
- await bridge.write(chunk);
321
+ await bridge.write(chunk, { signal: controller.signal });
292
322
  }
293
- })();
323
+ })().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
294
324
  const right = (async () => {
295
325
  while (true) {
296
- const chunk = await bridge.read();
326
+ const chunk = await bridge.read({ signal: controller.signal });
297
327
  if (chunk === null) {
298
328
  await runtimeStream.closeWrite();
299
329
  return;
300
330
  }
301
331
  let offset = 0;
302
332
  while (offset < chunk.length)
303
- offset += await runtimeStream.write(chunk.subarray(offset));
333
+ offset += await runtimeStream.write(chunk.subarray(offset), { signal: controller.signal });
304
334
  }
305
- })();
306
- await Promise.all([left, right]);
307
- await runtimeStream.close();
308
- await bridge.close();
335
+ })().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
336
+ try {
337
+ const settled = await Promise.allSettled([left, right]);
338
+ const failure = settled.find((result) => result.status === "rejected");
339
+ if (failure !== undefined) {
340
+ await resetBoth();
341
+ throw failure.reason;
342
+ }
343
+ try {
344
+ await Promise.all([runtimeStream.close(), bridge.close()]);
345
+ }
346
+ catch (error) {
347
+ await resetBoth();
348
+ throw error;
349
+ }
350
+ }
351
+ finally {
352
+ signal?.removeEventListener("abort", abort);
353
+ }
309
354
  }
310
355
  export function registerProxyControllerWindow(options) {
311
356
  const target = options.targetWindow ?? globalThis.window;
@@ -315,6 +360,7 @@ export function registerProxyControllerWindow(options) {
315
360
  }
316
361
  const nonce = capability(options.capabilityNonce);
317
362
  let disposed = false;
363
+ const active = new Set();
318
364
  const onMessage = (event) => {
319
365
  if (disposed || !allowed.has(event.origin) || (options.expectedSource !== undefined && event.source !== options.expectedSource))
320
366
  return;
@@ -331,6 +377,7 @@ export function registerProxyControllerWindow(options) {
331
377
  void (async () => {
332
378
  let canceled = false;
333
379
  const openController = new AbortController();
380
+ active.add(openController);
334
381
  port.onmessage = (message) => {
335
382
  if (message.data?.type === "reset" ||
336
383
  message.data?.type === "close") {
@@ -349,17 +396,30 @@ export function registerProxyControllerWindow(options) {
349
396
  return;
350
397
  }
351
398
  port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: true, protocol: opened.protocol });
352
- await bridgeStreams(opened.stream, port);
399
+ await bridgeStreams(opened.stream, port, openController.signal);
353
400
  }
354
401
  catch {
355
- port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
402
+ try {
403
+ port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
404
+ }
405
+ catch { /* Port is already closed. */ }
356
406
  port.close();
357
407
  }
408
+ finally {
409
+ active.delete(openController);
410
+ }
358
411
  })();
359
412
  }
360
413
  };
361
414
  target.addEventListener("message", onMessage);
362
- return Object.freeze({ dispose: () => { disposed = true; target.removeEventListener("message", onMessage); } });
415
+ return Object.freeze({
416
+ dispose: () => {
417
+ disposed = true;
418
+ target.removeEventListener("message", onMessage);
419
+ for (const controller of active)
420
+ controller.abort(new SessionError("closed"));
421
+ },
422
+ });
363
423
  }
364
424
  export async function registerProxyAppWindowWithServiceWorkerControl(options) {
365
425
  await registerServiceWorkerAndEnsureControl({
@@ -4,6 +4,7 @@ export type WebSocketPatchOptions = Readonly<{
4
4
  shouldProxy?: (url: URL) => boolean;
5
5
  maxWsFrameBytes?: number;
6
6
  maxWsBufferedAmountBytes?: number;
7
+ closeHandshakeTimeoutMs?: number;
7
8
  }>;
8
9
  export declare function installWebSocketPatch(options: WebSocketPatchOptions): Readonly<{
9
10
  uninstall(): void;
@@ -64,6 +64,7 @@ export function installWebSocketPatch(options) {
64
64
  const runtimeLimits = options.runtime.limits;
65
65
  const maxFrameBytes = limit("maxWsFrameBytes", options.maxWsFrameBytes, runtimeLimits.maxWsFrameBytes ?? 1024 * 1024);
66
66
  const maxBufferedBytes = limit("maxWsBufferedAmountBytes", options.maxWsBufferedAmountBytes, runtimeLimits.maxWsBufferedAmountBytes ?? 4 * 1024 * 1024);
67
+ const closeHandshakeTimeoutMs = limit("closeHandshakeTimeoutMs", options.closeHandshakeTimeoutMs, 5_000);
67
68
  const shouldProxy = options.shouldProxy ?? ((url) => {
68
69
  const location = globalThis.location;
69
70
  if (location?.hostname === undefined || location.hostname === "")
@@ -95,6 +96,7 @@ export function installWebSocketPatch(options) {
95
96
  abort = new AbortController();
96
97
  stream;
97
98
  writes = Promise.resolve();
99
+ closeTimer;
98
100
  constructor(input, protocols) {
99
101
  const url = new URL(String(input), globalThis.location?.href);
100
102
  if (!shouldProxy(url))
@@ -152,12 +154,17 @@ export function installWebSocketPatch(options) {
152
154
  const reasonBytes = encoder.encode(reason);
153
155
  if (reasonBytes.length > 123)
154
156
  throw new DOMException("WebSocket close reason is too long", "SyntaxError");
157
+ if (this.readyState === ProxyWebSocket.CONNECTING || this.stream === undefined) {
158
+ this.fail();
159
+ return;
160
+ }
155
161
  this.readyState = ProxyWebSocket.CLOSING;
156
162
  const payload = code === undefined ? new Uint8Array() : new Uint8Array([...u16be(code), ...reasonBytes]);
157
- this.writes = this.writes.then(async () => {
158
- if (this.stream !== undefined)
159
- await writeFrame(this.stream, 8, payload, maxFrameBytes);
160
- }).catch(() => undefined).finally(() => this.abort.abort());
163
+ const stream = this.stream;
164
+ this.closeTimer = setTimeout(() => this.fail(), closeHandshakeTimeoutMs);
165
+ this.writes = this.writes
166
+ .then(async () => await writeFrame(stream, 8, payload, maxFrameBytes))
167
+ .catch(() => this.fail());
161
168
  }
162
169
  async connect(url, protocols) {
163
170
  try {
@@ -183,10 +190,19 @@ export function installWebSocketPatch(options) {
183
190
  this.writes = this.writes.then(async () => await writeFrame(stream, 10, frame.payload, maxFrameBytes)).catch(() => this.fail());
184
191
  }
185
192
  else if (frame.opcode === 8) {
193
+ if (frame.payload.length === 1)
194
+ throw new Error("invalid WebSocket close frame");
186
195
  const code = frame.payload.length >= 2 ? readU16(frame.payload) : 1000;
187
196
  const reason = frame.payload.length > 2 ? decoder.decode(frame.payload.subarray(2)) : "";
188
- this.readyState = ProxyWebSocket.CLOSED;
189
- this.emit("close", new CloseEvent("close", { code, reason, wasClean: true }));
197
+ const peerInitiated = this.readyState === ProxyWebSocket.OPEN;
198
+ this.readyState = ProxyWebSocket.CLOSING;
199
+ if (this.closeTimer !== undefined)
200
+ clearTimeout(this.closeTimer);
201
+ if (peerInitiated) {
202
+ this.writes = this.writes.then(async () => await writeFrame(stream, 8, frame.payload, maxFrameBytes));
203
+ }
204
+ await this.writes;
205
+ this.completeClose(code, reason);
190
206
  return;
191
207
  }
192
208
  else if (frame.opcode === 1) {
@@ -211,12 +227,29 @@ export function installWebSocketPatch(options) {
211
227
  fail() {
212
228
  if (this.readyState === ProxyWebSocket.CLOSED)
213
229
  return;
230
+ if (this.closeTimer !== undefined)
231
+ clearTimeout(this.closeTimer);
214
232
  this.readyState = ProxyWebSocket.CLOSED;
215
233
  this.bufferedAmount = 0;
216
234
  this.emit("error", new Event("error"));
217
235
  this.emit("close", new CloseEvent("close", { code: 1006, reason: "proxy WebSocket failed", wasClean: false }));
236
+ const stream = this.stream;
237
+ this.stream = undefined;
238
+ this.abort.abort();
239
+ void stream?.reset().catch(() => undefined);
240
+ }
241
+ completeClose(code, reason) {
242
+ if (this.readyState === ProxyWebSocket.CLOSED)
243
+ return;
244
+ if (this.closeTimer !== undefined)
245
+ clearTimeout(this.closeTimer);
246
+ this.readyState = ProxyWebSocket.CLOSED;
247
+ this.bufferedAmount = 0;
248
+ this.emit("close", new CloseEvent("close", { code, reason, wasClean: true }));
249
+ const stream = this.stream;
218
250
  this.stream = undefined;
219
251
  this.abort.abort();
252
+ void stream?.close().catch(() => stream.reset().catch(() => undefined));
220
253
  }
221
254
  }
222
255
  globalThis.WebSocket = ProxyWebSocket;
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
- import { assertRpcEnvelope } from "./validate.js";
2
+ import { assertRpcEnvelope, assertRpcTypeId } from "./validate.js";
3
3
  // Guard against precision loss when encoding request IDs as numbers.
4
4
  const MAX_SAFE_REQUEST_ID = BigInt(Number.MAX_SAFE_INTEGER);
5
5
  // RpcClient sends request/response envelopes and dispatches notifications.
@@ -25,12 +25,13 @@ export class RpcClient {
25
25
  async call(typeId, payload, signal) {
26
26
  if (this.closed)
27
27
  throw new Error("rpc client closed");
28
+ const validatedTypeId = assertRpcTypeId(typeId);
28
29
  if (this.nextId > MAX_SAFE_REQUEST_ID)
29
30
  throw new Error("request id overflow");
30
31
  const requestId = this.nextId;
31
32
  this.nextId += 1n;
32
33
  const env = {
33
- type_id: typeId >>> 0,
34
+ type_id: validatedTypeId,
34
35
  request_id: Number(requestId),
35
36
  response_to: 0,
36
37
  payload
@@ -39,7 +40,7 @@ export class RpcClient {
39
40
  this.pending.set(requestId, { resolve, reject });
40
41
  });
41
42
  try {
42
- await writeJsonFrame(this.write, env);
43
+ await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
43
44
  }
44
45
  catch (e) {
45
46
  this.pending.delete(requestId);
@@ -71,7 +72,7 @@ export class RpcClient {
71
72
  }
72
73
  // onNotify registers a handler for incoming notifications.
73
74
  onNotify(typeId, handler) {
74
- const tid = typeId >>> 0;
75
+ const tid = assertRpcTypeId(typeId);
75
76
  const set = this.notifyHandlers.get(tid) ?? new Set();
76
77
  set.add(handler);
77
78
  this.notifyHandlers.set(tid, set);
@@ -86,13 +87,14 @@ export class RpcClient {
86
87
  async notify(typeId, payload) {
87
88
  if (this.closed)
88
89
  throw new Error("rpc client closed");
90
+ const validatedTypeId = assertRpcTypeId(typeId);
89
91
  const env = {
90
- type_id: typeId >>> 0,
92
+ type_id: validatedTypeId,
91
93
  request_id: 0,
92
94
  response_to: 0,
93
95
  payload
94
96
  };
95
- await writeJsonFrame(this.write, env);
97
+ await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
96
98
  }
97
99
  async readLoop() {
98
100
  try {
@@ -101,7 +103,7 @@ export class RpcClient {
101
103
  if (v.response_to === 0) {
102
104
  // Notification: response_to=0 and request_id=0.
103
105
  if (v.request_id === 0) {
104
- const set = this.notifyHandlers.get(v.type_id >>> 0);
106
+ const set = this.notifyHandlers.get(v.type_id);
105
107
  if (set != null) {
106
108
  for (const h of set) {
107
109
  try {
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
- import { assertRpcEnvelope, assertRpcError } from "./validate.js";
2
+ import { assertRpcEnvelope, assertRpcError, assertRpcTypeId } from "./validate.js";
3
3
  import { SDK_DEFAULTS } from "../defaults.js";
4
4
  const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
5
5
  maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
@@ -10,13 +10,13 @@ export class RpcRouter {
10
10
  handlers = new Map();
11
11
  notifyHandlers = new Map();
12
12
  register(typeId, handler) {
13
- this.handlers.set(typeId >>> 0, handler);
13
+ this.handlers.set(assertRpcTypeId(typeId), handler);
14
14
  }
15
15
  handler(typeId) {
16
- return this.handlers.get(typeId >>> 0);
16
+ return this.handlers.get(assertRpcTypeId(typeId));
17
17
  }
18
18
  onNotify(typeId, handler) {
19
- const normalized = typeId >>> 0;
19
+ const normalized = assertRpcTypeId(typeId);
20
20
  const handlers = this.notifyHandlers.get(normalized) ?? new Set();
21
21
  handlers.add(handler);
22
22
  this.notifyHandlers.set(normalized, handlers);
@@ -27,7 +27,7 @@ export class RpcRouter {
27
27
  };
28
28
  }
29
29
  async dispatchNotification(typeId, payload) {
30
- const normalized = typeId >>> 0;
30
+ const normalized = assertRpcTypeId(typeId);
31
31
  const requestHandler = this.handlers.get(normalized);
32
32
  if (requestHandler !== undefined)
33
33
  await requestHandler(payload);
@@ -76,7 +76,7 @@ export class RpcServer {
76
76
  if (this.closed)
77
77
  throw new Error("rpc server closed");
78
78
  await this.writeEnvelope({
79
- type_id: typeId >>> 0,
79
+ type_id: assertRpcTypeId(typeId),
80
80
  request_id: 0,
81
81
  response_to: 0,
82
82
  payload,
@@ -93,6 +93,7 @@ export class RpcServer {
93
93
  return await new Promise(() => undefined);
94
94
  }));
95
95
  let failure;
96
+ const aborted = abortPromise(signal);
96
97
  try {
97
98
  while (!this.closed) {
98
99
  if (signal?.aborted)
@@ -101,6 +102,7 @@ export class RpcServer {
101
102
  readJsonFrame(this.transport.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES),
102
103
  this.terminalSignal.then((error) => { throw error; }),
103
104
  workerFailure,
105
+ ...(aborted === undefined ? [] : [aborted.promise]),
104
106
  ]);
105
107
  const v = assertRpcEnvelope(next);
106
108
  if (v.response_to !== 0)
@@ -126,6 +128,9 @@ export class RpcServer {
126
128
  failure = err;
127
129
  this.terminalError = err;
128
130
  }
131
+ finally {
132
+ aborted?.cleanup();
133
+ }
129
134
  let closeError;
130
135
  try {
131
136
  this.close(failure ?? this.terminalError ?? new Error("rpc server closed"));
@@ -173,19 +178,23 @@ export class RpcServer {
173
178
  if (work == null)
174
179
  return;
175
180
  const v = work.envelope;
176
- const h = this.router.handler(v.type_id);
177
- let out;
178
- if (h == null)
179
- out = { payload: null, error: { code: 404, message: "handler not found" } };
180
- else {
181
- try {
182
- out = await h(v.payload);
183
- }
184
- catch {
185
- out = { payload: null, error: { code: 500, message: "internal error" } };
186
- }
187
- }
188
181
  try {
182
+ const h = this.router.handler(v.type_id);
183
+ let out;
184
+ if (h == null)
185
+ out = { payload: null, error: { code: 404, message: "handler not found" } };
186
+ else {
187
+ const outcome = await Promise.race([
188
+ Promise.resolve().then(() => h(v.payload)).then((value) => ({ kind: "completed", value }), () => ({
189
+ kind: "completed",
190
+ value: { payload: null, error: { code: 500, message: "internal error" } },
191
+ })),
192
+ this.terminalSignal.then(() => ({ kind: "terminated" })),
193
+ ]);
194
+ if (outcome.kind === "terminated")
195
+ return;
196
+ out = outcome.value;
197
+ }
189
198
  if (this.closed)
190
199
  return;
191
200
  await this.writeResponse(v, out);
@@ -201,7 +210,12 @@ export class RpcServer {
201
210
  if (work == null)
202
211
  return;
203
212
  const v = work.envelope;
204
- await this.router.dispatchNotification(v.type_id, v.payload);
213
+ const completed = await Promise.race([
214
+ Promise.resolve().then(() => this.router.dispatchNotification(v.type_id, v.payload)).then(() => true),
215
+ this.terminalSignal.then(() => false),
216
+ ]);
217
+ if (!completed)
218
+ return;
205
219
  }
206
220
  }
207
221
  async nextWork(queue, waiters) {
@@ -236,7 +250,7 @@ export class RpcServer {
236
250
  await this.writeEnvelope(resp);
237
251
  }
238
252
  async writeEnvelope(envelope) {
239
- const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope));
253
+ const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope, DEFAULT_MAX_JSON_FRAME_BYTES));
240
254
  this.writeChain = write;
241
255
  try {
242
256
  await write;
@@ -247,6 +261,16 @@ export class RpcServer {
247
261
  }
248
262
  }
249
263
  }
264
+ function abortPromise(signal) {
265
+ if (signal === undefined || signal.aborted)
266
+ return undefined;
267
+ let onAbort;
268
+ const promise = new Promise((_resolve, reject) => {
269
+ onAbort = () => reject(signal.reason ?? new Error("aborted"));
270
+ signal.addEventListener("abort", onAbort, { once: true });
271
+ });
272
+ return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
273
+ }
250
274
  function positiveInteger(value, name) {
251
275
  if (!Number.isSafeInteger(value) || value <= 0)
252
276
  throw new RangeError(`${name} must be a positive integer`);
@@ -1,3 +1,4 @@
1
1
  import type { RpcEnvelope, RpcError } from "./wire.js";
2
2
  export declare function assertRpcEnvelope(v: unknown): RpcEnvelope;
3
+ export declare function assertRpcTypeId(value: unknown): number;
3
4
  export declare function assertRpcError(value: unknown): RpcError;
@@ -6,21 +6,33 @@ const strictDecoder = new TextDecoder("utf-8", { fatal: true });
6
6
  // The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
7
7
  // to avoid silent precision loss on request/response correlation.
8
8
  export function assertRpcEnvelope(v) {
9
- if (typeof v !== "object" || v == null)
9
+ if (typeof v !== "object" || v == null || Array.isArray(v))
10
10
  throw new Error("bad rpc envelope");
11
11
  const o = v;
12
- if (!isSafeU32Number(o.type_id))
13
- throw new Error("bad rpc envelope: type_id");
12
+ const keys = Object.keys(o);
13
+ if (keys.some((key) => key !== "type_id" && key !== "request_id" && key !== "response_to" && key !== "payload" && key !== "error")) {
14
+ throw new Error("bad rpc envelope: shape");
15
+ }
16
+ if (!Object.prototype.hasOwnProperty.call(o, "payload"))
17
+ throw new Error("bad rpc envelope: payload");
18
+ assertRpcTypeId(o.type_id);
14
19
  if (!isSafeU64Number(o.request_id))
15
20
  throw new Error("bad rpc envelope: request_id");
16
21
  if (!isSafeU64Number(o.response_to))
17
22
  throw new Error("bad rpc envelope: response_to");
18
- // payload: unknown (JSON)
19
- if (o.error != null) {
23
+ if (o.request_id !== 0 && o.response_to !== 0)
24
+ throw new Error("bad rpc envelope: request/response shape");
25
+ if (o.error != null && o.response_to === 0)
26
+ throw new Error("bad rpc envelope: error shape");
27
+ if (o.error != null)
20
28
  assertRpcError(o.error);
21
- }
22
29
  return o;
23
30
  }
31
+ export function assertRpcTypeId(value) {
32
+ if (!isSafeU32Number(value) || value === 0)
33
+ throw new RangeError("RPC typeId must be a non-zero u32 integer");
34
+ return value;
35
+ }
24
36
  export function assertRpcError(value) {
25
37
  if (typeof value !== "object" || value == null)
26
38
  throw new Error("bad rpc envelope: error");
@@ -1,5 +1,6 @@
1
1
  import { SessionError } from "./contract.js";
2
2
  import { createStreamMetadataV2, streamMetadataValuesV2 } from "./streamMetadata.js";
3
+ import { assertRpcTypeId } from "../rpc/validate.js";
3
4
  /** @internal */
4
5
  export function projectSessionV2(session) {
5
6
  const notificationOwner = {
@@ -132,6 +133,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
132
133
  return Object.freeze({
133
134
  async call(typeId, payload, decodeResponse, options) {
134
135
  try {
136
+ assertRpcTypeId(typeId);
135
137
  assertJsonValue(payload);
136
138
  const result = await peer.call(typeId, payload, options?.signal);
137
139
  if (result.error !== undefined) {
@@ -146,6 +148,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
146
148
  },
147
149
  async notify(typeId, payload, options) {
148
150
  try {
151
+ assertRpcTypeId(typeId);
149
152
  assertJsonValue(payload);
150
153
  if (options?.signal?.aborted)
151
154
  throw options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
@@ -156,6 +159,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
156
159
  }
157
160
  },
158
161
  onNotify(typeId, decodePayload, handler) {
162
+ assertRpcTypeId(typeId);
159
163
  if (notificationOwner.closed)
160
164
  return () => undefined;
161
165
  const unsubscribe = peer.onNotify(typeId, (payload) => {
@@ -1224,6 +1224,10 @@ class EncryptedStreamV2 {
1224
1224
  if (this.terminalError !== undefined)
1225
1225
  return false;
1226
1226
  this.terminalError = error;
1227
+ const pendingSendRekey = this.pendingSendRekey;
1228
+ this.pendingSendRekey = undefined;
1229
+ pendingSendRekey?.armed.reject(error);
1230
+ pendingSendRekey?.done.reject(error);
1227
1231
  this.opened.reject(error);
1228
1232
  this.data.fail(error);
1229
1233
  return true;
@@ -12,6 +12,8 @@ export declare class YamuxStream {
12
12
  private readWaiters;
13
13
  private error;
14
14
  private resetTask;
15
+ private closeTask;
16
+ private closeRequested;
15
17
  private writeChain;
16
18
  private writeQueueBytes;
17
19
  private finalized;
@@ -23,6 +25,7 @@ export declare class YamuxStream {
23
25
  write(data: Uint8Array): Promise<void>;
24
26
  private writeSerial;
25
27
  close(): Promise<void>;
28
+ private closeSerial;
26
29
  reset(err?: Error): Promise<void>;
27
30
  abort(err?: Error): void;
28
31
  private fail;
@@ -24,6 +24,8 @@ export class YamuxStream {
24
24
  // Terminal error (reset/overflow) for the stream.
25
25
  error = null;
26
26
  resetTask;
27
+ closeTask;
28
+ closeRequested = false;
27
29
  writeChain = Promise.resolve();
28
30
  writeQueueBytes = 0;
29
31
  finalized = false;
@@ -89,6 +91,8 @@ export class YamuxStream {
89
91
  }
90
92
  // write sends DATA frames, respecting the send window.
91
93
  async write(data) {
94
+ if (this.closeRequested)
95
+ throw new Error("stream closed");
92
96
  this.ensureWritable();
93
97
  const byteCount = data.byteLength;
94
98
  const nextQueueBytes = this.writeQueueBytes + byteCount;
@@ -129,10 +133,19 @@ export class YamuxStream {
129
133
  }
130
134
  }
131
135
  // close sends FIN and transitions to local close.
132
- async close() {
133
- if (this.state === "closed")
134
- return;
135
- if (this.state === "reset")
136
+ close() {
137
+ if (this.closeTask !== undefined)
138
+ return this.closeTask;
139
+ if (this.state === "closed" || this.state === "reset" || this.state === "localClose")
140
+ return Promise.resolve();
141
+ this.closeRequested = true;
142
+ const task = this.writeChain.then(async () => await this.closeSerial());
143
+ this.writeChain = task.catch(() => undefined);
144
+ this.closeTask = task;
145
+ return task;
146
+ }
147
+ async closeSerial() {
148
+ if (this.state === "closed" || this.state === "reset" || this.state === "localClose")
136
149
  return;
137
150
  const wasRemoteClose = this.state === "remoteClose";
138
151
  const flags = this.sendFlags() | FLAG_FIN;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
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.3"
80
+ "@floegence/flowersec-node-native": "2.5.4"
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": "aa50aab509e3519b2b9c8e49abccc407a831a741"
98
+ "flowersecSourceCommit": "026cb52d116d2a04de50d0f0621fff57c7657120"
99
99
  }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:ec0f2db1-aea5-5835-89bb-52b08c4a4a1c",
4
+ "serialNumber": "urn:uuid:9db05d70-0170-5669-8eb4-881e06b18415",
5
5
  "version": 1,
6
6
  "metadata": {
7
7
  "component": {
8
8
  "type": "library",
9
9
  "name": "@floegence/flowersec-core",
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"
10
+ "version": "2.5.4",
11
+ "purl": "pkg:npm/%40floegence/flowersec-core@2.5.4",
12
+ "bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.4"
13
13
  },
14
14
  "properties": [
15
15
  {
16
16
  "name": "flowersec:source-inventory-sha256",
17
- "value": "1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb"
17
+ "value": "308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8"
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.3",
193
+ "ref": "pkg:npm/%40floegence/flowersec-core@2.5.4",
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/1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb",
6
+ "documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8",
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-290762592d648256929a",
17
- "versionInfo": "2.5.3",
16
+ "SPDXID": "SPDXRef-Package-972ad62e2707c407a361",
17
+ "versionInfo": "2.5.4",
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: 1a8ba3adc252c3c72e7a5129a172b0c767a075161ac1d0379f0b257f091816fb",
23
+ "comment": "Flowersec source inventory SHA-256: 308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8",
24
24
  "externalRefs": [
25
25
  {
26
26
  "referenceCategory": "PACKAGE-MANAGER",
27
27
  "referenceType": "purl",
28
- "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.3"
28
+ "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.4"
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-290762592d648256929a"
139
+ "relatedSpdxElement": "SPDXRef-Package-972ad62e2707c407a361"
140
140
  },
141
141
  {
142
- "spdxElementId": "SPDXRef-Package-290762592d648256929a",
142
+ "spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
143
143
  "relationshipType": "DEPENDS_ON",
144
144
  "relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
145
145
  },
146
146
  {
147
- "spdxElementId": "SPDXRef-Package-290762592d648256929a",
147
+ "spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
148
148
  "relationshipType": "DEPENDS_ON",
149
149
  "relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
150
150
  },
151
151
  {
152
- "spdxElementId": "SPDXRef-Package-290762592d648256929a",
152
+ "spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
153
153
  "relationshipType": "DEPENDS_ON",
154
154
  "relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
155
155
  },
156
156
  {
157
- "spdxElementId": "SPDXRef-Package-290762592d648256929a",
157
+ "spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
158
158
  "relationshipType": "DEPENDS_ON",
159
159
  "relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
160
160
  },
161
161
  {
162
- "spdxElementId": "SPDXRef-Package-290762592d648256929a",
162
+ "spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
163
163
  "relationshipType": "DEPENDS_ON",
164
164
  "relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
165
165
  },