@floegence/flowersec-core 0.24.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.
@@ -7,6 +7,7 @@ import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_B
7
7
  import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders, isSafeHeaderValue, isValidHeaderName, normalizeHeaderName } from "./headerPolicy.js";
8
8
  export async function serveProxySession(session, options, signal) {
9
9
  const compiled = compileOptions(options);
10
+ const requestBodyBudget = createRequestBodyBudget(compiled.maxBufferedRequestBodyBytes);
10
11
  const active = new Set();
11
12
  try {
12
13
  while (!signal?.aborted) {
@@ -30,7 +31,7 @@ export async function serveProxySession(session, options, signal) {
30
31
  await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
31
32
  continue;
32
33
  }
33
- const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
34
+ const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, requestBodyBudget, signal)
34
35
  .catch(() => { })
35
36
  .finally(() => active.delete(task));
36
37
  active.add(task);
@@ -50,12 +51,13 @@ async function serveRPCStream(stream, router, options, signal) {
50
51
  await server.serve(signal);
51
52
  }
52
53
  export function serveProxyStream(kind, stream, options, signal) {
53
- return serveProxyStreamCompiled(kind, stream, compileOptions(options), signal);
54
+ const compiled = compileOptions(options);
55
+ return serveProxyStreamCompiled(kind, stream, compiled, createRequestBodyBudget(compiled.maxBufferedRequestBodyBytes), signal);
54
56
  }
55
- async function serveProxyStreamCompiled(kind, stream, options, signal) {
57
+ async function serveProxyStreamCompiled(kind, stream, options, requestBodyBudget, signal) {
56
58
  try {
57
59
  if (kind === PROXY_KIND_HTTP1)
58
- await serveHTTP(stream, options, signal);
60
+ await serveHTTP(stream, options, requestBodyBudget, signal);
59
61
  else
60
62
  await serveWebSocket(stream, options, signal);
61
63
  }
@@ -66,79 +68,102 @@ async function serveProxyStreamCompiled(kind, stream, options, signal) {
66
68
  catch { /* The peer may already have reset the stream. */ }
67
69
  }
68
70
  }
69
- async function serveHTTP(stream, options, signal) {
71
+ async function serveHTTP(stream, options, requestBodyBudget, signal) {
70
72
  const reader = createByteReader(stream, signal === undefined ? {} : { signal });
71
73
  let requestId = "unknown";
72
74
  try {
73
75
  const meta = assertHTTPRequestMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
74
76
  requestId = meta.request_id;
75
77
  const path = parsePath(meta.path);
76
- const body = await readBody(reader, options.maxChunkBytes, options.maxBodyBytes);
77
- const headers = requestHeaders(meta.headers, options);
78
- applyExternalOrigin(headers, meta.external_origin);
79
- const target = new URL(options.upstream);
80
- target.pathname = path.pathname;
81
- target.search = path.search;
82
- target.hash = "";
83
- const timeoutMs = resolveTimeout(meta.timeout_ms, options);
84
- const controller = new AbortController();
85
- const onAbort = () => controller.abort(signal?.reason);
86
- signal?.addEventListener("abort", onAbort, { once: true });
87
- const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(new Error("upstream request timeout")), timeoutMs) : undefined;
78
+ const bufferedBody = await readBody(reader, options.maxChunkBytes, options.maxBodyBytes, requestBodyBudget);
88
79
  try {
89
- const method = meta.method.trim().toUpperCase();
90
- const response = await options.fetch(target, {
91
- method,
92
- headers,
93
- redirect: "manual",
94
- signal: controller.signal,
95
- ...((method === "GET" || method === "HEAD") ? {} : { body: body.slice().buffer }),
96
- });
97
- const responseHeaders = collectResponseHeaders(response.headers, options);
98
- const responseMeta = {
99
- v: PROXY_PROTOCOL_VERSION,
100
- request_id: requestId,
101
- ok: true,
102
- status: response.status,
103
- headers: responseHeaders,
104
- };
105
- await writeJsonFrame(stream, responseMeta);
106
- if (response.body == null) {
80
+ if (signal?.aborted)
81
+ throw new ProxyServerError("canceled", "proxy request canceled");
82
+ const headers = requestHeaders(meta.headers, options);
83
+ applyExternalOrigin(headers, meta.external_origin);
84
+ const target = new URL(options.upstream);
85
+ target.pathname = path.pathname;
86
+ target.search = path.search;
87
+ target.hash = "";
88
+ const timeoutMs = resolveTimeout(meta.timeout_ms, options);
89
+ const controller = new AbortController();
90
+ const onAbort = () => controller.abort(new ProxyServerError("canceled", "proxy request canceled"));
91
+ signal?.addEventListener("abort", onAbort, { once: true });
92
+ if (signal?.aborted)
93
+ onAbort();
94
+ const timer = timeoutMs > 0
95
+ ? setTimeout(() => controller.abort(new ProxyServerError("timeout", "upstream request timeout")), timeoutMs)
96
+ : undefined;
97
+ try {
98
+ const method = meta.method.trim().toUpperCase();
99
+ let response;
100
+ try {
101
+ if (controller.signal.aborted)
102
+ throw controller.signal.reason;
103
+ response = await options.fetch(target, {
104
+ method,
105
+ headers,
106
+ redirect: "manual",
107
+ signal: controller.signal,
108
+ ...((method === "GET" || method === "HEAD")
109
+ ? {}
110
+ : { body: bufferedBody.bytes.buffer }),
111
+ });
112
+ }
113
+ finally {
114
+ bufferedBody.release();
115
+ }
116
+ const responseHeaders = collectResponseHeaders(response.headers, options);
117
+ const responseMeta = {
118
+ v: PROXY_PROTOCOL_VERSION,
119
+ request_id: requestId,
120
+ ok: true,
121
+ status: response.status,
122
+ headers: responseHeaders,
123
+ };
124
+ await writeJsonFrame(stream, responseMeta);
125
+ if (response.body == null) {
126
+ await stream.write(u32be(0));
127
+ return;
128
+ }
129
+ const bodyReader = response.body.getReader();
130
+ let total = 0;
131
+ while (true) {
132
+ const next = await bodyReader.read();
133
+ if (next.done)
134
+ break;
135
+ const bytes = next.value;
136
+ total += bytes.length;
137
+ if (total > options.maxBodyBytes)
138
+ throw new ProxyServerError("response_body_too_large", "response body too large");
139
+ for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
140
+ const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
141
+ await stream.write(u32be(chunk.length));
142
+ await stream.write(chunk);
143
+ }
144
+ }
107
145
  await stream.write(u32be(0));
108
- return;
109
146
  }
110
- const bodyReader = response.body.getReader();
111
- let total = 0;
112
- while (true) {
113
- const next = await bodyReader.read();
114
- if (next.done)
115
- break;
116
- const bytes = next.value;
117
- total += bytes.length;
118
- if (total > options.maxBodyBytes)
119
- throw new ProxyServerError("response_body_too_large", "response body too large");
120
- for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
121
- const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
122
- await stream.write(u32be(chunk.length));
123
- await stream.write(chunk);
124
- }
147
+ finally {
148
+ if (timer != null)
149
+ clearTimeout(timer);
150
+ signal?.removeEventListener("abort", onAbort);
125
151
  }
126
- await stream.write(u32be(0));
127
152
  }
128
153
  finally {
129
- if (timer != null)
130
- clearTimeout(timer);
131
- signal?.removeEventListener("abort", onAbort);
154
+ bufferedBody.release();
132
155
  }
133
156
  }
134
157
  catch (error) {
135
- await writeHTTPError(stream, requestId, classifyHTTPError(error), error);
158
+ await writeHTTPError(stream, requestId, classifyHTTPError(error));
136
159
  }
137
160
  }
138
161
  async function serveWebSocket(stream, options, signal) {
139
162
  const reader = createByteReader(stream, signal === undefined ? {} : { signal });
140
163
  let connId = "unknown";
141
164
  let raw;
165
+ let upstreamOpened = false;
166
+ let removePostOpenAbortListener;
142
167
  try {
143
168
  const meta = assertWSOpenMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
144
169
  connId = meta.conn_id;
@@ -164,55 +189,137 @@ async function serveWebSocket(stream, options, signal) {
164
189
  headers,
165
190
  maxPayload: options.maxWsFrameBytes,
166
191
  perMessageDeflate: false,
167
- handshakeTimeout: Math.min(options.defaultTimeoutMs, options.maxTimeoutMs),
192
+ handshakeTimeout: resolveTimeout(undefined, options),
168
193
  });
169
- await waitForUpstreamOpen(raw, signal);
170
- const response = {
171
- v: PROXY_PROTOCOL_VERSION,
172
- conn_id: connId,
173
- ok: true,
174
- protocol: String(raw.protocol ?? ""),
175
- };
176
- await writeJsonFrame(stream, response);
177
194
  let writeChain = Promise.resolve();
195
+ let queuedWriteBytes = 0;
196
+ let terminalSettled = false;
197
+ let terminalError;
178
198
  let terminalResolve;
179
199
  const terminal = new Promise((resolve) => { terminalResolve = resolve; });
180
- const writeFrame = (op, payload) => {
181
- const write = writeChain.then(() => writeWSFrame(stream, op, payload, options.maxWsFrameBytes));
182
- writeChain = write.catch(() => { });
183
- void write.catch((error) => terminalResolve(asError(error)));
200
+ const settleTerminal = (error) => {
201
+ if (terminalSettled)
202
+ return;
203
+ terminalSettled = true;
204
+ terminalError = error;
205
+ terminalResolve(error);
206
+ };
207
+ const queueFrame = (op, payload, allowCloseFrame = false) => {
208
+ if (terminalSettled)
209
+ return Promise.reject(new Error("proxy WebSocket is closed"));
210
+ const frameBytes = payload.byteLength + 5;
211
+ const nextQueuedWriteBytes = queuedWriteBytes + frameBytes;
212
+ if (!Number.isSafeInteger(nextQueuedWriteBytes)
213
+ || (!allowCloseFrame && nextQueuedWriteBytes > options.maxWsQueuedBytes)) {
214
+ const error = new ProxyServerError("resource_exhausted", "proxy WebSocket write queue exhausted");
215
+ settleTerminal(error);
216
+ try {
217
+ raw.close(1011, "proxy buffer limit exceeded");
218
+ }
219
+ catch { /* Best effort. */ }
220
+ return Promise.reject(error);
221
+ }
222
+ queuedWriteBytes = nextQueuedWriteBytes;
223
+ const write = writeChain
224
+ .then(() => {
225
+ if (terminalSettled)
226
+ throw terminalError ?? new Error("proxy WebSocket is closed");
227
+ return writeWSFrame(stream, op, payload, options.maxWsFrameBytes);
228
+ })
229
+ .finally(() => {
230
+ queuedWriteBytes = Math.max(0, queuedWriteBytes - frameBytes);
231
+ });
232
+ writeChain = write;
233
+ void write.catch((error) => settleTerminal(asError(error)));
234
+ return write;
235
+ };
236
+ const queueEventFrame = (op, payload) => {
237
+ void queueFrame(op, payload).catch(() => { });
184
238
  };
185
- raw.on("message", (data, isBinary) => writeFrame(isBinary ? 2 : 1, toBytes(data)));
186
- raw.on("ping", (data) => writeFrame(9, toBytes(data)));
187
- raw.on("pong", (data) => writeFrame(10, toBytes(data)));
188
239
  raw.on("close", (code, reason) => {
240
+ if (terminalSettled)
241
+ return;
242
+ if (!upstreamOpened) {
243
+ settleTerminal(new Error("upstream WebSocket closed before open"));
244
+ return;
245
+ }
189
246
  const reasonBytes = toBytes(reason);
190
247
  const payload = new Uint8Array(2 + reasonBytes.length);
191
248
  payload[0] = (code >>> 8) & 0xff;
192
249
  payload[1] = code & 0xff;
193
250
  payload.set(reasonBytes, 2);
194
- writeFrame(8, payload);
195
- terminalResolve();
251
+ void queueFrame(8, payload, true).then(() => settleTerminal(), (error) => settleTerminal(asError(error)));
196
252
  });
197
- raw.on("error", (error) => terminalResolve(error));
253
+ raw.on("error", (error) => settleTerminal(error));
254
+ await waitForUpstreamOpen(raw, signal);
255
+ upstreamOpened = true;
256
+ const response = {
257
+ v: PROXY_PROTOCOL_VERSION,
258
+ conn_id: connId,
259
+ ok: true,
260
+ protocol: String(raw.protocol ?? ""),
261
+ };
262
+ const openResponseWrite = writeJsonFrame(stream, response);
263
+ writeChain = openResponseWrite;
264
+ void openResponseWrite.catch((error) => settleTerminal(asError(error)));
265
+ raw.on("message", (data, isBinary) => queueEventFrame(isBinary ? 2 : 1, toBytes(data)));
266
+ raw.on("ping", (data) => queueEventFrame(9, toBytes(data)));
267
+ raw.on("pong", (data) => queueEventFrame(10, toBytes(data)));
268
+ const onPostOpenAbort = () => {
269
+ const error = asError(signal?.reason ?? new Error("proxy WebSocket aborted"));
270
+ settleTerminal(error);
271
+ try {
272
+ if (typeof raw.terminate === "function")
273
+ raw.terminate();
274
+ else
275
+ raw.close();
276
+ }
277
+ catch {
278
+ // The terminal error is already authoritative.
279
+ }
280
+ };
281
+ if (signal != null) {
282
+ signal.addEventListener("abort", onPostOpenAbort, { once: true });
283
+ removePostOpenAbortListener = () => signal.removeEventListener("abort", onPostOpenAbort);
284
+ if (signal.aborted)
285
+ onPostOpenAbort();
286
+ }
287
+ const openResponseResult = await Promise.race([
288
+ openResponseWrite.then(() => undefined),
289
+ terminal,
290
+ ]);
291
+ if (openResponseResult != null)
292
+ throw openResponseResult;
293
+ try {
294
+ if (!terminalSettled && typeof raw.resume === "function")
295
+ raw.resume();
296
+ }
297
+ catch (error) {
298
+ settleTerminal(asError(error));
299
+ throw error;
300
+ }
198
301
  const inbound = (async () => {
199
302
  while (true) {
200
303
  const frame = await readWSFrame(reader, options.maxWsFrameBytes);
201
304
  switch (frame.op) {
202
305
  case 1:
203
- raw.send(frame.payload, { binary: false });
306
+ await sendUpstreamFrame(raw, 1, frame.payload);
204
307
  break;
205
308
  case 2:
206
- raw.send(frame.payload, { binary: true });
309
+ await sendUpstreamFrame(raw, 2, frame.payload);
207
310
  break;
208
- case 8:
311
+ case 8: {
209
312
  raw.close(frame.payload.length >= 2 ? (frame.payload[0] << 8) | frame.payload[1] : 1000, new TextDecoder().decode(frame.payload.subarray(2)));
313
+ const closeError = await terminal;
314
+ if (closeError != null)
315
+ throw closeError;
210
316
  return;
317
+ }
211
318
  case 9:
212
- raw.ping(frame.payload);
319
+ await sendUpstreamFrame(raw, 9, frame.payload);
213
320
  break;
214
321
  case 10:
215
- raw.pong(frame.payload);
322
+ await sendUpstreamFrame(raw, 10, frame.payload);
216
323
  break;
217
324
  default: throw new Error("invalid websocket frame operation");
218
325
  }
@@ -224,10 +331,11 @@ async function serveWebSocket(stream, options, signal) {
224
331
  await writeChain;
225
332
  }
226
333
  catch (error) {
227
- if (raw == null || raw.readyState !== 1)
228
- await writeWSOpenError(stream, connId, classifyWSError(error), error);
334
+ if (!upstreamOpened)
335
+ await writeWSOpenError(stream, connId, classifyWSError(error));
229
336
  }
230
337
  finally {
338
+ removePostOpenAbortListener?.();
231
339
  try {
232
340
  raw?.close();
233
341
  }
@@ -246,7 +354,14 @@ function compileOptions(input) {
246
354
  const maxJsonFrameBytes = positive(input.maxJsonFrameBytes, DEFAULT_MAX_JSON_FRAME_BYTES, "maxJsonFrameBytes");
247
355
  const maxChunkBytes = positive(input.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES, "maxChunkBytes");
248
356
  const maxBodyBytes = positive(input.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes");
357
+ const maxBufferedRequestBodyBytes = positive(input.maxBufferedRequestBodyBytes, maxBodyBytes, "maxBufferedRequestBodyBytes");
249
358
  const maxWsFrameBytes = positive(input.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES, "maxWsFrameBytes");
359
+ const defaultMaxWsQueuedBytes = maxWsFrameBytes <= Number.MAX_SAFE_INTEGER - 5
360
+ ? maxWsFrameBytes + 5
361
+ : maxWsFrameBytes;
362
+ const maxWsQueuedBytes = positive(input.maxWsQueuedBytes, defaultMaxWsQueuedBytes, "maxWsQueuedBytes");
363
+ if (maxWsQueuedBytes < 5)
364
+ throw new Error("maxWsQueuedBytes must be at least 5");
250
365
  const defaultTimeoutMs = nonNegative(input.defaultTimeoutMs, 30_000, "defaultTimeoutMs");
251
366
  const maxTimeoutMs = nonNegative(input.maxTimeoutMs, 300_000, "maxTimeoutMs");
252
367
  if (maxTimeoutMs > 0 && defaultTimeoutMs > maxTimeoutMs)
@@ -257,7 +372,9 @@ function compileOptions(input) {
257
372
  maxJsonFrameBytes,
258
373
  maxChunkBytes,
259
374
  maxBodyBytes,
375
+ maxBufferedRequestBodyBytes,
260
376
  maxWsFrameBytes,
377
+ maxWsQueuedBytes,
261
378
  defaultTimeoutMs,
262
379
  maxTimeoutMs,
263
380
  maxConcurrentStreams: positive(input.maxConcurrentStreams, 64, "maxConcurrentStreams"),
@@ -349,30 +466,70 @@ function applyExternalOrigin(headers, input) {
349
466
  headers.set("x-forwarded-proto", new URL(external).protocol.slice(0, -1));
350
467
  headers.set("host", new URL(external).host);
351
468
  }
352
- async function readBody(reader, maxChunkBytes, maxBodyBytes) {
469
+ function createRequestBodyBudget(maxBytes) {
470
+ let usedBytes = 0;
471
+ return {
472
+ reserve: (bytes) => {
473
+ const next = usedBytes + bytes;
474
+ if (!Number.isSafeInteger(next) || next > maxBytes) {
475
+ throw new ProxyServerError("resource_exhausted", "proxy request body buffer exhausted");
476
+ }
477
+ usedBytes = next;
478
+ },
479
+ release: (bytes) => {
480
+ usedBytes = Math.max(0, usedBytes - bytes);
481
+ },
482
+ };
483
+ }
484
+ async function readBody(reader, maxChunkBytes, maxBodyBytes, budget) {
353
485
  const chunks = [];
354
486
  let total = 0;
355
- while (true) {
356
- const length = readU32be(await reader.readExactly(4), 0);
357
- if (length === 0)
358
- break;
359
- if (length > maxChunkBytes)
360
- throw new ProxyServerError("request_body_too_large", "request chunk too large");
361
- total += length;
362
- if (total > maxBodyBytes)
363
- throw new ProxyServerError("request_body_too_large", "request body too large");
364
- chunks.push(await reader.readExactly(length));
487
+ let handedOff = false;
488
+ try {
489
+ while (true) {
490
+ const length = readU32be(await reader.readExactly(4), 0);
491
+ if (length === 0)
492
+ break;
493
+ if (length > maxChunkBytes)
494
+ throw new ProxyServerError("request_body_too_large", "request chunk too large");
495
+ const next = total + length;
496
+ if (!Number.isSafeInteger(next) || next > maxBodyBytes) {
497
+ throw new ProxyServerError("request_body_too_large", "request body too large");
498
+ }
499
+ budget.reserve(length);
500
+ total = next;
501
+ chunks.push(await reader.readExactly(length));
502
+ }
503
+ const body = new Uint8Array(total);
504
+ let offset = 0;
505
+ for (const chunk of chunks) {
506
+ body.set(chunk, offset);
507
+ offset += chunk.length;
508
+ }
509
+ let released = false;
510
+ handedOff = true;
511
+ return {
512
+ bytes: body,
513
+ release: () => {
514
+ if (released)
515
+ return;
516
+ released = true;
517
+ budget.release(total);
518
+ },
519
+ };
365
520
  }
366
- const body = new Uint8Array(total);
367
- let offset = 0;
368
- for (const chunk of chunks) {
369
- body.set(chunk, offset);
370
- offset += chunk.length;
521
+ finally {
522
+ if (!handedOff)
523
+ budget.release(total);
371
524
  }
372
- return body;
373
525
  }
374
- async function writeHTTPError(stream, requestId, code, error) {
375
- const meta = { v: PROXY_PROTOCOL_VERSION, request_id: requestId.trim() || "unknown", ok: false, error: { code, message: asError(error).message } };
526
+ async function writeHTTPError(stream, requestId, code) {
527
+ const meta = {
528
+ v: PROXY_PROTOCOL_VERSION,
529
+ request_id: requestId.trim() || "unknown",
530
+ ok: false,
531
+ error: { code, message: publicHTTPErrorMessage(code) },
532
+ };
376
533
  try {
377
534
  await writeJsonFrame(stream, meta);
378
535
  await stream.write(u32be(0));
@@ -396,8 +553,13 @@ async function writeWSFrame(stream, op, payload, maxBytes) {
396
553
  if (payload.length > 0)
397
554
  await stream.write(payload);
398
555
  }
399
- async function writeWSOpenError(stream, connId, code, error) {
400
- const response = { v: PROXY_PROTOCOL_VERSION, conn_id: connId.trim() || "unknown", ok: false, error: { code, message: asError(error).message } };
556
+ async function writeWSOpenError(stream, connId, code) {
557
+ const response = {
558
+ v: PROXY_PROTOCOL_VERSION,
559
+ conn_id: connId.trim() || "unknown",
560
+ ok: false,
561
+ error: { code, message: publicWSErrorMessage(code) },
562
+ };
401
563
  try {
402
564
  await writeJsonFrame(stream, response);
403
565
  }
@@ -405,13 +567,65 @@ async function writeWSOpenError(stream, connId, code, error) {
405
567
  }
406
568
  function waitForUpstreamOpen(websocket, signal) {
407
569
  return new Promise((resolve, reject) => {
408
- const cleanup = () => { websocket.off("open", onOpen); websocket.off("error", onError); signal?.removeEventListener("abort", onAbort); };
409
- const onOpen = () => { cleanup(); resolve(); };
570
+ const cleanup = () => {
571
+ websocket.off("open", onOpen);
572
+ websocket.off("error", onError);
573
+ websocket.off("close", onClose);
574
+ signal?.removeEventListener("abort", onAbort);
575
+ };
576
+ const onOpen = () => {
577
+ try {
578
+ if (typeof websocket.pause === "function")
579
+ websocket.pause();
580
+ }
581
+ catch (error) {
582
+ cleanup();
583
+ reject(asError(error));
584
+ return;
585
+ }
586
+ cleanup();
587
+ resolve();
588
+ };
410
589
  const onError = (error) => { cleanup(); reject(error); };
590
+ const onClose = () => { cleanup(); reject(new Error("upstream WebSocket closed before open")); };
411
591
  const onAbort = () => { cleanup(); reject(signal?.reason ?? new Error("aborted")); };
412
592
  websocket.once("open", onOpen);
413
593
  websocket.once("error", onError);
594
+ websocket.once("close", onClose);
414
595
  signal?.addEventListener("abort", onAbort, { once: true });
596
+ if (signal?.aborted)
597
+ onAbort();
598
+ });
599
+ }
600
+ function sendUpstreamFrame(websocket, op, payload) {
601
+ return new Promise((resolve, reject) => {
602
+ const finish = (error) => {
603
+ if (error == null)
604
+ resolve();
605
+ else
606
+ reject(asError(error));
607
+ };
608
+ try {
609
+ switch (op) {
610
+ case 1:
611
+ websocket.send(payload, { binary: false }, finish);
612
+ return;
613
+ case 2:
614
+ websocket.send(payload, { binary: true }, finish);
615
+ return;
616
+ case 9:
617
+ websocket.ping(payload, undefined, finish);
618
+ return;
619
+ case 10:
620
+ websocket.pong(payload, undefined, finish);
621
+ return;
622
+ default:
623
+ reject(new Error("invalid upstream WebSocket operation"));
624
+ }
625
+ }
626
+ catch (error) {
627
+ reject(asError(error));
628
+ }
415
629
  });
416
630
  }
417
631
  function resolveTimeout(input, options) {
@@ -458,6 +672,33 @@ function classifyWSError(error) {
458
672
  return error.code;
459
673
  return "upstream_ws_dial_failed";
460
674
  }
675
+ function publicHTTPErrorMessage(code) {
676
+ switch (code) {
677
+ case "invalid_request_meta":
678
+ return "invalid proxy request";
679
+ case "request_body_too_large":
680
+ case "response_body_too_large":
681
+ return "proxy body limit exceeded";
682
+ case "resource_exhausted":
683
+ return "proxy resource limit exceeded";
684
+ case "timeout":
685
+ return "upstream request timed out";
686
+ case "canceled":
687
+ return "proxy request canceled";
688
+ default:
689
+ return "upstream request failed";
690
+ }
691
+ }
692
+ function publicWSErrorMessage(code) {
693
+ switch (code) {
694
+ case "invalid_ws_open_meta":
695
+ return "invalid proxy WebSocket request";
696
+ case "resource_exhausted":
697
+ return "proxy WebSocket resource limit exceeded";
698
+ default:
699
+ return "upstream WebSocket connection failed";
700
+ }
701
+ }
461
702
  function toBytes(input) {
462
703
  if (input instanceof Uint8Array)
463
704
  return input;
@@ -3,7 +3,7 @@ export declare const PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE = "flowersec-proxy:wind
3
3
  export declare const PROXY_WINDOW_FETCH_MSG_TYPE = "flowersec-proxy:fetch";
4
4
  export declare const PROXY_WINDOW_WS_OPEN_MSG_TYPE = "flowersec-proxy:ws_open";
5
5
  export declare const PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE = "flowersec-proxy:ws_open_ack";
6
- export declare const PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY = "stream_write_ack_v1";
6
+ export declare const PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY = "stream_bidirectional_ack_v2";
7
7
  export declare const PROXY_WINDOW_WS_ERROR_MSG_TYPE = "flowersec-proxy:ws_error";
8
8
  export declare const PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE = "flowersec-proxy:stream_chunk";
9
9
  export declare const PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE = "flowersec-proxy:stream_write_ack";
@@ -32,12 +32,13 @@ export type ProxyWindowWsOpenMsg = Readonly<{
32
32
  type: typeof PROXY_WINDOW_WS_OPEN_MSG_TYPE;
33
33
  path: string;
34
34
  protocols?: readonly string[];
35
+ capabilities: readonly string[];
35
36
  capabilityNonce?: string;
36
37
  }>;
37
38
  export type ProxyWindowWsOpenAckMsg = Readonly<{
38
39
  type: typeof PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE;
39
40
  protocol: string;
40
- capabilities?: readonly string[];
41
+ capabilities: readonly string[];
41
42
  }>;
42
43
  export type ProxyWindowWsErrorMsg = Readonly<{
43
44
  type: typeof PROXY_WINDOW_WS_ERROR_MSG_TYPE;
@@ -46,7 +47,7 @@ export type ProxyWindowWsErrorMsg = Readonly<{
46
47
  export type ProxyWindowStreamChunkMsg = Readonly<{
47
48
  type: typeof PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE;
48
49
  data: ArrayBuffer;
49
- writeId?: number;
50
+ writeId: number;
50
51
  }>;
51
52
  export type ProxyWindowStreamWriteAckMsg = Readonly<{
52
53
  type: typeof PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE;
@@ -2,7 +2,7 @@ export const PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE = "flowersec-proxy:window_fetch
2
2
  export const PROXY_WINDOW_FETCH_MSG_TYPE = "flowersec-proxy:fetch";
3
3
  export const PROXY_WINDOW_WS_OPEN_MSG_TYPE = "flowersec-proxy:ws_open";
4
4
  export const PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE = "flowersec-proxy:ws_open_ack";
5
- export const PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY = "stream_write_ack_v1";
5
+ export const PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY = "stream_bidirectional_ack_v2";
6
6
  export const PROXY_WINDOW_WS_ERROR_MSG_TYPE = "flowersec-proxy:ws_error";
7
7
  export const PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE = "flowersec-proxy:stream_chunk";
8
8
  export const PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE = "flowersec-proxy:stream_write_ack";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {