@floegence/flowersec-core 0.26.0 → 2.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 (270) hide show
  1. package/README.md +91 -22
  2. package/THIRD_PARTY_NOTICES.md +141 -0
  3. package/dist/browser/connectSession.d.ts +13 -0
  4. package/dist/browser/connectSession.js +40 -0
  5. package/dist/browser/index.d.ts +3 -10
  6. package/dist/browser/index.js +2 -5
  7. package/dist/browser/runtimeCapability.d.ts +13 -0
  8. package/dist/browser/runtimeCapability.js +27 -0
  9. package/dist/browser/sessionRuntime.d.ts +2 -0
  10. package/dist/browser/sessionRuntime.js +21 -0
  11. package/dist/browser/webTransportClient.d.ts +8 -0
  12. package/dist/browser/webTransportClient.js +55 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +126 -0
  15. package/dist/cliSpendMarker.d.ts +1 -0
  16. package/dist/cliSpendMarker.js +22 -0
  17. package/dist/connectionController.d.ts +54 -0
  18. package/dist/connectionController.js +438 -0
  19. package/dist/connector/adapters/webSocketCandidate.d.ts +7 -0
  20. package/dist/connector/adapters/webSocketCandidate.js +116 -0
  21. package/dist/connector/adapters/webTransportCandidate.d.ts +5 -0
  22. package/dist/connector/adapters/webTransportCandidate.js +73 -0
  23. package/dist/connector/admissionCommit.d.ts +31 -0
  24. package/dist/connector/admissionCommit.js +239 -0
  25. package/dist/connector/sessionAcceptor.d.ts +16 -0
  26. package/dist/connector/sessionAcceptor.js +114 -0
  27. package/dist/connector/sessionConfig.d.ts +3 -0
  28. package/dist/connector/sessionConfig.js +30 -0
  29. package/dist/connector/sessionConnector.d.ts +45 -0
  30. package/dist/connector/sessionConnector.js +603 -0
  31. package/dist/defaults.d.ts +7 -7
  32. package/dist/defaults.js +7 -7
  33. package/dist/facade.d.ts +12 -30
  34. package/dist/facade.js +6 -30
  35. package/dist/node/connectSession.d.ts +21 -0
  36. package/dist/node/connectSession.js +62 -0
  37. package/dist/node/index.d.ts +3 -11
  38. package/dist/node/index.js +2 -8
  39. package/dist/node/runtimeCapability.d.ts +7 -0
  40. package/dist/node/runtimeCapability.js +10 -0
  41. package/dist/node/sessionRuntime.d.ts +2 -0
  42. package/dist/node/sessionRuntime.js +13 -0
  43. package/dist/node/webTransportClient.d.ts +9 -0
  44. package/dist/node/webTransportClient.js +56 -0
  45. package/dist/node/webTransportServer.d.ts +22 -0
  46. package/dist/node/webTransportServer.js +100 -0
  47. package/dist/node/wsFactory.d.ts +3 -1
  48. package/dist/node/wsFactory.js +4 -3
  49. package/dist/proxy/controllerGuard.d.ts +4 -4
  50. package/dist/proxy/controllerGuard.js +83 -177
  51. package/dist/proxy/disableUpstreamServiceWorkerRegister.d.ts +3 -1
  52. package/dist/proxy/disableUpstreamServiceWorkerRegister.js +19 -17
  53. package/dist/proxy/index.d.ts +16 -15
  54. package/dist/proxy/index.js +8 -14
  55. package/dist/proxy/integration.d.ts +23 -71
  56. package/dist/proxy/integration.js +37 -340
  57. package/dist/proxy/registerServiceWorker.d.ts +1 -10
  58. package/dist/proxy/registerServiceWorker.js +43 -105
  59. package/dist/proxy/runtime.d.ts +3 -63
  60. package/dist/proxy/runtime.js +349 -462
  61. package/dist/proxy/scope.d.ts +6 -0
  62. package/dist/proxy/scope.js +131 -0
  63. package/dist/proxy/serviceWorker.d.ts +5 -16
  64. package/dist/proxy/serviceWorker.js +230 -617
  65. package/dist/proxy/stream.d.ts +9 -0
  66. package/dist/proxy/stream.js +40 -0
  67. package/dist/proxy/types.d.ts +78 -30
  68. package/dist/proxy/windowBridge.d.ts +61 -0
  69. package/dist/proxy/windowBridge.js +344 -0
  70. package/dist/proxy/wsPatch.d.ts +4 -14
  71. package/dist/proxy/wsPatch.js +173 -268
  72. package/dist/public/artifact.d.ts +11 -0
  73. package/dist/public/artifact.js +44 -0
  74. package/dist/public/artifactLease.d.ts +10 -0
  75. package/dist/public/artifactLease.js +48 -0
  76. package/dist/public/connectError.d.ts +6 -0
  77. package/dist/public/connectError.js +9 -0
  78. package/dist/public/contract.d.ts +83 -0
  79. package/dist/public/contract.js +17 -0
  80. package/dist/public/streamMetadata.d.ts +10 -0
  81. package/dist/public/streamMetadata.js +40 -0
  82. package/dist/rpc/caller.d.ts +1 -1
  83. package/dist/rpc/client.d.ts +1 -4
  84. package/dist/rpc/client.js +0 -21
  85. package/dist/rpc/rpcProxy.d.ts +1 -1
  86. package/dist/rpc/server.d.ts +1 -1
  87. package/dist/rpc/validate.d.ts +1 -1
  88. package/dist/rpc/wire.d.ts +15 -0
  89. package/dist/runtime/errors.d.ts +6 -0
  90. package/dist/runtime/errors.js +10 -0
  91. package/dist/transport/webSocketAdapter.d.ts +24 -0
  92. package/dist/transport/webSocketAdapter.js +234 -0
  93. package/dist/transport/webTransportAdapter.d.ts +27 -0
  94. package/dist/transport/webTransportAdapter.js +519 -0
  95. package/dist/utils/errors.d.ts +2 -20
  96. package/dist/utils/errors.js +73 -20
  97. package/dist/v2/admissionError.d.ts +4 -0
  98. package/dist/v2/admissionError.js +8 -0
  99. package/dist/v2/artifact.d.ts +128 -0
  100. package/dist/v2/artifact.js +1126 -0
  101. package/dist/v2/artifactLease.d.ts +1 -0
  102. package/dist/v2/artifactLease.js +7 -0
  103. package/dist/v2/capability.d.ts +29 -0
  104. package/dist/v2/capability.js +190 -0
  105. package/dist/v2/carrier.d.ts +79 -0
  106. package/dist/v2/carrier.js +405 -0
  107. package/dist/v2/contract.d.ts +37 -0
  108. package/dist/v2/contract.js +1 -0
  109. package/dist/v2/handshake.d.ts +80 -0
  110. package/dist/v2/handshake.js +470 -0
  111. package/dist/v2/opaqueArtifact.d.ts +1 -0
  112. package/dist/v2/opaqueArtifact.js +2 -0
  113. package/dist/v2/protocol.d.ts +99 -0
  114. package/dist/v2/protocol.js +739 -0
  115. package/dist/v2/publicSession.d.ts +1 -0
  116. package/dist/v2/publicSession.js +179 -0
  117. package/dist/v2/retryDisposition.d.ts +16 -0
  118. package/dist/v2/retryDisposition.js +30 -0
  119. package/dist/v2/session.d.ts +277 -0
  120. package/dist/v2/session.js +1879 -0
  121. package/dist/v2/streamLifetimeLedger.d.ts +31 -0
  122. package/dist/v2/streamLifetimeLedger.js +131 -0
  123. package/dist/v2/streamMetadata.d.ts +1 -0
  124. package/dist/v2/streamMetadata.js +7 -0
  125. package/dist/v2/unreliableMessage.d.ts +43 -0
  126. package/dist/v2/unreliableMessage.js +252 -0
  127. package/dist/vendor/tr46.d.ts +10 -0
  128. package/dist/vendor/tr46.js +501 -0
  129. package/dist/ws-client/binaryTransport.d.ts +0 -3
  130. package/dist/ws-client/binaryTransport.js +15 -39
  131. package/dist/yamux/errors.d.ts +0 -4
  132. package/dist/yamux/errors.js +0 -9
  133. package/dist/yamux/session.d.ts +0 -5
  134. package/dist/yamux/session.js +4 -70
  135. package/dist/yamux/stream.d.ts +1 -0
  136. package/dist/yamux/stream.js +4 -0
  137. package/package.json +29 -83
  138. package/sbom/cyclonedx.json +2764 -0
  139. package/sbom/spdx.json +1947 -0
  140. package/YAMUX_ALIGNMENT.md +0 -127
  141. package/dist/_examples/flowersec/demo/v1.facade.gen.d.ts +0 -12
  142. package/dist/_examples/flowersec/demo/v1.facade.gen.js +0 -15
  143. package/dist/_examples/flowersec/demo/v1.gen.d.ts +0 -16
  144. package/dist/_examples/flowersec/demo/v1.gen.js +0 -86
  145. package/dist/_examples/flowersec/demo/v1.rpc.gen.d.ts +0 -11
  146. package/dist/_examples/flowersec/demo/v1.rpc.gen.js +0 -22
  147. package/dist/browser/connect.d.ts +0 -12
  148. package/dist/browser/connect.js +0 -31
  149. package/dist/browser/controlplane.d.ts +0 -15
  150. package/dist/browser/controlplane.js +0 -64
  151. package/dist/browser/reconnectConfig.d.ts +0 -15
  152. package/dist/browser/reconnectConfig.js +0 -20
  153. package/dist/client-connect/common.d.ts +0 -26
  154. package/dist/client-connect/common.js +0 -167
  155. package/dist/client-connect/connectCore.d.ts +0 -61
  156. package/dist/client-connect/connectCore.js +0 -572
  157. package/dist/client-connect/contract.d.ts +0 -8
  158. package/dist/client-connect/contract.js +0 -51
  159. package/dist/client-connect/termination.d.ts +0 -6
  160. package/dist/client-connect/termination.js +0 -7
  161. package/dist/client-connect/transportSecurity.d.ts +0 -27
  162. package/dist/client-connect/transportSecurity.js +0 -199
  163. package/dist/client-connect/tunnelAttachCloseReason.d.ts +0 -3
  164. package/dist/client-connect/tunnelAttachCloseReason.js +0 -23
  165. package/dist/client.d.ts +0 -23
  166. package/dist/connect/artifact.d.ts +0 -37
  167. package/dist/connect/artifact.js +0 -217
  168. package/dist/connect/internalNormalize.d.ts +0 -22
  169. package/dist/connect/internalNormalize.js +0 -173
  170. package/dist/controlplane/channelInit.d.ts +0 -27
  171. package/dist/controlplane/channelInit.js +0 -116
  172. package/dist/controlplane/http.d.ts +0 -16
  173. package/dist/controlplane/http.js +0 -59
  174. package/dist/controlplane/index.d.ts +0 -6
  175. package/dist/controlplane/index.js +0 -5
  176. package/dist/controlplane/issuer.d.ts +0 -18
  177. package/dist/controlplane/issuer.js +0 -113
  178. package/dist/controlplane/request.d.ts +0 -51
  179. package/dist/controlplane/request.js +0 -273
  180. package/dist/controlplane/token.d.ts +0 -33
  181. package/dist/controlplane/token.js +0 -119
  182. package/dist/direct-client/connect.d.ts +0 -9
  183. package/dist/direct-client/connect.js +0 -90
  184. package/dist/direct-client/index.d.ts +0 -1
  185. package/dist/direct-client/index.js +0 -1
  186. package/dist/e2ee/constants.d.ts +0 -9
  187. package/dist/e2ee/constants.js +0 -18
  188. package/dist/e2ee/errors.d.ts +0 -5
  189. package/dist/e2ee/errors.js +0 -8
  190. package/dist/e2ee/framing.d.ts +0 -12
  191. package/dist/e2ee/framing.js +0 -57
  192. package/dist/e2ee/handshake.d.ts +0 -88
  193. package/dist/e2ee/handshake.js +0 -353
  194. package/dist/e2ee/index.d.ts +0 -7
  195. package/dist/e2ee/index.js +0 -7
  196. package/dist/e2ee/kdf.d.ts +0 -15
  197. package/dist/e2ee/kdf.js +0 -39
  198. package/dist/e2ee/record.d.ts +0 -11
  199. package/dist/e2ee/record.js +0 -76
  200. package/dist/e2ee/secureChannel.d.ts +0 -92
  201. package/dist/e2ee/secureChannel.js +0 -328
  202. package/dist/e2ee/transcript.d.ts +0 -23
  203. package/dist/e2ee/transcript.js +0 -31
  204. package/dist/endpoint/index.d.ts +0 -84
  205. package/dist/endpoint/index.js +0 -557
  206. package/dist/endpoint/node.d.ts +0 -9
  207. package/dist/endpoint/node.js +0 -51
  208. package/dist/gen/flowersec/controlplane/v1.gen.d.ts +0 -36
  209. package/dist/gen/flowersec/controlplane/v1.gen.js +0 -135
  210. package/dist/gen/flowersec/direct/v1.gen.d.ts +0 -21
  211. package/dist/gen/flowersec/direct/v1.gen.js +0 -101
  212. package/dist/gen/flowersec/e2ee/v1.gen.d.ts +0 -68
  213. package/dist/gen/flowersec/e2ee/v1.gen.js +0 -194
  214. package/dist/gen/flowersec/rpc/v1.gen.d.ts +0 -30
  215. package/dist/gen/flowersec/rpc/v1.gen.js +0 -107
  216. package/dist/gen/flowersec/tunnel/v1.gen.d.ts +0 -23
  217. package/dist/gen/flowersec/tunnel/v1.gen.js +0 -104
  218. package/dist/index.d.ts +0 -22
  219. package/dist/index.js +0 -22
  220. package/dist/node/connect.d.ts +0 -12
  221. package/dist/node/connect.js +0 -29
  222. package/dist/node/reconnectConfig.d.ts +0 -16
  223. package/dist/node/reconnectConfig.js +0 -20
  224. package/dist/observability/index.d.ts +0 -1
  225. package/dist/observability/index.js +0 -1
  226. package/dist/observability/observer.d.ts +0 -52
  227. package/dist/observability/observer.js +0 -314
  228. package/dist/proxy/appWindow.d.ts +0 -37
  229. package/dist/proxy/appWindow.js +0 -273
  230. package/dist/proxy/bootstrap.d.ts +0 -35
  231. package/dist/proxy/bootstrap.js +0 -119
  232. package/dist/proxy/constants.d.ts +0 -7
  233. package/dist/proxy/constants.js +0 -8
  234. package/dist/proxy/controllerWindow.d.ts +0 -12
  235. package/dist/proxy/controllerWindow.js +0 -384
  236. package/dist/proxy/cookieJar.d.ts +0 -7
  237. package/dist/proxy/cookieJar.js +0 -136
  238. package/dist/proxy/headerPolicy.d.ts +0 -14
  239. package/dist/proxy/headerPolicy.js +0 -134
  240. package/dist/proxy/portStream.d.ts +0 -5
  241. package/dist/proxy/portStream.js +0 -256
  242. package/dist/proxy/preset.d.ts +0 -29
  243. package/dist/proxy/preset.js +0 -111
  244. package/dist/proxy/runtimeScope.d.ts +0 -57
  245. package/dist/proxy/runtimeScope.js +0 -218
  246. package/dist/proxy/server.d.ts +0 -28
  247. package/dist/proxy/server.js +0 -833
  248. package/dist/proxy/windowBridgeProtocol.d.ts +0 -65
  249. package/dist/proxy/windowBridgeProtocol.js +0 -11
  250. package/dist/reconnect/artifactControlplane.d.ts +0 -16
  251. package/dist/reconnect/artifactControlplane.js +0 -35
  252. package/dist/reconnect/index.d.ts +0 -36
  253. package/dist/reconnect/index.js +0 -388
  254. package/dist/rpc-proxy/index.d.ts +0 -1
  255. package/dist/rpc-proxy/index.js +0 -1
  256. package/dist/rpc-proxy/rpcProxy.d.ts +0 -1
  257. package/dist/rpc-proxy/rpcProxy.js +0 -1
  258. package/dist/streamhello/index.d.ts +0 -1
  259. package/dist/streamhello/index.js +0 -1
  260. package/dist/streamhello/streamHello.d.ts +0 -3
  261. package/dist/streamhello/streamHello.js +0 -13
  262. package/dist/streamio/index.d.ts +0 -32
  263. package/dist/streamio/index.js +0 -87
  264. package/dist/tunnel-client/connect.d.ts +0 -9
  265. package/dist/tunnel-client/connect.js +0 -157
  266. package/dist/tunnel-client/index.d.ts +0 -1
  267. package/dist/tunnel-client/index.js +0 -1
  268. package/dist/ws/index.d.ts +0 -1
  269. package/dist/ws/index.js +0 -1
  270. /package/dist/{client.js → rpc/wire.js} +0 -0
@@ -1,502 +1,408 @@
1
- import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
- import { createByteReader } from "../streamio/index.js";
1
+ import { SDK_DEFAULTS } from "../defaults.js";
2
+ import { readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
3
3
  import { base64urlEncode } from "../utils/base64url.js";
4
4
  import { readU32be, u32be } from "../utils/bin.js";
5
- import { AbortError, FlowersecError, isFlowersecError } from "../utils/errors.js";
6
- import { CookieJar } from "./cookieJar.js";
7
- import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION } from "./constants.js";
8
- import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders } from "./headerPolicy.js";
9
- class ProxyRuntimePolicyError extends Error {
10
- status = 403;
11
- }
12
- function randomB64u(bytes) {
13
- const b = new Uint8Array(bytes);
14
- if (globalThis.crypto?.getRandomValues) {
15
- globalThis.crypto.getRandomValues(b);
16
- }
17
- else {
18
- for (let i = 0; i < b.length; i++)
19
- b[i] = Math.floor(Math.random() * 256);
20
- }
21
- return base64urlEncode(b);
22
- }
23
- function pathOnly(path) {
24
- const p = path.trim();
25
- if (!p.startsWith("/"))
26
- throw new Error("path must start with /");
27
- if (p.startsWith("//"))
28
- throw new Error("path must not start with //");
29
- if (/[ \t\r\n]/.test(p))
30
- throw new Error("path contains whitespace");
31
- if (p.includes("://"))
32
- throw new Error("path must not include scheme/host");
33
- return p;
5
+ import { SessionError } from "../public/contract.js";
6
+ import { createStreamMetadata } from "../public/streamMetadata.js";
7
+ import { ProxyByteReader, writeAll } from "./stream.js";
8
+ const PROXY_HTTP_STREAM_KIND = "flowersec-proxy/http1";
9
+ const PROXY_WEBSOCKET_STREAM_KIND = "flowersec-proxy/ws";
10
+ const PROXY_WIRE_VERSION = 1;
11
+ const DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES = 4 * 1024 * 1024;
12
+ const DEFAULT_MAX_CONCURRENT_HTTP_STREAMS = 24;
13
+ const DEFAULT_MAX_QUEUED_HTTP_REQUESTS = 128;
14
+ const DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES = 64 * 1024 * 1024;
15
+ class ProxyPolicyError extends Error {
34
16
  }
35
- function requestPathname(path) {
36
- const q = path.indexOf("?");
37
- return q >= 0 ? path.slice(0, q) : path;
17
+ function randomID() {
18
+ const bytes = new Uint8Array(18);
19
+ if (globalThis.crypto?.getRandomValues !== undefined)
20
+ globalThis.crypto.getRandomValues(bytes);
21
+ else
22
+ for (let index = 0; index < bytes.length; index++)
23
+ bytes[index] = Math.floor(Math.random() * 256);
24
+ return base64urlEncode(bytes);
38
25
  }
39
- function cookiePathFromRequestPath(path) {
40
- return requestPathname(path);
26
+ function positiveLimit(name, input, fallback) {
27
+ if (input === undefined || input === 0)
28
+ return fallback;
29
+ if (!Number.isSafeInteger(input) || input < 0)
30
+ throw new TypeError(`${name} must be a non-negative safe integer`);
31
+ return input;
41
32
  }
42
- function normalizeTimeoutMs(timeoutMs) {
43
- const v = Math.floor(timeoutMs ?? 0);
44
- if (v < 0)
45
- throw new Error("timeout_ms must be >= 0");
46
- return v;
33
+ function strictlyPositiveLimit(name, input, fallback) {
34
+ if (input === undefined)
35
+ return fallback;
36
+ if (!Number.isSafeInteger(input) || input <= 0)
37
+ throw new TypeError(`${name} must be a positive safe integer`);
38
+ return input;
47
39
  }
48
- function normalizeExternalOrigin(externalOriginRaw) {
49
- if (typeof externalOriginRaw !== "string")
50
- return undefined;
51
- const externalOrigin = externalOriginRaw.trim();
52
- if (externalOrigin === "")
53
- return undefined;
54
- let parsed;
55
- try {
56
- parsed = new URL(externalOrigin);
57
- }
58
- catch {
59
- throw new Error("external_origin must be an http(s) origin");
40
+ function normalizeTimeout(input) {
41
+ if (input === undefined)
42
+ return 0;
43
+ if (!Number.isSafeInteger(input) || input < 0 || input > SDK_DEFAULTS.proxy.maxTimeoutMs) {
44
+ throw new TypeError("timeoutMs must be within the proxy timeout contract");
60
45
  }
61
- if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.host === "") {
62
- throw new Error("external_origin must be an http(s) origin");
63
- }
64
- if (parsed.username !== "" || parsed.password !== "" || (parsed.pathname !== "" && parsed.pathname !== "/") || parsed.search !== "" || parsed.hash !== "") {
65
- throw new Error("external_origin must be an origin without credentials, path, query, or fragment");
66
- }
67
- return parsed.origin;
46
+ return input;
68
47
  }
69
- function normalizeOptionalToken(name, input) {
70
- if (input == null)
71
- return undefined;
72
- const s = String(input);
73
- if (s === "")
74
- return undefined;
75
- if (s.trim() !== s || /[\s\u0000-\u001f\u007f]/.test(s)) {
76
- throw new Error(`${name} must not contain whitespace or control characters`);
48
+ 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");
77
51
  }
78
- return s;
52
+ return input;
53
+ }
54
+ function pathName(path) {
55
+ const query = path.indexOf("?");
56
+ return query < 0 ? path : path.slice(0, query);
79
57
  }
80
- function normalizePathPolicyPrefixes(name, input) {
81
- const out = [];
82
- if (input == null || input.length === 0)
83
- return out;
84
- for (const raw of input) {
85
- const s = pathOnly(String(raw ?? ""));
86
- if (s.includes("?"))
87
- throw new Error(`${name} must not include query`);
88
- if (!out.includes(s))
89
- out.push(s);
58
+ function normalizePrefixes(name, values) {
59
+ const result = [];
60
+ for (const raw of values ?? []) {
61
+ const value = normalizePath(raw);
62
+ if (value.includes("?"))
63
+ throw new TypeError(`${name} must not include a query`);
64
+ if (!result.includes(value))
65
+ result.push(value);
90
66
  }
91
- return out;
67
+ return Object.freeze(result);
92
68
  }
93
69
  function normalizePathPolicy(policy) {
94
70
  return {
95
- allowedPathPrefixes: normalizePathPolicyPrefixes("pathPolicy.allowedPathPrefixes", policy?.allowedPathPrefixes),
96
- deniedPathPrefixes: normalizePathPolicyPrefixes("pathPolicy.deniedPathPrefixes", policy?.deniedPathPrefixes),
97
- allowedWebSocketPathPrefixes: normalizePathPolicyPrefixes("pathPolicy.allowedWebSocketPathPrefixes", policy?.allowedWebSocketPathPrefixes),
98
- deniedWebSocketPathPrefixes: normalizePathPolicyPrefixes("pathPolicy.deniedWebSocketPathPrefixes", policy?.deniedWebSocketPathPrefixes),
71
+ allowedPathPrefixes: normalizePrefixes("allowedPathPrefixes", policy?.allowedPathPrefixes),
72
+ deniedPathPrefixes: normalizePrefixes("deniedPathPrefixes", policy?.deniedPathPrefixes),
73
+ allowedWebSocketPathPrefixes: normalizePrefixes("allowedWebSocketPathPrefixes", policy?.allowedWebSocketPathPrefixes),
74
+ deniedWebSocketPathPrefixes: normalizePrefixes("deniedWebSocketPathPrefixes", policy?.deniedWebSocketPathPrefixes),
99
75
  };
100
76
  }
101
- function assertPathPolicyAllows(kind, path, policy) {
102
- const pathname = requestPathname(path);
103
- const denied = kind === "websocket" ? [...policy.deniedPathPrefixes, ...policy.deniedWebSocketPathPrefixes] : policy.deniedPathPrefixes;
104
- for (const prefix of denied) {
105
- if (pathname.startsWith(prefix))
106
- throw new ProxyRuntimePolicyError(`${kind} path is denied by proxy runtime policy`);
107
- }
77
+ function enforcePathPolicy(kind, path, policy) {
78
+ const candidate = pathName(path);
79
+ const denied = kind === "websocket"
80
+ ? [...policy.deniedPathPrefixes, ...policy.deniedWebSocketPathPrefixes]
81
+ : policy.deniedPathPrefixes;
82
+ if (denied.some((prefix) => candidate.startsWith(prefix)))
83
+ throw new ProxyPolicyError("proxy path denied");
108
84
  const allowed = kind === "websocket" && policy.allowedWebSocketPathPrefixes.length > 0
109
85
  ? policy.allowedWebSocketPathPrefixes
110
86
  : policy.allowedPathPrefixes;
111
- if (allowed.length === 0)
112
- return;
113
- for (const prefix of allowed) {
114
- if (pathname.startsWith(prefix))
115
- return;
87
+ if (allowed.length > 0 && !allowed.some((prefix) => candidate.startsWith(prefix))) {
88
+ throw new ProxyPolicyError("proxy path not allowed");
89
+ }
90
+ }
91
+ function normalizeOrigin(input) {
92
+ if (input === undefined || input === "")
93
+ return undefined;
94
+ let parsed;
95
+ try {
96
+ parsed = new URL(input);
97
+ }
98
+ catch {
99
+ throw new TypeError("externalOrigin must be an HTTP origin");
100
+ }
101
+ if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.origin !== input || parsed.username !== "" || parsed.password !== "") {
102
+ throw new TypeError("externalOrigin must be an HTTP origin");
116
103
  }
117
- throw new ProxyRuntimePolicyError(`${kind} path is not allowed by proxy runtime policy`);
104
+ return input;
118
105
  }
119
- function normalizeMaxBytes(name, v, defaultValue) {
120
- if (v == null)
121
- return defaultValue;
122
- if (!Number.isFinite(v))
123
- throw new Error(`${name} must be a finite number`);
124
- const n = Math.floor(v);
125
- if (!Number.isSafeInteger(n))
126
- throw new Error(`${name} must be a safe integer`);
127
- if (n < 0)
128
- throw new Error(`${name} must be >= 0`);
129
- if (n === 0)
130
- return defaultValue;
131
- return n;
106
+ function normalizeToken(input) {
107
+ if (input === undefined || input === "")
108
+ return undefined;
109
+ if (input !== input.trim() || /[\u0000-\u0020\u007f]/u.test(input) || input.length > 256) {
110
+ throw new TypeError("runtimeRegistrationToken is invalid");
111
+ }
112
+ return input;
132
113
  }
133
- const DEFAULT_MAX_CONCURRENT_HTTP_STREAMS = 24;
134
- const DEFAULT_MAX_QUEUED_HTTP_REQUESTS = 128;
135
- const DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES = 64 * (1 << 20);
136
- const DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES = 4 * (1 << 20);
137
- function normalizePositiveLimit(name, value, defaultValue) {
138
- if (value == null)
139
- return defaultValue;
140
- if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value <= 0) {
141
- throw new Error(`${name} must be a positive safe integer`);
114
+ const BASE_REQUEST_HEADERS = new Set(["accept", "accept-language", "content-type", "if-match", "if-none-match", "range"]);
115
+ const BASE_RESPONSE_HEADERS = new Set(["accept-ranges", "cache-control", "content-disposition", "content-language", "content-range", "content-type", "etag", "expires", "last-modified", "location"]);
116
+ const FORBIDDEN_HEADERS = new Set(["authorization", "connection", "cookie", "host", "keep-alive", "proxy-authorization", "set-cookie", "transfer-encoding", "upgrade"]);
117
+ function normalizeHeaderNames(values) {
118
+ const result = new Set();
119
+ for (const raw of values ?? []) {
120
+ const value = raw.toLowerCase().trim();
121
+ if (!/^[!#$%&'*+\-.^_`|~0-9a-z]+$/u.test(value) || FORBIDDEN_HEADERS.has(value)) {
122
+ throw new TypeError("proxy header allowlist contains a forbidden name");
123
+ }
124
+ result.add(value);
142
125
  }
143
- return value;
126
+ return result;
144
127
  }
145
- function normalizeNonNegativeLimit(name, value, defaultValue) {
146
- if (value == null)
147
- return defaultValue;
148
- if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0) {
149
- throw new Error(`${name} must be a non-negative safe integer`);
128
+ function filterHeaders(input, base, extra) {
129
+ const result = [];
130
+ for (const entry of input) {
131
+ if (typeof entry?.name !== "string" || typeof entry?.value !== "string")
132
+ continue;
133
+ const name = entry.name.toLowerCase().trim();
134
+ if ((!base.has(name) && !extra.has(name)) || FORBIDDEN_HEADERS.has(name) || /[\r\n]/u.test(entry.value))
135
+ continue;
136
+ result.push(Object.freeze({ name, value: entry.value }));
150
137
  }
151
- return value;
138
+ return result;
152
139
  }
153
- class HttpStreamAdmission {
154
- path;
155
- maxConcurrent;
156
- maxQueued;
157
- maxQueuedBodyBytes;
140
+ function parseRuntimeRequest(raw) {
141
+ const headers = Array.isArray(raw.headers) ? raw.headers.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.name === "string" && typeof entry.value === "string") : [];
142
+ const body = raw.body instanceof ArrayBuffer ? raw.body : undefined;
143
+ return {
144
+ id: typeof raw.id === "string" ? raw.id : "",
145
+ method: typeof raw.method === "string" ? raw.method : "GET",
146
+ path: typeof raw.path === "string" ? raw.path : "",
147
+ headers,
148
+ ...(typeof raw.external_origin === "string" ? { externalOrigin: raw.external_origin } : {}),
149
+ ...(raw.response_flow_control === "chunk_credit_v2" ? { responseFlowControl: "chunk_credit_v2" } : {}),
150
+ ...(body === undefined ? {} : { body }),
151
+ };
152
+ }
153
+ class StreamAdmission {
154
+ concurrent;
155
+ queued;
156
+ queuedBodyBytes;
158
157
  active = 0;
159
- pending = [];
160
- pendingBodyBytes = 0;
158
+ queuedBytes = 0;
161
159
  closed = false;
162
- constructor(path, maxConcurrent, maxQueued, maxQueuedBodyBytes) {
163
- this.path = path;
164
- this.maxConcurrent = maxConcurrent;
165
- this.maxQueued = maxQueued;
166
- this.maxQueuedBodyBytes = maxQueuedBodyBytes;
160
+ queue = [];
161
+ constructor(concurrent, queued, queuedBodyBytes) {
162
+ this.concurrent = concurrent;
163
+ this.queued = queued;
164
+ this.queuedBodyBytes = queuedBodyBytes;
167
165
  }
168
- acquire(bodyBytes, signal) {
166
+ acquire(bytes, signal) {
169
167
  if (this.closed)
170
- return Promise.reject(this.closedError());
171
- if (signal?.aborted)
172
- return Promise.reject(this.abortedError());
173
- if (this.active < this.maxConcurrent && this.pending.length === 0) {
168
+ return Promise.reject(new SessionError("closed"));
169
+ if (signal?.aborted === true)
170
+ return Promise.reject(new SessionError("canceled"));
171
+ if (this.active < this.concurrent && this.queue.length === 0) {
174
172
  this.active++;
175
- return Promise.resolve(this.createRelease());
176
- }
177
- if (this.pending.length >= this.maxQueued) {
178
- return Promise.reject(new FlowersecError({
179
- path: this.path,
180
- stage: "yamux",
181
- code: "resource_exhausted",
182
- message: "proxy runtime HTTP request queue is full",
183
- }));
173
+ return Promise.resolve(this.releaseFunction());
184
174
  }
185
- if (this.pendingBodyBytes + bodyBytes > this.maxQueuedBodyBytes) {
186
- return Promise.reject(new FlowersecError({
187
- path: this.path,
188
- stage: "yamux",
189
- code: "resource_exhausted",
190
- message: "proxy runtime HTTP request body queue is full",
191
- }));
175
+ if (this.queue.length >= this.queued || this.queuedBytes + bytes > this.queuedBodyBytes) {
176
+ return Promise.reject(new SessionError("resource_exhausted"));
192
177
  }
193
178
  return new Promise((resolve, reject) => {
194
- const waiter = {
195
- bodyBytes,
196
- resolve,
197
- reject,
179
+ const entry = {
180
+ bytes,
198
181
  ...(signal === undefined ? {} : { signal }),
182
+ resolve,
183
+ reject: (error) => reject(error),
199
184
  };
200
- waiter.onAbort = () => {
201
- const index = this.pending.indexOf(waiter);
185
+ const onAbort = () => {
186
+ const index = this.queue.indexOf(entry);
202
187
  if (index < 0)
203
188
  return;
204
- this.pending.splice(index, 1);
205
- this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
206
- this.cleanupWaiter(waiter);
207
- reject(this.abortedError());
189
+ this.queue.splice(index, 1);
190
+ this.queuedBytes -= bytes;
191
+ reject(new SessionError("canceled"));
208
192
  };
209
- signal?.addEventListener("abort", waiter.onAbort, { once: true });
210
- this.pending.push(waiter);
211
- this.pendingBodyBytes += bodyBytes;
193
+ signal?.addEventListener("abort", onAbort, { once: true });
194
+ this.queue.push(entry);
195
+ this.queuedBytes += bytes;
212
196
  });
213
197
  }
214
198
  close() {
215
199
  if (this.closed)
216
200
  return;
217
201
  this.closed = true;
218
- for (const waiter of this.pending.splice(0)) {
219
- this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
220
- this.cleanupWaiter(waiter);
221
- waiter.reject(this.closedError());
222
- }
202
+ for (const entry of this.queue.splice(0))
203
+ entry.reject(new SessionError("closed"));
204
+ this.queuedBytes = 0;
223
205
  }
224
- assertOpen() {
225
- if (this.closed)
226
- throw this.closedError();
227
- }
228
- createRelease() {
206
+ releaseFunction() {
229
207
  let released = false;
230
208
  return () => {
231
209
  if (released)
232
210
  return;
233
211
  released = true;
234
- this.active = Math.max(0, this.active - 1);
212
+ this.active--;
235
213
  this.drain();
236
214
  };
237
215
  }
238
216
  drain() {
239
- while (!this.closed && this.active < this.maxConcurrent && this.pending.length > 0) {
240
- const waiter = this.pending.shift();
241
- this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
242
- this.cleanupWaiter(waiter);
243
- if (waiter.signal?.aborted) {
244
- waiter.reject(this.abortedError());
217
+ while (!this.closed && this.active < this.concurrent && this.queue.length > 0) {
218
+ const entry = this.queue.shift();
219
+ this.queuedBytes -= entry.bytes;
220
+ if (entry.signal?.aborted === true) {
221
+ entry.reject(new SessionError("canceled"));
245
222
  continue;
246
223
  }
247
224
  this.active++;
248
- waiter.resolve(this.createRelease());
249
- }
250
- }
251
- cleanupWaiter(waiter) {
252
- if (waiter.onAbort != null) {
253
- waiter.signal?.removeEventListener("abort", waiter.onAbort);
225
+ entry.resolve(this.releaseFunction());
254
226
  }
255
227
  }
256
- abortedError() {
257
- return new AbortError("proxy HTTP request canceled while waiting for stream admission");
258
- }
259
- closedError() {
260
- return new FlowersecError({
261
- path: this.path,
262
- stage: "close",
263
- code: "not_connected",
264
- message: "proxy runtime is disposed",
265
- });
228
+ }
229
+ async function writeChunks(stream, body, chunkBytes, maxBodyBytes, signal) {
230
+ if (body.length > maxBodyBytes)
231
+ throw new SessionError("resource_exhausted");
232
+ for (let offset = 0; offset < body.length; offset += chunkBytes) {
233
+ const chunk = body.subarray(offset, Math.min(body.length, offset + chunkBytes));
234
+ await writeAll(stream, u32be(chunk.length), { signal });
235
+ await writeAll(stream, chunk, { signal });
266
236
  }
237
+ await writeAll(stream, u32be(0), { signal });
267
238
  }
268
- async function writeChunkFrames(stream, body, chunkSize, maxBodyBytes) {
269
- if (maxBodyBytes > 0 && body.length > maxBodyBytes)
270
- throw new Error("request body too large");
271
- const n = Math.max(1, Math.floor(chunkSize));
272
- let off = 0;
273
- while (off < body.length) {
274
- const end = Math.min(body.length, off + n);
275
- const chunk = body.subarray(off, end);
276
- await stream.write(u32be(chunk.length));
277
- await stream.write(chunk);
278
- off = end;
239
+ async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
240
+ let total = 0;
241
+ while (true) {
242
+ const length = readU32be(await reader.readExactly(4), 0);
243
+ if (length === 0)
244
+ return;
245
+ if (length > maxChunkBytes || total + length > maxBodyBytes)
246
+ throw new SessionError("resource_exhausted");
247
+ total += length;
248
+ yield await reader.readExactly(length);
279
249
  }
280
- await stream.write(u32be(0));
281
250
  }
282
- async function readChunkFrames(reader, maxChunkBytes, maxBodyBytes) {
283
- async function* gen() {
284
- let total = 0;
285
- while (true) {
286
- const lenBuf = await reader.readExactly(4);
287
- const n = readU32be(lenBuf, 0);
288
- if (n === 0)
289
- return;
290
- if (maxChunkBytes > 0 && n > maxChunkBytes)
291
- throw new Error("response chunk too large");
292
- total += n;
293
- if (maxBodyBytes > 0 && total > maxBodyBytes)
294
- throw new Error("response body too large");
295
- const payload = await reader.readExactly(n);
296
- yield payload;
297
- }
251
+ function publicFailure(error) {
252
+ if (error instanceof ProxyPolicyError)
253
+ return { status: 403, code: "policy_denied", message: "proxy request denied" };
254
+ if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
255
+ return { status: 503, code: error.code, message: "proxy service unavailable" };
298
256
  }
299
- return gen();
257
+ if (error instanceof SessionError && error.code === "canceled")
258
+ return { status: 499, code: "canceled", message: "proxy request canceled" };
259
+ return { status: 502, code: "operation_failed", message: "proxy request failed" };
300
260
  }
301
- export function createProxyRuntime(opts) {
302
- const client = opts.client;
303
- const cookieJar = opts.cookieJar ?? new CookieJar();
304
- const maxJsonFrameBytes = normalizeMaxBytes("maxJsonFrameBytes", opts.maxJsonFrameBytes, DEFAULT_MAX_JSON_FRAME_BYTES);
305
- const maxChunkBytes = normalizeMaxBytes("maxChunkBytes", opts.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES);
306
- const maxBodyBytes = normalizeMaxBytes("maxBodyBytes", opts.maxBodyBytes, DEFAULT_MAX_BODY_BYTES);
307
- const maxWsFrameBytes = normalizeMaxBytes("maxWsFrameBytes", opts.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES);
308
- const maxWsBufferedAmountBytes = normalizeMaxBytes("maxWsBufferedAmountBytes", opts.maxWsBufferedAmountBytes, DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES);
309
- const maxConcurrentHttpStreams = normalizePositiveLimit("maxConcurrentHttpStreams", opts.maxConcurrentHttpStreams, DEFAULT_MAX_CONCURRENT_HTTP_STREAMS);
310
- const maxQueuedHttpRequests = normalizeNonNegativeLimit("maxQueuedHttpRequests", opts.maxQueuedHttpRequests, DEFAULT_MAX_QUEUED_HTTP_REQUESTS);
311
- const maxQueuedHttpBodyBytes = normalizeNonNegativeLimit("maxQueuedHttpBodyBytes", opts.maxQueuedHttpBodyBytes, DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES);
312
- const httpStreamAdmission = new HttpStreamAdmission(client.path, maxConcurrentHttpStreams, maxQueuedHttpRequests, maxQueuedHttpBodyBytes);
313
- const timeoutMs = normalizeTimeoutMs(opts.timeoutMs);
314
- const extraRequestHeaders = opts.extraRequestHeaders ?? [];
315
- const extraResponseHeaders = opts.extraResponseHeaders ?? [];
316
- const extraWsHeaders = opts.extraWsHeaders ?? [];
317
- const pathPolicy = normalizePathPolicy(opts.pathPolicy);
318
- const externalOriginOverride = normalizeExternalOrigin(opts.externalOrigin);
319
- const runtimeRegistrationToken = normalizeOptionalToken("runtimeRegistrationToken", opts.runtimeRegistrationToken);
320
- const registerRuntime = () => {
321
- void ensureServiceWorkerRuntimeRegistered({
322
- timeoutMs: 2_000,
323
- ...(runtimeRegistrationToken === undefined ? {} : { runtimeRegistrationToken }),
324
- }).catch(() => {
325
- // Best-effort: controllerchange will retry once the active Service Worker is ready.
326
- });
327
- };
328
- const onMessage = (ev) => {
329
- const data = ev.data;
330
- if (data == null || typeof data !== "object")
331
- return;
332
- if (data.type !== "flowersec-proxy:fetch")
261
+ export function createProxyRuntime(options) {
262
+ if (options.session === null || typeof options.session !== "object" || typeof options.session.openStream !== "function") {
263
+ throw new TypeError("proxy runtime requires a Session");
264
+ }
265
+ const maxJsonFrameBytes = positiveLimit("maxJsonFrameBytes", options.maxJsonFrameBytes, SDK_DEFAULTS.proxy.maxJsonFrameBytes);
266
+ const maxChunkBytes = positiveLimit("maxChunkBytes", options.maxChunkBytes, SDK_DEFAULTS.proxy.maxChunkBytes);
267
+ const maxBodyBytes = positiveLimit("maxBodyBytes", options.maxBodyBytes, SDK_DEFAULTS.proxy.maxBodyBytes);
268
+ const maxWsFrameBytes = positiveLimit("maxWsFrameBytes", options.maxWsFrameBytes, SDK_DEFAULTS.proxy.maxWsFrameBytes);
269
+ const maxWsBufferedAmountBytes = positiveLimit("maxWsBufferedAmountBytes", options.maxWsBufferedAmountBytes, DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES);
270
+ const maxConcurrentHttpStreams = strictlyPositiveLimit("maxConcurrentHttpStreams", options.maxConcurrentHttpStreams, DEFAULT_MAX_CONCURRENT_HTTP_STREAMS);
271
+ const maxQueuedHttpRequests = positiveLimit("maxQueuedHttpRequests", options.maxQueuedHttpRequests, DEFAULT_MAX_QUEUED_HTTP_REQUESTS);
272
+ const maxQueuedHttpBodyBytes = positiveLimit("maxQueuedHttpBodyBytes", options.maxQueuedHttpBodyBytes, DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES);
273
+ const timeoutMs = normalizeTimeout(options.timeoutMs);
274
+ const pathPolicy = normalizePathPolicy(options.pathPolicy);
275
+ const externalOrigin = normalizeOrigin(options.externalOrigin);
276
+ const runtimeRegistrationToken = normalizeToken(options.runtimeRegistrationToken);
277
+ const requestExtra = normalizeHeaderNames(options.extraRequestHeaders);
278
+ const responseExtra = normalizeHeaderNames(options.extraResponseHeaders);
279
+ const webSocketExtra = normalizeHeaderNames(options.extraWebSocketHeaders);
280
+ const admission = new StreamAdmission(maxConcurrentHttpStreams, maxQueuedHttpRequests, maxQueuedHttpBodyBytes);
281
+ let disposed = false;
282
+ const register = () => void ensureServiceWorkerRuntimeRegistered({
283
+ timeoutMs: 2_000,
284
+ ...(runtimeRegistrationToken === undefined ? {} : { runtimeRegistrationToken }),
285
+ }).catch(() => undefined);
286
+ const serviceWorker = globalThis.navigator?.serviceWorker;
287
+ const onMessage = (event) => {
288
+ if (event.data === null || typeof event.data !== "object" || event.data.type !== "flowersec-proxy:fetch")
333
289
  return;
334
- const msg = data;
335
- const port = ev.ports?.[0];
336
- if (!port)
290
+ const port = event.ports?.[0];
291
+ if (port === undefined)
337
292
  return;
338
- dispatchFetch(msg.req, port);
293
+ dispatchFetch(parseRuntimeRequest(event.data.req), port);
339
294
  };
340
- const sw = globalThis.navigator?.serviceWorker;
341
- sw?.addEventListener("message", onMessage);
342
- sw?.addEventListener("controllerchange", registerRuntime);
343
- registerRuntime();
344
- const dispatchFetch = (req, port) => {
345
- const ac = new AbortController();
346
- let stream = null;
347
- let releaseAdmission = null;
348
- const usesResponseCredits = req.response_flow_control === "chunk_credit_v1";
349
- let responseCreditAvailable = false;
350
- let responseCreditResolve = null;
351
- const wakeResponseCreditWaiter = () => {
352
- const resolve = responseCreditResolve;
353
- responseCreditResolve = null;
354
- resolve?.();
355
- };
356
- port.onmessage = (ev) => {
357
- const m = ev.data;
358
- if (!m || typeof m !== "object")
359
- return;
360
- if (m.type === "flowersec-proxy:abort") {
361
- responseCreditAvailable = false;
362
- ac.abort("aborted");
363
- wakeResponseCreditWaiter();
364
- return;
295
+ serviceWorker?.addEventListener("message", onMessage);
296
+ serviceWorker?.addEventListener("controllerchange", register);
297
+ register();
298
+ function dispatchFetch(request, port) {
299
+ const controller = new AbortController();
300
+ let stream;
301
+ let release;
302
+ let credit = request.responseFlowControl !== "chunk_credit_v2";
303
+ let creditWake;
304
+ port.onmessage = (event) => {
305
+ if (event.data?.type === "flowersec-proxy:abort") {
306
+ controller.abort();
307
+ creditWake?.();
365
308
  }
366
- if (usesResponseCredits && m.type === "flowersec-proxy:response_credit") {
367
- responseCreditAvailable = true;
368
- wakeResponseCreditWaiter();
309
+ else if (event.data?.type === "flowersec-proxy:response_credit") {
310
+ credit = true;
311
+ creditWake?.();
369
312
  }
370
313
  };
371
- const waitForResponseCredit = async () => {
372
- if (!usesResponseCredits)
373
- return;
374
- if (ac.signal.aborted)
375
- throw new AbortError("aborted");
376
- while (!responseCreditAvailable) {
377
- if (ac.signal.aborted)
378
- throw new AbortError("aborted");
379
- await new Promise((resolve) => {
380
- responseCreditResolve = resolve;
381
- });
314
+ const waitForCredit = async () => {
315
+ while (!credit) {
316
+ if (controller.signal.aborted)
317
+ throw new SessionError("canceled");
318
+ await new Promise((resolve) => { creditWake = resolve; });
319
+ creditWake = undefined;
382
320
  }
383
- if (ac.signal.aborted)
384
- throw new AbortError("aborted");
385
- responseCreditAvailable = false;
321
+ credit = request.responseFlowControl !== "chunk_credit_v2";
386
322
  };
387
323
  void (async () => {
388
324
  try {
389
- const path = pathOnly(req.path);
390
- assertPathPolicyAllows("http", path, pathPolicy);
391
- const requestID = req.id.trim() !== "" ? req.id : randomB64u(18);
392
- const externalOrigin = externalOriginOverride ?? normalizeExternalOrigin(req.external_origin);
393
- const body = req.body != null ? new Uint8Array(req.body) : new Uint8Array();
394
- if (maxBodyBytes > 0 && body.length > maxBodyBytes)
395
- throw new Error("request body too large");
396
- releaseAdmission = await httpStreamAdmission.acquire(body.byteLength, ac.signal);
397
- httpStreamAdmission.assertOpen();
398
- stream = await client.openStream(PROXY_KIND_HTTP1, { signal: ac.signal });
399
- const reader = createByteReader(stream, { signal: ac.signal });
400
- const filteredReqHeaders = filterRequestHeaders(req.headers, { extraAllowed: extraRequestHeaders });
401
- const cookieHeader = cookieJar.getCookieHeader(cookiePathFromRequestPath(path));
402
- const reqHeaders = cookieHeader === "" ? filteredReqHeaders : [...filteredReqHeaders, { name: "cookie", value: cookieHeader }];
403
- await writeJsonFrame(stream, {
404
- v: PROXY_PROTOCOL_VERSION,
325
+ if (disposed)
326
+ throw new SessionError("closed");
327
+ const path = normalizePath(request.path);
328
+ enforcePathPolicy("http", path, pathPolicy);
329
+ const body = request.body === undefined ? new Uint8Array() : new Uint8Array(request.body);
330
+ release = await admission.acquire(body.length, controller.signal);
331
+ stream = await options.session.openStream(PROXY_HTTP_STREAM_KIND, {
332
+ signal: controller.signal,
333
+ metadata: createStreamMetadata({ protocol: "flowersec.proxy.http", version: 2 }),
334
+ });
335
+ const reader = new ProxyByteReader(stream, { signal: controller.signal });
336
+ const requestID = request.id.trim() === "" ? randomID() : request.id;
337
+ const headers = filterHeaders(request.headers, BASE_REQUEST_HEADERS, requestExtra);
338
+ const requestOrigin = externalOrigin ?? normalizeOrigin(request.externalOrigin);
339
+ await writeJsonFrame({ write: async (data) => await writeAll(stream, data, { signal: controller.signal }) }, {
340
+ v: PROXY_WIRE_VERSION,
405
341
  request_id: requestID,
406
- method: req.method,
342
+ method: request.method.toUpperCase(),
407
343
  path,
408
- headers: reqHeaders,
409
- ...(externalOrigin === undefined ? {} : { external_origin: externalOrigin }),
410
- timeout_ms: timeoutMs
344
+ headers,
345
+ ...(requestOrigin === undefined ? {} : { external_origin: requestOrigin }),
346
+ ...(timeoutMs === 0 ? {} : { timeout_ms: timeoutMs }),
411
347
  });
412
- await writeChunkFrames(stream, body, Math.min(64 * 1024, maxChunkBytes), maxBodyBytes);
413
- const respMeta = (await readJsonFrame(reader, maxJsonFrameBytes));
414
- if (respMeta.v !== PROXY_PROTOCOL_VERSION || respMeta.request_id !== requestID) {
415
- throw new Error("invalid upstream response meta");
416
- }
417
- if (!respMeta.ok) {
418
- const msg = respMeta.error?.message ?? "upstream error";
419
- port.postMessage({
420
- type: "flowersec-proxy:response_error",
421
- status: 502,
422
- ...(respMeta.error?.code === undefined ? {} : { code: respMeta.error.code }),
423
- message: msg,
424
- });
425
- await stream.reset(new Error(msg));
426
- stream = null;
427
- return;
348
+ await writeChunks(stream, body, maxChunkBytes, maxBodyBytes, controller.signal);
349
+ const response = await readJsonFrame(reader, maxJsonFrameBytes);
350
+ if (response.v !== PROXY_WIRE_VERSION || response.request_id !== requestID || response.ok !== true || !Number.isInteger(response.status)) {
351
+ throw new Error("invalid proxy response");
428
352
  }
429
- const status = Math.max(0, Math.floor(respMeta.status ?? 502));
430
- const rawHeaders = Array.isArray(respMeta.headers) ? respMeta.headers : [];
431
- const { passthrough, setCookie } = filterResponseHeaders(rawHeaders, { extraAllowed: extraResponseHeaders });
432
- cookieJar.updateFromSetCookieHeaders(setCookie, path);
433
- port.postMessage({ type: "flowersec-proxy:response_meta", status, headers: passthrough });
434
- const chunks = await readChunkFrames(reader, maxChunkBytes, maxBodyBytes);
435
- for await (const chunk of chunks) {
436
- await waitForResponseCredit();
437
- if (ac.signal.aborted)
438
- throw new AbortError("aborted");
439
- // Always transfer an ArrayBuffer (SharedArrayBuffer is not transferable).
440
- const ab = chunk.slice().buffer;
441
- port.postMessage({ type: "flowersec-proxy:response_chunk", data: ab }, [ab]);
353
+ const headersOut = filterHeaders(response.headers ?? [], BASE_RESPONSE_HEADERS, responseExtra);
354
+ port.postMessage({ type: "flowersec-proxy:response_meta", status: response.status, headers: headersOut });
355
+ for await (const chunk of readChunks(reader, maxChunkBytes, maxBodyBytes)) {
356
+ await waitForCredit();
357
+ const data = chunk.slice().buffer;
358
+ port.postMessage({ type: "flowersec-proxy:response_chunk", data }, [data]);
442
359
  }
443
360
  port.postMessage({ type: "flowersec-proxy:response_end" });
444
361
  await stream.close();
445
- stream = null;
362
+ stream = undefined;
446
363
  }
447
- catch (e) {
448
- const msg = e instanceof Error ? e.message : String(e);
449
- const code = isFlowersecError(e) ? e.code : undefined;
450
- const status = e instanceof ProxyRuntimePolicyError
451
- ? e.status
452
- : code === "resource_exhausted" || code === "not_connected"
453
- ? 503
454
- : 502;
455
- port.postMessage({
456
- type: "flowersec-proxy:response_error",
457
- status,
458
- ...(code === undefined ? {} : { code }),
459
- message: msg,
460
- });
461
- await stream?.reset(new Error(msg));
364
+ catch (error) {
365
+ const failure = publicFailure(error);
366
+ port.postMessage({ type: "flowersec-proxy:response_error", ...failure });
367
+ await stream?.reset().catch(() => undefined);
462
368
  }
463
369
  finally {
464
- wakeResponseCreditWaiter();
465
- releaseAdmission?.();
466
- try {
467
- port.close();
468
- }
469
- catch {
470
- // Best-effort.
471
- }
370
+ release?.();
371
+ port.close();
472
372
  }
473
373
  })();
474
- };
475
- async function openWebSocketStream(pathRaw, wsOpts = {}) {
476
- const path = pathOnly(pathRaw);
477
- assertPathPolicyAllows("websocket", path, pathPolicy);
478
- const openOpts = wsOpts.signal ? { signal: wsOpts.signal } : undefined;
479
- const stream = await client.openStream(PROXY_KIND_WS, openOpts);
480
- const reader = createByteReader(stream, openOpts);
481
- const cookie = cookieJar.getCookieHeader(cookiePathFromRequestPath(path));
482
- const headers = [];
483
- const protos = wsOpts.protocols?.filter((p) => p.trim() !== "") ?? [];
484
- if (protos.length > 0)
485
- headers.push({ name: "sec-websocket-protocol", value: protos.join(", ") });
486
- if (cookie !== "")
487
- headers.push({ name: "cookie", value: cookie });
488
- const filtered = filterWsOpenHeaders(headers, { extraAllowed: extraWsHeaders });
489
- await writeJsonFrame(stream, { v: PROXY_PROTOCOL_VERSION, conn_id: randomB64u(18), path, headers: filtered });
490
- const resp = (await readJsonFrame(reader, maxJsonFrameBytes));
491
- if (resp.v !== PROXY_PROTOCOL_VERSION || resp.ok !== true) {
492
- const msg = resp.error?.message ?? "upstream ws open failed";
493
- await stream.reset(new Error(msg));
494
- throw new Error(msg);
374
+ }
375
+ async function openWebSocketStream(input, openOptions = {}) {
376
+ if (disposed)
377
+ throw new SessionError("closed");
378
+ const path = normalizePath(input);
379
+ enforcePathPolicy("websocket", path, pathPolicy);
380
+ const stream = await options.session.openStream(PROXY_WEBSOCKET_STREAM_KIND, {
381
+ ...(openOptions.signal === undefined ? {} : { signal: openOptions.signal }),
382
+ metadata: createStreamMetadata({ protocol: "flowersec.proxy.websocket", version: 2 }),
383
+ });
384
+ try {
385
+ const protocols = (openOptions.protocols ?? []).filter((value) => value.trim() !== "" && value === value.trim());
386
+ const headers = filterHeaders(protocols.length === 0 ? [] : [{ name: "sec-websocket-protocol", value: protocols.join(", ") }], new Set(["sec-websocket-protocol"]), webSocketExtra);
387
+ await writeJsonFrame({ write: async (data) => await writeAll(stream, data, openOptions.signal === undefined ? {} : { signal: openOptions.signal }) }, {
388
+ v: PROXY_WIRE_VERSION,
389
+ conn_id: randomID(),
390
+ path,
391
+ headers,
392
+ });
393
+ const response = await readJsonFrame(new ProxyByteReader(stream, openOptions.signal === undefined ? {} : { signal: openOptions.signal }), maxJsonFrameBytes);
394
+ if (response.v !== PROXY_WIRE_VERSION || response.ok !== true || (response.protocol !== undefined && typeof response.protocol !== "string")) {
395
+ throw new Error("proxy WebSocket open failed");
396
+ }
397
+ return Object.freeze({ stream, protocol: response.protocol ?? "" });
398
+ }
399
+ catch (error) {
400
+ await stream.reset().catch(() => undefined);
401
+ throw error instanceof SessionError ? error : new SessionError("operation_failed");
495
402
  }
496
- return { stream, protocol: resp.protocol ?? "" };
497
403
  }
498
- return {
499
- limits: {
404
+ return Object.freeze({
405
+ limits: Object.freeze({
500
406
  maxJsonFrameBytes,
501
407
  maxChunkBytes,
502
408
  maxBodyBytes,
@@ -505,74 +411,55 @@ export function createProxyRuntime(opts) {
505
411
  maxConcurrentHttpStreams,
506
412
  maxQueuedHttpRequests,
507
413
  maxQueuedHttpBodyBytes,
508
- },
414
+ }),
509
415
  dispatchFetch,
510
416
  openWebSocketStream,
511
417
  dispose: () => {
512
- httpStreamAdmission.close();
513
- sw?.removeEventListener("message", onMessage);
514
- sw?.removeEventListener("controllerchange", registerRuntime);
515
- }
516
- };
418
+ if (disposed)
419
+ return;
420
+ disposed = true;
421
+ admission.close();
422
+ serviceWorker?.removeEventListener("message", onMessage);
423
+ serviceWorker?.removeEventListener("controllerchange", register);
424
+ },
425
+ });
517
426
  }
518
- export async function ensureServiceWorkerRuntimeRegistered(opts = {}) {
519
- const ctl = globalThis.navigator?.serviceWorker?.controller;
520
- if (!ctl || typeof ctl.postMessage !== "function")
427
+ export async function ensureServiceWorkerRuntimeRegistered(options = {}) {
428
+ const controller = globalThis.navigator?.serviceWorker?.controller;
429
+ if (controller === null || controller === undefined || typeof controller.postMessage !== "function")
521
430
  return;
522
- const timeoutMs = Math.max(0, Math.floor(opts.timeoutMs ?? 2_000));
523
- const runtimeRegistrationToken = normalizeOptionalToken("runtimeRegistrationToken", opts.runtimeRegistrationToken);
524
- const ch = new MessageChannel();
431
+ const timeoutMs = options.timeoutMs ?? 2_000;
432
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60_000)
433
+ throw new TypeError("invalid runtime registration timeout");
434
+ const token = normalizeToken(options.runtimeRegistrationToken);
435
+ const channel = new MessageChannel();
525
436
  await new Promise((resolve, reject) => {
526
- let done = false;
527
- let timer = null;
437
+ let settled = false;
528
438
  const finish = (error) => {
529
- if (done)
439
+ if (settled)
530
440
  return;
531
- done = true;
532
- if (timer != null)
441
+ settled = true;
442
+ if (timer !== undefined)
533
443
  clearTimeout(timer);
534
- ch.port1.onmessage = null;
535
- ch.port1.onmessageerror = null;
536
- try {
537
- ch.port1.close();
538
- }
539
- catch {
540
- // Best-effort.
541
- }
542
- if (error == null) {
543
- resolve();
544
- return;
545
- }
546
- reject(error instanceof Error ? error : new Error(String(error)));
444
+ channel.port1.close();
445
+ error === undefined ? resolve() : reject(error);
547
446
  };
548
- ch.port1.onmessage = (ev) => {
549
- const data = ev.data;
550
- if (data == null || typeof data !== "object")
447
+ channel.port1.onmessage = (event) => {
448
+ if (event.data?.type !== "flowersec-proxy:register-runtime-ack")
551
449
  return;
552
- if (data.type !== "flowersec-proxy:register-runtime-ack")
553
- return;
554
- if (data.ok !== true) {
555
- finish(new Error("service worker runtime registration was rejected"));
556
- return;
557
- }
558
- finish();
450
+ finish(event.data.ok === true ? undefined : new Error("service worker runtime registration rejected"));
559
451
  };
560
- ch.port1.onmessageerror = () => {
561
- finish(new Error("service worker runtime registration failed"));
562
- };
563
- if (timeoutMs > 0) {
564
- timer = setTimeout(() => {
565
- finish(new Error("service worker runtime registration timed out"));
566
- }, timeoutMs);
567
- }
452
+ channel.port1.onmessageerror = () => finish(new Error("service worker runtime registration failed"));
453
+ const timer = timeoutMs === 0 ? undefined : setTimeout(() => finish(new Error("service worker runtime registration timed out")), timeoutMs);
568
454
  try {
569
- ctl.postMessage({
455
+ controller.postMessage({
570
456
  type: "flowersec-proxy:register-runtime",
571
- ...(runtimeRegistrationToken === undefined ? {} : { token: runtimeRegistrationToken }),
572
- }, [ch.port2]);
457
+ version: 2,
458
+ ...(token === undefined ? {} : { token }),
459
+ }, [channel.port2]);
573
460
  }
574
- catch (error) {
575
- finish(error);
461
+ catch {
462
+ finish(new Error("service worker runtime registration failed"));
576
463
  }
577
464
  });
578
465
  }