@floegence/flowersec-core 2.5.3 → 3.0.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.
Files changed (119) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/framing/jsonframe.d.ts +1 -1
  11. package/dist/framing/jsonframe.js +3 -1
  12. package/dist/interop/proxyServerPeer.js +1 -1
  13. package/dist/interop/serverParityPeer.js +8 -1
  14. package/dist/node/acceptor.d.ts +10 -0
  15. package/dist/node/acceptor.js +53 -7
  16. package/dist/node/acceptorV3.d.ts +57 -0
  17. package/dist/node/acceptorV3.js +227 -0
  18. package/dist/node/connectSessionV3.d.ts +21 -0
  19. package/dist/node/connectSessionV3.js +73 -0
  20. package/dist/node/index.d.ts +15 -10
  21. package/dist/node/index.js +6 -5
  22. package/dist/node/nativeTransportAddon.d.ts +39 -1
  23. package/dist/node/nativeTransportAddon.js +29 -5
  24. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  25. package/dist/node/rawQuicAdapterV3.js +76 -0
  26. package/dist/node/rawQuicServerV3.d.ts +24 -0
  27. package/dist/node/rawQuicServerV3.js +34 -0
  28. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  29. package/dist/node/runtimeAuthorizationV3.js +126 -0
  30. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  31. package/dist/node/tunnelRuntimeV3.js +857 -0
  32. package/dist/node/v2.d.ts +11 -0
  33. package/dist/node/v2.js +6 -0
  34. package/dist/node/webSocketServerV3.d.ts +27 -0
  35. package/dist/node/webSocketServerV3.js +219 -0
  36. package/dist/proxy/runtime.js +39 -6
  37. package/dist/proxy/serviceWorker.d.ts +2 -0
  38. package/dist/proxy/serviceWorker.js +63 -7
  39. package/dist/proxy/windowBridge.d.ts +1 -0
  40. package/dist/proxy/windowBridge.js +93 -33
  41. package/dist/proxy/wsPatch.d.ts +1 -0
  42. package/dist/proxy/wsPatch.js +39 -6
  43. package/dist/public/streamHandlers.d.ts +3 -0
  44. package/dist/public/streamHandlers.js +6 -1
  45. package/dist/rpc/client.js +9 -7
  46. package/dist/rpc/server.js +44 -20
  47. package/dist/rpc/validate.d.ts +1 -0
  48. package/dist/rpc/validate.js +18 -6
  49. package/dist/v2/index.d.ts +14 -0
  50. package/dist/v2/index.js +7 -0
  51. package/dist/v2/protocol.js +23 -32
  52. package/dist/v2/publicSession.js +4 -0
  53. package/dist/v2/session.d.ts +5 -0
  54. package/dist/v2/session.js +94 -35
  55. package/dist/v3/admissionError.d.ts +4 -0
  56. package/dist/v3/admissionError.js +8 -0
  57. package/dist/v3/artifact.d.ts +145 -0
  58. package/dist/v3/artifact.js +1162 -0
  59. package/dist/v3/artifactLease.d.ts +17 -0
  60. package/dist/v3/artifactLease.js +109 -0
  61. package/dist/v3/browserRuntime.d.ts +37 -0
  62. package/dist/v3/browserRuntime.js +197 -0
  63. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  64. package/dist/v3/browserSessionRuntime.js +21 -0
  65. package/dist/v3/capability.d.ts +32 -0
  66. package/dist/v3/capability.js +345 -0
  67. package/dist/v3/carrier.d.ts +84 -0
  68. package/dist/v3/carrier.js +411 -0
  69. package/dist/v3/connectionController.d.ts +87 -0
  70. package/dist/v3/connectionController.js +805 -0
  71. package/dist/v3/contract.d.ts +65 -0
  72. package/dist/v3/contract.js +3 -0
  73. package/dist/v3/controller.d.ts +43 -0
  74. package/dist/v3/controller.js +303 -0
  75. package/dist/v3/handshake.d.ts +81 -0
  76. package/dist/v3/handshake.js +471 -0
  77. package/dist/v3/jcs.d.ts +6 -0
  78. package/dist/v3/jcs.js +69 -0
  79. package/dist/v3/jsonPreflight.d.ts +1 -0
  80. package/dist/v3/jsonPreflight.js +251 -0
  81. package/dist/v3/nodeRuntime.d.ts +40 -0
  82. package/dist/v3/nodeRuntime.js +315 -0
  83. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  84. package/dist/v3/nodeSessionRuntime.js +13 -0
  85. package/dist/v3/protocol.d.ts +99 -0
  86. package/dist/v3/protocol.js +773 -0
  87. package/dist/v3/publicApi.d.ts +12 -0
  88. package/dist/v3/publicApi.js +46 -0
  89. package/dist/v3/publicSession.d.ts +1 -0
  90. package/dist/v3/publicSession.js +281 -0
  91. package/dist/v3/retryDisposition.d.ts +16 -0
  92. package/dist/v3/retryDisposition.js +30 -0
  93. package/dist/v3/runtimeAdapters.d.ts +9 -0
  94. package/dist/v3/runtimeAdapters.js +109 -0
  95. package/dist/v3/security.d.ts +34 -0
  96. package/dist/v3/security.js +100 -0
  97. package/dist/v3/serverAdmission.d.ts +38 -0
  98. package/dist/v3/serverAdmission.js +223 -0
  99. package/dist/v3/session.d.ts +318 -0
  100. package/dist/v3/session.js +2289 -0
  101. package/dist/v3/sessionConnector.d.ts +37 -0
  102. package/dist/v3/sessionConnector.js +440 -0
  103. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  104. package/dist/v3/streamLifetimeLedger.js +131 -0
  105. package/dist/v3/streamMetadata.d.ts +1 -0
  106. package/dist/v3/streamMetadata.js +7 -0
  107. package/dist/v3/transportConstants.d.ts +56 -0
  108. package/dist/v3/transportConstants.js +59 -0
  109. package/dist/v3/unreliableMessage.d.ts +44 -0
  110. package/dist/v3/unreliableMessage.js +284 -0
  111. package/dist/v3/webSocketCarrier.d.ts +31 -0
  112. package/dist/v3/webSocketCarrier.js +370 -0
  113. package/dist/v3/webTransportCarrier.d.ts +27 -0
  114. package/dist/v3/webTransportCarrier.js +534 -0
  115. package/dist/yamux/stream.d.ts +3 -0
  116. package/dist/yamux/stream.js +17 -4
  117. package/package.json +4 -4
  118. package/sbom/cyclonedx.json +6 -6
  119. package/sbom/spdx.json +11 -11
@@ -0,0 +1,11 @@
1
+ export * from "../v2/index.js";
2
+ export { connect, createConnectionController } from "./connectSession.js";
3
+ export type { ConnectionControllerOptions, SessionOptions, SessionTLSOptions, } from "./connectSession.js";
4
+ export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
5
+ export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions, } from "./acceptor.js";
6
+ export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
7
+ export type { TunnelAuthorizationDecision, TunnelRuntimeListener, TunnelRuntimeOptions, } from "./tunnelRuntime.js";
8
+ export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
9
+ export type { ArtifactMetadata, ControlPlaneErrorCode, DirectIssueOptions, IssuedTunnelPair, Scope, SessionOptions as ControlPlaneSessionOptions, TunnelIssueOptions, } from "./controlplane.js";
10
+ export { ProxyServer, ProxyServerError } from "./proxyServer.js";
11
+ export type { ProxyServerOptions, StreamHandlerRegistrar } from "./proxyServer.js";
@@ -0,0 +1,6 @@
1
+ export * from "../v2/index.js";
2
+ export { connect, createConnectionController } from "./connectSession.js";
3
+ export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
4
+ export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
5
+ export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
6
+ export { ProxyServer, ProxyServerError } from "./proxyServer.js";
@@ -0,0 +1,27 @@
1
+ import type { CarrierSessionV3 } from "../v3/carrier.js";
2
+ import type { PathKind } from "../v3/contract.js";
3
+ export type NodeWebSocketListenerOptionsV3 = Readonly<{
4
+ host: string;
5
+ port: number;
6
+ path: PathKind;
7
+ tls: Readonly<{
8
+ certificate: string;
9
+ privateKey: string;
10
+ }>;
11
+ allowedOrigins: readonly string[];
12
+ inboundBidirectionalStreamCapacity: number;
13
+ maxPendingSessions?: number;
14
+ pendingSessionTimeoutMs?: number;
15
+ cleanupTimeoutMs?: number;
16
+ }>;
17
+ export type NodeWebSocketListenerV3 = Readonly<{
18
+ address(): Readonly<{
19
+ host: string;
20
+ port: number;
21
+ }>;
22
+ accept(options?: Readonly<{
23
+ signal?: AbortSignal;
24
+ }>): Promise<CarrierSessionV3>;
25
+ close(): Promise<void>;
26
+ }>;
27
+ export declare function startNodeWebSocketListenerV3(options: NodeWebSocketListenerOptionsV3): Promise<NodeWebSocketListenerV3>;
@@ -0,0 +1,219 @@
1
+ import { createServer as createHTTPSServer } from "node:https";
2
+ import { createRequire } from "node:module";
3
+ import { constants } from "node:crypto";
4
+ import { createServerWebSocketCarrierSessionV3 } from "../v3/webSocketCarrier.js";
5
+ import { WebSocketBinaryTransport } from "../ws-client/binaryTransport.js";
6
+ import { defaultWsMaxPayload } from "./wsDefaults.js";
7
+ import { FLOWERSEC_V3_PATHS, websocketSubprotocolForPathV3, } from "../v3/transportConstants.js";
8
+ export async function startNodeWebSocketListenerV3(options) {
9
+ validateOptions(options);
10
+ const maxPendingSessions = options.maxPendingSessions ?? 1_024;
11
+ const pendingSessionTimeoutMs = options.pendingSessionTimeoutMs ?? 10_000;
12
+ const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 2_000;
13
+ const protocol = websocketSubprotocolForPathV3(options.path);
14
+ const endpoint = FLOWERSEC_V3_PATHS.websocket[options.path];
15
+ const require = createRequire(import.meta.url);
16
+ const wsModule = require("ws");
17
+ const WebSocketServer = wsModule.WebSocketServer;
18
+ const server = createHTTPSServer({
19
+ key: options.tls.privateKey,
20
+ cert: options.tls.certificate,
21
+ minVersion: "TLSv1.3",
22
+ maxVersion: "TLSv1.3",
23
+ secureOptions: constants.SSL_OP_NO_TICKET,
24
+ });
25
+ const sessions = new SessionQueueV3(maxPendingSessions, pendingSessionTimeoutMs);
26
+ const sockets = new Set();
27
+ const networkSockets = new Set();
28
+ const wss = new WebSocketServer({
29
+ noServer: true,
30
+ perMessageDeflate: false,
31
+ maxPayload: defaultWsMaxPayload({}),
32
+ handleProtocols(protocols) { return protocols.size === 1 && protocols.has(protocol) ? protocol : false; },
33
+ });
34
+ server.on("connection", (socket) => {
35
+ networkSockets.add(socket);
36
+ socket.once("close", () => networkSockets.delete(socket));
37
+ });
38
+ server.on("upgrade", (request, socket, head) => {
39
+ const origin = request.headers.origin;
40
+ const requested = request.headers["sec-websocket-protocol"];
41
+ if (request.url !== endpoint || typeof origin !== "string" ||
42
+ !options.allowedOrigins.includes(origin) || requested !== protocol) {
43
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
44
+ socket.destroy();
45
+ return;
46
+ }
47
+ wss.handleUpgrade(request, socket, head, (webSocket) => wss.emit("connection", webSocket));
48
+ });
49
+ wss.on("connection", (socket) => {
50
+ sockets.add(socket);
51
+ const session = createServerWebSocketCarrierSessionV3(new WebSocketBinaryTransport(socket), {
52
+ path: options.path,
53
+ inboundBidirectionalStreamCapacity: options.inboundBidirectionalStreamCapacity,
54
+ });
55
+ socket.once("close", () => {
56
+ sockets.delete(socket);
57
+ sessions.remove(session);
58
+ });
59
+ sessions.push(session);
60
+ });
61
+ await new Promise((resolve, reject) => {
62
+ server.once("error", reject);
63
+ server.listen(options.port, options.host, resolve);
64
+ });
65
+ let closePromise;
66
+ return Object.freeze({
67
+ address() {
68
+ const address = server.address();
69
+ if (address === null || typeof address === "string")
70
+ throw new Error("Node WebSocket v3 listener is not listening");
71
+ return Object.freeze({ host: address.address, port: address.port });
72
+ },
73
+ async accept(acceptOptions = {}) { return await sessions.shift(acceptOptions.signal); },
74
+ async close() {
75
+ closePromise ??= closeListener();
76
+ await closePromise;
77
+ },
78
+ });
79
+ async function closeListener() {
80
+ sessions.close();
81
+ for (const socket of sockets)
82
+ socket.close();
83
+ const graceful = Promise.all([
84
+ new Promise((resolve, reject) => {
85
+ wss.close((error) => error === undefined ? resolve() : reject(error));
86
+ }),
87
+ new Promise((resolve, reject) => {
88
+ server.close((error) => error === undefined ? resolve() : reject(error));
89
+ }),
90
+ ]);
91
+ let timer;
92
+ let timedOut;
93
+ try {
94
+ timedOut = await Promise.race([
95
+ graceful.then(() => false),
96
+ new Promise((resolve) => {
97
+ timer = setTimeout(() => resolve(true), cleanupTimeoutMs);
98
+ timer.unref?.();
99
+ }),
100
+ ]);
101
+ }
102
+ finally {
103
+ if (timer !== undefined)
104
+ clearTimeout(timer);
105
+ }
106
+ if (!timedOut)
107
+ return;
108
+ for (const socket of sockets)
109
+ socket.terminate();
110
+ for (const socket of networkSockets)
111
+ socket.destroy();
112
+ }
113
+ }
114
+ function validateOptions(options) {
115
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65_535 ||
116
+ options.allowedOrigins.length === 0 || new Set(options.allowedOrigins).size !== options.allowedOrigins.length ||
117
+ options.allowedOrigins.some((origin) => !validOrigin(origin)) ||
118
+ !Number.isInteger(options.inboundBidirectionalStreamCapacity) ||
119
+ options.inboundBidirectionalStreamCapacity < 3 || options.inboundBidirectionalStreamCapacity > 130 ||
120
+ (options.maxPendingSessions !== undefined && (!Number.isSafeInteger(options.maxPendingSessions) || options.maxPendingSessions < 1 || options.maxPendingSessions > 1_024)) ||
121
+ (options.pendingSessionTimeoutMs !== undefined && (!Number.isSafeInteger(options.pendingSessionTimeoutMs) || options.pendingSessionTimeoutMs < 1 || options.pendingSessionTimeoutMs > 600_000)) ||
122
+ (options.cleanupTimeoutMs !== undefined && (!Number.isSafeInteger(options.cleanupTimeoutMs) || options.cleanupTimeoutMs < 1 || options.cleanupTimeoutMs > 600_000)) ||
123
+ options.tls.certificate.length === 0 || options.tls.privateKey.length === 0) {
124
+ throw new TypeError("invalid Node WebSocket v3 listener options");
125
+ }
126
+ }
127
+ function validOrigin(raw) {
128
+ try {
129
+ const value = new URL(raw);
130
+ return (value.protocol === "https:" || value.protocol === "http:") && value.origin === raw &&
131
+ value.username === "" && value.password === "";
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ class SessionQueueV3 {
138
+ maximum;
139
+ timeoutMs;
140
+ values = [];
141
+ waiters = new Set();
142
+ closed = false;
143
+ constructor(maximum, timeoutMs) {
144
+ this.maximum = maximum;
145
+ this.timeoutMs = timeoutMs;
146
+ }
147
+ push(value) {
148
+ if (this.closed) {
149
+ value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
150
+ return;
151
+ }
152
+ const waiter = this.waiters.values().next().value;
153
+ if (waiter !== undefined) {
154
+ this.waiters.delete(waiter);
155
+ waiter.resolve(value);
156
+ return;
157
+ }
158
+ if (this.values.length >= this.maximum) {
159
+ value.abort({ code: 1, reason: "WebSocket v3 pending capacity exceeded" });
160
+ return;
161
+ }
162
+ const entry = {
163
+ value,
164
+ timer: setTimeout(() => {
165
+ if (this.remove(value)) {
166
+ value.abort({ code: 1, reason: "WebSocket v3 pending admission timed out" });
167
+ }
168
+ }, this.timeoutMs),
169
+ };
170
+ this.values.push(entry);
171
+ }
172
+ remove(value) {
173
+ const index = this.values.findIndex((entry) => entry.value === value);
174
+ if (index < 0)
175
+ return false;
176
+ const [entry] = this.values.splice(index, 1);
177
+ if (entry !== undefined)
178
+ clearTimeout(entry.timer);
179
+ return true;
180
+ }
181
+ async shift(signal) {
182
+ if (signal?.aborted === true)
183
+ throw signal.reason ?? new Error("accept canceled");
184
+ const entry = this.values.shift();
185
+ if (entry !== undefined) {
186
+ clearTimeout(entry.timer);
187
+ return entry.value;
188
+ }
189
+ if (this.closed)
190
+ throw new Error("WebSocket v3 listener is closed");
191
+ return await new Promise((resolve, reject) => {
192
+ const waiter = {
193
+ resolve: (session) => { cleanup(); resolve(session); },
194
+ reject: (error) => { cleanup(); reject(error); },
195
+ };
196
+ const abort = () => {
197
+ this.waiters.delete(waiter);
198
+ reject(signal?.reason ?? new Error("accept canceled"));
199
+ };
200
+ const cleanup = () => signal?.removeEventListener("abort", abort);
201
+ this.waiters.add(waiter);
202
+ signal?.addEventListener("abort", abort, { once: true });
203
+ });
204
+ }
205
+ close() {
206
+ if (this.closed)
207
+ return;
208
+ this.closed = true;
209
+ const error = new Error("WebSocket v3 listener is closed");
210
+ for (const waiter of this.waiters)
211
+ waiter.reject(error);
212
+ this.waiters.clear();
213
+ for (const entry of this.values) {
214
+ clearTimeout(entry.timer);
215
+ entry.value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
216
+ }
217
+ this.values.length = 0;
218
+ }
219
+ }
@@ -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;