@floegence/flowersec-core 0.27.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 (268) 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 -27
  34. package/dist/facade.js +6 -24
  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 -81
  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 -13
  148. package/dist/browser/connect.js +0 -31
  149. package/dist/browser/reconnectConfig.d.ts +0 -15
  150. package/dist/browser/reconnectConfig.js +0 -20
  151. package/dist/client-connect/common.d.ts +0 -26
  152. package/dist/client-connect/common.js +0 -167
  153. package/dist/client-connect/connectCore.d.ts +0 -61
  154. package/dist/client-connect/connectCore.js +0 -572
  155. package/dist/client-connect/contract.d.ts +0 -8
  156. package/dist/client-connect/contract.js +0 -51
  157. package/dist/client-connect/termination.d.ts +0 -6
  158. package/dist/client-connect/termination.js +0 -7
  159. package/dist/client-connect/transportSecurity.d.ts +0 -27
  160. package/dist/client-connect/transportSecurity.js +0 -199
  161. package/dist/client-connect/tunnelAttachCloseReason.d.ts +0 -3
  162. package/dist/client-connect/tunnelAttachCloseReason.js +0 -23
  163. package/dist/client.d.ts +0 -23
  164. package/dist/connect/artifact.d.ts +0 -36
  165. package/dist/connect/artifact.js +0 -213
  166. package/dist/connect/resolveArtifact.d.ts +0 -22
  167. package/dist/connect/resolveArtifact.js +0 -87
  168. package/dist/controlplane/channelInit.d.ts +0 -27
  169. package/dist/controlplane/channelInit.js +0 -116
  170. package/dist/controlplane/http.d.ts +0 -16
  171. package/dist/controlplane/http.js +0 -59
  172. package/dist/controlplane/index.d.ts +0 -6
  173. package/dist/controlplane/index.js +0 -5
  174. package/dist/controlplane/issuer.d.ts +0 -18
  175. package/dist/controlplane/issuer.js +0 -113
  176. package/dist/controlplane/request.d.ts +0 -51
  177. package/dist/controlplane/request.js +0 -273
  178. package/dist/controlplane/token.d.ts +0 -33
  179. package/dist/controlplane/token.js +0 -119
  180. package/dist/direct-client/connect.d.ts +0 -9
  181. package/dist/direct-client/connect.js +0 -90
  182. package/dist/direct-client/index.d.ts +0 -1
  183. package/dist/direct-client/index.js +0 -1
  184. package/dist/e2ee/constants.d.ts +0 -9
  185. package/dist/e2ee/constants.js +0 -18
  186. package/dist/e2ee/errors.d.ts +0 -5
  187. package/dist/e2ee/errors.js +0 -8
  188. package/dist/e2ee/framing.d.ts +0 -12
  189. package/dist/e2ee/framing.js +0 -57
  190. package/dist/e2ee/handshake.d.ts +0 -88
  191. package/dist/e2ee/handshake.js +0 -353
  192. package/dist/e2ee/index.d.ts +0 -7
  193. package/dist/e2ee/index.js +0 -7
  194. package/dist/e2ee/kdf.d.ts +0 -15
  195. package/dist/e2ee/kdf.js +0 -39
  196. package/dist/e2ee/record.d.ts +0 -11
  197. package/dist/e2ee/record.js +0 -80
  198. package/dist/e2ee/secureChannel.d.ts +0 -93
  199. package/dist/e2ee/secureChannel.js +0 -342
  200. package/dist/e2ee/transcript.d.ts +0 -23
  201. package/dist/e2ee/transcript.js +0 -31
  202. package/dist/endpoint/index.d.ts +0 -84
  203. package/dist/endpoint/index.js +0 -557
  204. package/dist/endpoint/node.d.ts +0 -9
  205. package/dist/endpoint/node.js +0 -51
  206. package/dist/gen/flowersec/controlplane/v1.gen.d.ts +0 -36
  207. package/dist/gen/flowersec/controlplane/v1.gen.js +0 -135
  208. package/dist/gen/flowersec/direct/v1.gen.d.ts +0 -21
  209. package/dist/gen/flowersec/direct/v1.gen.js +0 -101
  210. package/dist/gen/flowersec/e2ee/v1.gen.d.ts +0 -68
  211. package/dist/gen/flowersec/e2ee/v1.gen.js +0 -194
  212. package/dist/gen/flowersec/rpc/v1.gen.d.ts +0 -30
  213. package/dist/gen/flowersec/rpc/v1.gen.js +0 -107
  214. package/dist/gen/flowersec/tunnel/v1.gen.d.ts +0 -23
  215. package/dist/gen/flowersec/tunnel/v1.gen.js +0 -104
  216. package/dist/index.d.ts +0 -22
  217. package/dist/index.js +0 -22
  218. package/dist/node/connect.d.ts +0 -10
  219. package/dist/node/connect.js +0 -29
  220. package/dist/node/reconnectConfig.d.ts +0 -16
  221. package/dist/node/reconnectConfig.js +0 -20
  222. package/dist/observability/index.d.ts +0 -1
  223. package/dist/observability/index.js +0 -1
  224. package/dist/observability/observer.d.ts +0 -52
  225. package/dist/observability/observer.js +0 -314
  226. package/dist/proxy/appWindow.d.ts +0 -37
  227. package/dist/proxy/appWindow.js +0 -273
  228. package/dist/proxy/bootstrap.d.ts +0 -35
  229. package/dist/proxy/bootstrap.js +0 -119
  230. package/dist/proxy/constants.d.ts +0 -7
  231. package/dist/proxy/constants.js +0 -8
  232. package/dist/proxy/controllerWindow.d.ts +0 -12
  233. package/dist/proxy/controllerWindow.js +0 -384
  234. package/dist/proxy/cookieJar.d.ts +0 -7
  235. package/dist/proxy/cookieJar.js +0 -136
  236. package/dist/proxy/headerPolicy.d.ts +0 -14
  237. package/dist/proxy/headerPolicy.js +0 -134
  238. package/dist/proxy/portStream.d.ts +0 -5
  239. package/dist/proxy/portStream.js +0 -256
  240. package/dist/proxy/preset.d.ts +0 -29
  241. package/dist/proxy/preset.js +0 -111
  242. package/dist/proxy/runtimeScope.d.ts +0 -57
  243. package/dist/proxy/runtimeScope.js +0 -218
  244. package/dist/proxy/server.d.ts +0 -28
  245. package/dist/proxy/server.js +0 -833
  246. package/dist/proxy/windowBridgeProtocol.d.ts +0 -65
  247. package/dist/proxy/windowBridgeProtocol.js +0 -11
  248. package/dist/reconnect/artifactControlplane.d.ts +0 -16
  249. package/dist/reconnect/artifactControlplane.js +0 -35
  250. package/dist/reconnect/index.d.ts +0 -36
  251. package/dist/reconnect/index.js +0 -388
  252. package/dist/rpc-proxy/index.d.ts +0 -1
  253. package/dist/rpc-proxy/index.js +0 -1
  254. package/dist/rpc-proxy/rpcProxy.d.ts +0 -1
  255. package/dist/rpc-proxy/rpcProxy.js +0 -1
  256. package/dist/streamhello/index.d.ts +0 -1
  257. package/dist/streamhello/index.js +0 -1
  258. package/dist/streamhello/streamHello.d.ts +0 -3
  259. package/dist/streamhello/streamHello.js +0 -13
  260. package/dist/streamio/index.d.ts +0 -32
  261. package/dist/streamio/index.js +0 -87
  262. package/dist/tunnel-client/connect.d.ts +0 -9
  263. package/dist/tunnel-client/connect.js +0 -147
  264. package/dist/tunnel-client/index.d.ts +0 -1
  265. package/dist/tunnel-client/index.js +0 -1
  266. package/dist/ws/index.d.ts +0 -1
  267. package/dist/ws/index.js +0 -1
  268. /package/dist/{client.js → rpc/wire.js} +0 -0
@@ -1,626 +1,239 @@
1
- import { DEFAULT_MAX_BODY_BYTES } from "./constants.js";
2
- function normalizePathPrefix(name, v) {
3
- const s = typeof v === "string" ? v.trim() : "";
4
- if (s === "")
5
- return "";
6
- if (!s.startsWith("/"))
7
- throw new Error(`${name} must start with "/"`);
8
- if (s.startsWith("//"))
9
- throw new Error(`${name} must not start with "//"`);
10
- if (/[ \t\r\n]/.test(s))
11
- throw new Error(`${name} must not contain whitespace`);
12
- if (s.includes("://"))
13
- throw new Error(`${name} must not include scheme/host`);
14
- return s;
15
- }
16
- function normalizeRootRelativeScriptURL(name, v) {
17
- if (typeof v !== "string")
18
- throw new Error(`${name} must be a string`);
19
- const s = v.trim();
20
- if (s === "")
21
- throw new Error(`${name} must be non-empty`);
22
- if (s !== v)
23
- throw new Error(`${name} must not contain leading or trailing whitespace`);
24
- if (!s.startsWith("/"))
25
- throw new Error(`${name} must start with "/"`);
26
- if (s.startsWith("//"))
27
- throw new Error(`${name} must not start with "//"`);
28
- if (s.includes("://"))
29
- throw new Error(`${name} must not include scheme/host`);
30
- if (s.includes("\\"))
31
- throw new Error(`${name} must not contain backslash`);
32
- if (/[\s\u0000-\u001f\u007f]/.test(s))
33
- throw new Error(`${name} must not contain whitespace or control characters`);
34
- if (/[<>"'`]/.test(s))
35
- throw new Error(`${name} must not contain HTML attribute delimiters`);
36
- return s;
37
- }
38
- function normalizePathList(name, input) {
39
- const out = [];
40
- if (input == null || input.length === 0)
41
- return out;
42
- for (const raw of input) {
43
- const s = normalizePathPrefix(name, raw);
44
- if (s === "")
45
- continue;
46
- out.push(s);
47
- }
48
- return Array.from(new Set(out));
49
- }
50
- function normalizeMessageTypeList(name, input) {
51
- const out = [];
52
- if (input == null || input.length === 0)
53
- return out;
54
- for (const raw of input) {
55
- const s = typeof raw === "string" ? raw.trim() : "";
56
- if (s === "")
57
- continue;
58
- if (/[\r\n]/.test(s))
59
- throw new Error(`${name} must not contain newline`);
60
- out.push(s);
61
- }
62
- return Array.from(new Set(out));
63
- }
64
- function normalizeOptionalToken(name, input) {
65
- if (input == null)
66
- return "";
67
- const s = String(input);
68
- if (s === "")
69
- return "";
70
- if (s.trim() !== s)
71
- throw new Error(`${name} must not contain leading or trailing whitespace`);
72
- if (/[\s\u0000-\u001f\u007f]/.test(s))
73
- throw new Error(`${name} must not contain whitespace or control characters`);
74
- return s;
75
- }
76
- const defaultMaxInjectHTMLBytes = 2 * 1024 * 1024;
77
- function normalizeMaxBytes(name, v, defaultValue) {
78
- if (v == null)
79
- return defaultValue;
80
- if (typeof v !== "number" || !Number.isFinite(v))
81
- throw new Error(`${name} must be a finite number`);
82
- const n = Math.floor(v);
83
- if (!Number.isSafeInteger(n))
84
- throw new Error(`${name} must be a safe integer`);
85
- if (n < 0)
86
- throw new Error(`${name} must be >= 0`);
87
- if (n === 0)
88
- return defaultValue;
89
- return n;
90
- }
91
- // createProxyServiceWorkerScript returns a Service Worker script that forwards fetches to a runtime
92
- // in a controlled window via postMessage + MessageChannel.
93
- //
94
- // The runtime side is implemented by createProxyRuntime(...) in this package.
95
- export function createProxyServiceWorkerScript(opts = {}) {
96
- const sameOriginOnly = opts.sameOriginOnly ?? true;
97
- if (typeof sameOriginOnly !== "boolean") {
98
- throw new Error("sameOriginOnly must be a boolean");
99
- }
100
- const maxRequestBodyBytes = normalizeMaxBytes("maxRequestBodyBytes", opts.maxRequestBodyBytes, DEFAULT_MAX_BODY_BYTES);
101
- const maxInjectHTMLBytes = normalizeMaxBytes("maxInjectHTMLBytes", opts.maxInjectHTMLBytes, defaultMaxInjectHTMLBytes);
102
- const proxyPathPrefix = normalizePathPrefix("proxyPathPrefix", opts.proxyPathPrefix);
103
- const stripProxyPathPrefix = opts.stripProxyPathPrefix ?? false;
104
- if (typeof stripProxyPathPrefix !== "boolean") {
105
- throw new Error("stripProxyPathPrefix must be a boolean");
106
- }
107
- const passthroughPaths = normalizePathList("passthrough.paths", opts.passthrough?.paths);
108
- const passthroughPrefixes = normalizePathList("passthrough.prefixes", opts.passthrough?.prefixes);
109
- const forwardFetchMessageTypes = normalizeMessageTypeList("forwardFetchMessageTypes", opts.forwardFetchMessageTypes);
110
- const keepScriptPathSuffixes = normalizePathList("conflictHints.keepScriptPathSuffixes", opts.conflictHints?.keepScriptPathSuffixes);
111
- const windowTarget = opts.windowTarget ?? "registered_runtime";
112
- if (windowTarget !== "registered_runtime" && windowTarget !== "request_client") {
113
- throw new Error('windowTarget must be either "registered_runtime" or "request_client"');
114
- }
115
- const windowClientMessageType = (() => {
116
- const normalized = typeof opts.windowClientMessageType === "string" ? opts.windowClientMessageType.trim() : "flowersec-proxy:fetch";
117
- if (normalized === "")
118
- throw new Error("windowClientMessageType must be non-empty");
119
- if (/[\r\n]/.test(normalized))
120
- throw new Error("windowClientMessageType must not contain newline");
121
- return normalized;
122
- })();
123
- const runtimeRegistrationToken = normalizeOptionalToken("runtimeRegistrationToken", opts.runtimeRegistrationToken);
124
- const runtimeClientPathPrefix = normalizePathPrefix("runtimeClientPathPrefix", opts.runtimeClientPathPrefix);
125
- const injectHTML = opts.injectHTML ?? null;
126
- // Injection mode defaults to inline_module when injectHTML is provided.
127
- const injectMode = injectHTML?.mode ?? "inline_module";
128
- const runtimeGlobal = injectHTML != null ? (injectHTML.runtimeGlobal?.trim() ?? "__flowersecProxyRuntime") : "__flowersecProxyRuntime";
129
- if (injectHTML != null && runtimeGlobal === "") {
130
- throw new Error("injectHTML.runtimeGlobal must be non-empty");
131
- }
132
- const excludeInjectPrefixes = normalizePathList("injectHTML.excludePathPrefixes", injectHTML?.excludePathPrefixes);
133
- const stripValidatorHeaders = injectHTML?.stripValidatorHeaders ?? true;
134
- if (typeof stripValidatorHeaders !== "boolean") {
135
- throw new Error("injectHTML.stripValidatorHeaders must be a boolean");
136
- }
137
- const setNoStore = injectHTML?.setNoStore ?? true;
138
- if (typeof setNoStore !== "boolean") {
139
- throw new Error("injectHTML.setNoStore must be a boolean");
140
- }
141
- let proxyModuleUrl = "";
142
- let injectScriptUrl = "";
143
- if (injectHTML != null) {
144
- if (injectMode === "inline_module") {
145
- // Note: union type guarantees proxyModuleUrl exists, but keep a runtime check for JS callers.
146
- proxyModuleUrl =
147
- "proxyModuleUrl" in injectHTML && typeof injectHTML.proxyModuleUrl === "string"
148
- ? injectHTML.proxyModuleUrl.trim()
149
- : "";
150
- if (proxyModuleUrl === "") {
151
- throw new Error("injectHTML.proxyModuleUrl must be non-empty");
152
- }
1
+ function strings(name, values, max = 64) {
2
+ if ((values?.length ?? 0) > max)
3
+ throw new TypeError(`${name} contains too many entries`);
4
+ const result = [];
5
+ for (const raw of values ?? []) {
6
+ if (typeof raw !== "string" || raw === "" || raw !== raw.trim() || /[\u0000-\u001f\u007f]/u.test(raw)) {
7
+ throw new TypeError(`${name} contains an invalid entry`);
153
8
  }
154
- else {
155
- if (!("scriptUrl" in injectHTML))
156
- throw new Error("injectHTML.scriptUrl must be non-empty");
157
- injectScriptUrl = normalizeRootRelativeScriptURL("injectHTML.scriptUrl", injectHTML.scriptUrl);
158
- }
159
- }
160
- return `// Generated by @floegence/flowersec-core/proxy
161
- const SAME_ORIGIN_ONLY = ${JSON.stringify(sameOriginOnly)};
162
- const PROXY_PATH_PREFIX = ${JSON.stringify(proxyPathPrefix)};
163
- const STRIP_PROXY_PATH_PREFIX = ${JSON.stringify(stripProxyPathPrefix)};
164
-
165
- const PASSTHROUGH_PATHS = new Set(${JSON.stringify(passthroughPaths)});
166
- const PASSTHROUGH_PREFIXES = ${JSON.stringify(passthroughPrefixes)};
167
-
168
- const INJECT_HTML = ${JSON.stringify(injectHTML != null)};
169
- const INJECT_MODE = ${JSON.stringify(injectMode)};
170
- const PROXY_MODULE_URL = ${JSON.stringify(proxyModuleUrl)};
171
- const INJECT_SCRIPT_URL = ${JSON.stringify(injectScriptUrl)};
172
- const RUNTIME_GLOBAL = ${JSON.stringify(runtimeGlobal)};
173
- const INJECT_EXCLUDE_PREFIXES = ${JSON.stringify(excludeInjectPrefixes)};
174
- const INJECT_STRIP_VALIDATOR_HEADERS = ${JSON.stringify(stripValidatorHeaders)};
175
- const INJECT_SET_NO_STORE = ${JSON.stringify(setNoStore)};
176
-
177
- const MAX_REQUEST_BODY_BYTES = ${JSON.stringify(maxRequestBodyBytes)};
178
- const MAX_INJECT_HTML_BYTES = ${JSON.stringify(maxInjectHTMLBytes)};
179
- const FORWARD_FETCH_MESSAGE_TYPES = new Set(${JSON.stringify(forwardFetchMessageTypes)});
180
- const WINDOW_TARGET = ${JSON.stringify(windowTarget)};
181
- const WINDOW_CLIENT_MESSAGE_TYPE = ${JSON.stringify(windowClientMessageType)};
182
- const RUNTIME_REGISTRATION_TOKEN = ${JSON.stringify(runtimeRegistrationToken)};
183
- const RUNTIME_CLIENT_PATH_PREFIX = ${JSON.stringify(runtimeClientPathPrefix)};
184
- const CONFLICT_HINT_KEEP_SCRIPT_SUFFIXES = ${JSON.stringify(keepScriptPathSuffixes)};
185
-
186
- const INJECT_STRIP_HEADER_NAMES = new Set(["content-length", "etag", "last-modified", "content-md5"]);
187
-
188
- let runtimeClientId = null;
189
-
190
- self.addEventListener("install", (event) => {
191
- // Take over as soon as possible.
192
- event.waitUntil(self.skipWaiting());
193
- });
194
-
195
- self.addEventListener("activate", (event) => {
196
- runtimeClientId = null;
197
- event.waitUntil(self.clients.claim());
198
- });
199
-
200
- self.addEventListener("message", (event) => {
201
- const data = event.data;
202
- if (!data || typeof data !== "object") return;
203
- const msgType = typeof data.type === "string" ? data.type : "";
204
- if (msgType === "flowersec-proxy:register-runtime") {
205
- event.waitUntil((async () => {
206
- const sourceId = event.source && typeof event.source.id === "string" ? event.source.id : "";
207
- let ok = sourceId !== "";
208
- if (ok && RUNTIME_REGISTRATION_TOKEN) {
209
- ok = typeof data.token === "string" && data.token === RUNTIME_REGISTRATION_TOKEN;
210
- }
211
- if (ok && RUNTIME_CLIENT_PATH_PREFIX) {
212
- const c = await self.clients.get(sourceId);
213
- if (!c || typeof c.url !== "string") {
214
- ok = false;
215
- } else {
216
- try {
217
- const u = new URL(c.url);
218
- ok = u.origin === self.location.origin && u.pathname.startsWith(RUNTIME_CLIENT_PATH_PREFIX);
219
- } catch {
220
- ok = false;
221
- }
9
+ if (!result.includes(raw))
10
+ result.push(raw);
11
+ }
12
+ return Object.freeze(result);
13
+ }
14
+ function bounded(name, value, fallback, maximum) {
15
+ const result = value ?? fallback;
16
+ if (!Number.isSafeInteger(result) || result <= 0 || result > maximum)
17
+ throw new TypeError(`${name} is invalid`);
18
+ return result;
19
+ }
20
+ function token(name, value, fallback = "") {
21
+ const result = value ?? fallback;
22
+ if (result !== result.trim() || /[\u0000-\u0020\u007f]/u.test(result) || result.length > 512)
23
+ throw new TypeError(`${name} is invalid`);
24
+ return result;
25
+ }
26
+ function normalizeOptions(options) {
27
+ const proxyPathPrefix = token("proxyPathPrefix", options.proxyPathPrefix, "");
28
+ if (proxyPathPrefix !== "" && (!proxyPathPrefix.startsWith("/") || proxyPathPrefix.startsWith("//"))) {
29
+ throw new TypeError("proxyPathPrefix must be an origin-relative prefix");
30
+ }
31
+ const inject = options.injectHTML;
32
+ if (inject !== undefined) {
33
+ const mode = inject.mode ?? "inline_module";
34
+ if (mode === "inline_module" && token("injectHTML.proxyModuleUrl", inject.proxyModuleUrl) === "") {
35
+ throw new TypeError("inline HTML injection requires proxyModuleUrl");
222
36
  }
223
- }
224
- if (ok && runtimeClientId && runtimeClientId !== sourceId) {
225
- const existing = await self.clients.get(runtimeClientId);
226
- if (existing) {
227
- ok = false;
228
- } else {
229
- runtimeClientId = null;
37
+ if (mode !== "inline_module" && token("injectHTML.scriptUrl", inject.scriptUrl) === "") {
38
+ throw new TypeError("external HTML injection requires scriptUrl");
230
39
  }
231
- }
232
- if (ok) runtimeClientId = sourceId;
233
- const port = event.ports && event.ports[0];
234
- if (port) {
235
- try { port.postMessage({ type: "flowersec-proxy:register-runtime-ack", ok }); } catch {}
236
- try { port.close(); } catch {}
237
- }
238
- })());
239
- return;
240
- }
241
- if (!FORWARD_FETCH_MESSAGE_TYPES.has(msgType)) return;
242
-
243
- const port = event.ports && event.ports[0];
244
- if (!port) return;
245
-
246
- event.waitUntil((async () => {
247
- const preferredClientId =
248
- WINDOW_TARGET === "request_client" && event.source && typeof event.source.id === "string"
249
- ? event.source.id
250
- : "";
251
- const target = await getWindowClient(preferredClientId);
252
- if (!target) {
253
- try { port.postMessage({ type: "flowersec-proxy:response_error", status: 503, message: "flowersec-proxy target window not available" }); } catch {}
254
- try { port.close(); } catch {}
255
- return;
256
40
  }
257
-
258
- try {
259
- target.postMessage({ type: WINDOW_CLIENT_MESSAGE_TYPE, req: data.req }, [port]);
260
- } catch (e) {
261
- const msg = e instanceof Error ? e.message : String(e);
262
- try { port.postMessage({ type: "flowersec-proxy:response_error", status: 502, message: msg }); } catch {}
263
- try { port.close(); } catch {}
264
- }
265
- })());
266
- });
267
-
268
- async function getWindowClient(preferredClientId) {
269
- if (WINDOW_TARGET === "request_client") {
270
- if (preferredClientId) {
271
- const c = await self.clients.get(preferredClientId);
272
- if (c) return c;
273
- }
274
- const cs = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
275
- return cs.length > 0 ? cs[0] : null;
276
- }
277
-
278
- if (!runtimeClientId) return null;
279
- const c = await self.clients.get(runtimeClientId);
280
- if (c) return c;
281
- runtimeClientId = null;
282
- return null;
283
- }
284
-
285
- function shouldPassthrough(pathname) {
286
- if (PASSTHROUGH_PATHS.has(pathname)) return true;
287
- for (const p of PASSTHROUGH_PREFIXES) {
288
- if (pathname.startsWith(p)) return true;
289
- }
290
- return false;
291
- }
292
-
293
- function shouldSkipInject(pathname) {
294
- for (const p of INJECT_EXCLUDE_PREFIXES) {
295
- if (pathname.startsWith(p)) return true;
296
- }
297
- return false;
298
- }
299
-
300
- function headersToPairs(headers) {
301
- const out = [];
302
- headers.forEach((value, name) => out.push({ name, value }));
303
- return out;
304
- }
305
-
306
- function concatChunks(chunks) {
307
- let total = 0;
308
- for (const c of chunks) total += c.length;
309
- const out = new Uint8Array(total);
310
- let off = 0;
311
- for (const c of chunks) {
312
- out.set(c, off);
313
- off += c.length;
314
- }
315
- return out;
316
- }
317
-
318
- function makeError(status, message) {
319
- const s = Math.max(0, Math.floor(status));
320
- const e = new Error(String(message || "proxy error"));
321
- e.status = s > 0 ? s : 502;
322
- return e;
323
- }
324
-
325
- function getErrorStatus(err, fallback) {
326
- const raw =
327
- err && typeof err === "object" && typeof err.status === "number" && Number.isFinite(err.status)
328
- ? Math.floor(err.status)
329
- : fallback;
330
- return raw > 0 ? raw : 502;
331
- }
332
-
333
- async function readRequestBody(req) {
334
- const clRaw = req.headers.get("content-length");
335
- const cl = clRaw ? Number(clRaw) : 0;
336
- if (MAX_REQUEST_BODY_BYTES > 0 && Number.isFinite(cl) && cl > MAX_REQUEST_BODY_BYTES) {
337
- throw makeError(413, "request body too large");
338
- }
339
-
340
- // Prefer streaming reads so we can enforce MAX_REQUEST_BODY_BYTES without allocating unbounded buffers.
341
- if (!req.body) {
342
- const ab = await req.arrayBuffer();
343
- if (MAX_REQUEST_BODY_BYTES > 0 && ab.byteLength > MAX_REQUEST_BODY_BYTES) {
344
- throw makeError(413, "request body too large");
345
- }
346
- return ab;
347
- }
348
-
349
- const reader = req.body.getReader();
350
- const chunks = [];
351
- let total = 0;
352
- while (true) {
353
- const r = await reader.read();
354
- if (r.done) break;
355
- const b = r.value;
356
- total += b.length;
357
- if (MAX_REQUEST_BODY_BYTES > 0 && total > MAX_REQUEST_BODY_BYTES) {
358
- try { reader.cancel(); } catch {}
359
- throw makeError(413, "request body too large");
360
- }
361
- chunks.push(b);
362
- }
363
- return concatChunks(chunks).buffer;
364
- }
365
-
366
- function injectBootstrap(html) {
367
- let snippet = "";
368
-
369
- if (INJECT_MODE === "inline_module") {
370
- snippet =
371
- '<script type="module">' +
372
- 'import { installWebSocketPatch, disableUpstreamServiceWorkerRegister } from ' + JSON.stringify(PROXY_MODULE_URL) + ';' +
373
- 'const rt = window.top && window.top[' + JSON.stringify(RUNTIME_GLOBAL) + '];' +
374
- 'if (rt) { disableUpstreamServiceWorkerRegister(); installWebSocketPatch({ runtime: rt }); }' +
375
- '</script>';
376
- } else if (INJECT_MODE === "external_module") {
377
- snippet =
378
- '<script type="module" src="' +
379
- escapeHTMLAttributeValue(INJECT_SCRIPT_URL) +
380
- '"' +
381
- (RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + escapeHTMLAttributeValue(RUNTIME_GLOBAL) + '"' : "") +
382
- "></script>";
383
- } else if (INJECT_MODE === "external_script") {
384
- snippet =
385
- '<script src="' +
386
- escapeHTMLAttributeValue(INJECT_SCRIPT_URL) +
387
- '"' +
388
- (RUNTIME_GLOBAL ? ' data-flowersec-runtime-global="' + escapeHTMLAttributeValue(RUNTIME_GLOBAL) + '"' : "") +
389
- "></script>";
390
- }
391
-
392
- const lower = html.toLowerCase();
393
- const closeHead = lower.indexOf("</head>");
394
- if (closeHead >= 0) {
395
- return html.slice(0, closeHead) + snippet + html.slice(closeHead);
396
- }
397
- const idx = lower.indexOf("<head");
398
- if (idx >= 0) {
399
- const end = html.indexOf(">", idx);
400
- if (end >= 0) return html.slice(0, end + 1) + snippet + html.slice(end + 1);
401
- }
402
- return snippet + html;
403
- }
404
-
405
- function escapeHTMLAttributeValue(value) {
406
- return String(value).replace(/[&<>"'\`=]/g, (ch) => {
407
- switch (ch) {
408
- case "&": return "&amp;";
409
- case "<": return "&lt;";
410
- case ">": return "&gt;";
411
- case '"': return "&quot;";
412
- case "'": return "&#39;";
413
- case "\`": return "&#96;";
414
- case "=": return "&#61;";
415
- default: return ch;
416
- }
417
- });
418
- }
419
-
420
- self.addEventListener("fetch", (event) => {
421
- const url = new URL(event.request.url);
422
-
423
- // Only proxy same-origin requests by default. The runtime proxy protocol forwards path+query only.
424
- if (SAME_ORIGIN_ONLY && url.origin !== self.location.origin) return;
425
-
426
- if (PROXY_PATH_PREFIX && !url.pathname.startsWith(PROXY_PATH_PREFIX)) return;
427
- if (shouldPassthrough(url.pathname)) return;
428
-
429
- event.respondWith(handleFetch(event));
430
- });
431
-
432
- async function handleFetch(event) {
433
- let lastErrorStatus = 502;
434
- let lastErrorMessage = "proxy error";
435
-
436
- try {
437
- const preferredClientId =
438
- WINDOW_TARGET === "request_client"
439
- ? String(event.clientId || event.resultingClientId || "")
440
- : "";
441
- const target = await getWindowClient(preferredClientId);
442
- if (!target) return new Response("flowersec-proxy target window not available", { status: 503 });
443
-
444
- const req = event.request;
445
- const url = new URL(req.url);
446
- const id = Math.random().toString(16).slice(2) + Date.now().toString(16);
447
- const externalOrigin = url.origin === self.location.origin ? url.origin : "";
448
-
449
- let body;
450
- if (req.method === "GET" || req.method === "HEAD") {
451
- body = undefined;
452
- } else {
453
- body = await readRequestBody(req);
454
- }
455
-
456
- const ch = new MessageChannel();
457
- const port = ch.port1;
458
- const port2 = ch.port2;
459
-
460
- let metaResolve;
461
- let metaReject;
462
- const metaPromise = new Promise((resolve, reject) => { metaResolve = resolve; metaReject = reject; });
463
-
464
- const queued = [];
465
- const htmlChunks = [];
466
- let htmlBytes = 0;
467
- let shouldInjectHTML = false;
468
- const injectAllowed = INJECT_HTML && !shouldSkipInject(url.pathname);
469
-
470
- let doneResolve;
471
- let doneReject;
472
- const donePromise = new Promise((resolve, reject) => { doneResolve = resolve; doneReject = reject; });
473
-
474
- let controller = null;
475
- let responseCreditOutstanding = false;
476
-
477
- function requestResponseCredit() {
478
- if (responseCreditOutstanding) return;
479
- responseCreditOutstanding = true;
480
- try { port.postMessage({ type: "flowersec-proxy:response_credit" }); } catch {}
481
- }
482
-
483
- function abortRuntimeResponse() {
484
- responseCreditOutstanding = false;
485
- try { port.postMessage({ type: "flowersec-proxy:abort" }); } catch {}
486
- try { port.close(); } catch {}
487
- }
488
-
489
- const stream = new ReadableStream({
490
- start(c) { controller = c; },
491
- pull() { requestResponseCredit(); },
492
- cancel() {
493
- abortRuntimeResponse();
494
- }
41
+ return Object.freeze({
42
+ sameOriginOnly: options.sameOriginOnly ?? true,
43
+ maxRequestBodyBytes: bounded("maxRequestBodyBytes", options.maxRequestBodyBytes, 64 * 1024 * 1024, 256 * 1024 * 1024),
44
+ maxInjectHTMLBytes: bounded("maxInjectHTMLBytes", options.maxInjectHTMLBytes, 8 * 1024 * 1024, 32 * 1024 * 1024),
45
+ passthroughPaths: strings("passthrough.paths", options.passthrough?.paths),
46
+ passthroughPrefixes: strings("passthrough.prefixes", options.passthrough?.prefixes),
47
+ proxyPathPrefix,
48
+ stripProxyPathPrefix: options.stripProxyPathPrefix ?? false,
49
+ injectHTML: inject === undefined ? null : Object.freeze({
50
+ ...inject,
51
+ mode: inject.mode ?? "inline_module",
52
+ excludePathPrefixes: strings("injectHTML.excludePathPrefixes", inject.excludePathPrefixes),
53
+ }),
54
+ forwardFetchMessageTypes: strings("forwardFetchMessageTypes", options.forwardFetchMessageTypes),
55
+ windowTarget: options.windowTarget ?? "registered_runtime",
56
+ windowClientMessageType: token("windowClientMessageType", options.windowClientMessageType, "flowersec-proxy:fetch"),
57
+ runtimeRegistrationToken: token("runtimeRegistrationToken", options.runtimeRegistrationToken),
58
+ runtimeClientPathPrefix: token("runtimeClientPathPrefix", options.runtimeClientPathPrefix),
59
+ conflictHints: strings("conflictHints.keepScriptPathSuffixes", options.conflictHints?.keepScriptPathSuffixes),
495
60
  });
496
-
497
- function pushInjectChunk(b) {
498
- htmlBytes += b.length;
499
- if (MAX_INJECT_HTML_BYTES > 0 && htmlBytes > MAX_INJECT_HTML_BYTES) {
500
- const err = makeError(502, "html response too large to inject");
501
- lastErrorStatus = err.status;
502
- lastErrorMessage = err.message;
503
- metaReject(err);
504
- doneReject(err);
505
- controller?.error(err);
506
- abortRuntimeResponse();
507
- return false;
508
- }
509
- htmlChunks.push(b);
510
- return true;
511
- }
512
-
513
- port.onmessage = (ev) => {
514
- const m = ev.data;
515
- if (!m || typeof m.type !== "string") return;
516
- if (m.type === "flowersec-proxy:response_meta") {
517
- if (injectAllowed) {
518
- const ct = String((m.headers || []).find((h) => (h.name || "").toLowerCase() === "content-type")?.value || "");
519
- shouldInjectHTML = ct.toLowerCase().includes("text/html");
520
-
521
- if (shouldInjectHTML && MAX_INJECT_HTML_BYTES > 0) {
522
- const cl = Number(
523
- String((m.headers || []).find((h) => (h.name || "").toLowerCase() === "content-length")?.value || "")
524
- );
525
- if (Number.isFinite(cl) && cl > MAX_INJECT_HTML_BYTES) {
526
- const err = makeError(502, "html response too large to inject");
527
- lastErrorStatus = err.status;
528
- lastErrorMessage = err.message;
529
- metaReject(err);
530
- doneReject(err);
531
- controller?.error(err);
532
- abortRuntimeResponse();
533
- return;
61
+ }
62
+ function serviceWorkerMain(config) {
63
+ const worker = self;
64
+ let runtimeClientId = "";
65
+ const pathMatches = (path, values) => values.some((value) => path.startsWith(value));
66
+ const safeMessage = (error) => error instanceof Error && error.name === "AbortError"
67
+ ? "proxy request canceled"
68
+ : "proxy request failed";
69
+ worker.addEventListener("install", (event) => event.waitUntil(worker.skipWaiting()));
70
+ worker.addEventListener("activate", (event) => event.waitUntil(worker.clients.claim()));
71
+ worker.addEventListener("message", (event) => {
72
+ const data = event.data;
73
+ if (data === null || typeof data !== "object")
74
+ return;
75
+ if (data.type === "flowersec-proxy:register-runtime") {
76
+ const port = event.ports[0];
77
+ const source = event.source;
78
+ let ok = data.version === 2 && source !== null;
79
+ if (config.runtimeRegistrationToken !== "")
80
+ ok = ok && data.token === config.runtimeRegistrationToken;
81
+ if (ok && config.runtimeClientPathPrefix !== "") {
82
+ try {
83
+ ok = new URL(source.url).pathname.startsWith(config.runtimeClientPathPrefix);
84
+ }
85
+ catch {
86
+ ok = false;
87
+ }
534
88
  }
535
- }
536
- }
537
- metaResolve(m);
538
- if (shouldInjectHTML) requestResponseCredit();
539
- if (controller && !shouldInjectHTML) for (const q of queued) controller.enqueue(q);
540
- if (shouldInjectHTML) for (const q of queued) if (!pushInjectChunk(q)) return;
541
- queued.length = 0;
542
- return;
543
- }
544
- if (m.type === "flowersec-proxy:response_chunk") {
545
- responseCreditOutstanding = false;
546
- const b = new Uint8Array(m.data);
547
- if (shouldInjectHTML) {
548
- if (pushInjectChunk(b)) requestResponseCredit();
549
- return;
550
- }
551
- if (controller) controller.enqueue(b); else queued.push(b);
552
- return;
553
- }
554
- if (m.type === "flowersec-proxy:response_end") {
555
- responseCreditOutstanding = false;
556
- if (shouldInjectHTML) {
557
- doneResolve(htmlChunks);
558
- return;
89
+ if (ok)
90
+ runtimeClientId = source.id;
91
+ port?.postMessage({ type: "flowersec-proxy:register-runtime-ack", ok });
92
+ port?.close();
93
+ return;
559
94
  }
560
- controller?.close();
561
- return;
562
- }
563
- if (m.type === "flowersec-proxy:response_error") {
564
- responseCreditOutstanding = false;
565
- const status = typeof m.status === "number" && Number.isFinite(m.status) ? Math.floor(m.status) : 502;
566
- lastErrorStatus = status > 0 ? status : 502;
567
- lastErrorMessage = String(m.message || "proxy error");
568
- const err = makeError(lastErrorStatus, lastErrorMessage);
569
- metaReject(err);
570
- doneReject(err);
571
- controller?.error(err);
572
- try { port.close(); } catch {}
573
- return;
574
- }
575
- };
576
-
577
- let path = url.pathname + url.search;
578
- if (PROXY_PATH_PREFIX && STRIP_PROXY_PATH_PREFIX) {
579
- let rest = url.pathname.slice(PROXY_PATH_PREFIX.length);
580
- if (rest.startsWith("/")) rest = rest.slice(1);
581
- path = "/" + rest + url.search;
582
- }
583
-
584
- target.postMessage({
585
- type: WINDOW_CLIENT_MESSAGE_TYPE,
586
- req: {
587
- id,
588
- method: req.method,
589
- path,
590
- headers: headersToPairs(req.headers),
591
- ...(externalOrigin ? { external_origin: externalOrigin } : {}),
592
- response_flow_control: "chunk_credit_v1",
593
- body
594
- }
595
- }, [port2]);
596
-
597
- const meta = await metaPromise;
598
- const headers = new Headers();
599
- for (const h of (meta.headers || [])) {
600
- const name = String(h.name || "");
601
- const lower = name.toLowerCase();
602
- if (shouldInjectHTML && INJECT_STRIP_VALIDATOR_HEADERS && INJECT_STRIP_HEADER_NAMES.has(lower)) continue;
603
- headers.append(name, String(h.value || ""));
604
- }
605
-
606
- if (shouldInjectHTML && INJECT_SET_NO_STORE && !headers.has("Cache-Control")) {
607
- headers.set("Cache-Control", "no-store");
608
- }
609
-
610
- if (shouldInjectHTML) {
611
- const chunks = await donePromise;
612
- const raw = concatChunks(chunks);
613
- const html = new TextDecoder().decode(raw);
614
- const injected = injectBootstrap(html);
615
- return new Response(new TextEncoder().encode(injected), { status: meta.status || 502, headers });
616
- }
617
-
618
- return new Response(stream, { status: meta.status || 502, headers });
619
- } catch (err) {
620
- const status = getErrorStatus(err, lastErrorStatus);
621
- const msg = err instanceof Error ? err.message : String(err);
622
- return new Response(msg || lastErrorMessage || "flowersec-proxy error", { status });
623
- }
95
+ if (!config.forwardFetchMessageTypes.includes(String(data.type ?? "")))
96
+ return;
97
+ event.waitUntil((async () => {
98
+ const target = runtimeClientId === "" ? null : await worker.clients.get(runtimeClientId);
99
+ target?.postMessage(data, event.ports);
100
+ })());
101
+ });
102
+ worker.addEventListener("fetch", (event) => {
103
+ event.respondWith((async () => {
104
+ const request = event.request;
105
+ const url = new URL(request.url);
106
+ if (config.sameOriginOnly && url.origin !== worker.location.origin)
107
+ return await fetch(request);
108
+ if (config.passthroughPaths.includes(url.pathname) || pathMatches(url.pathname, config.passthroughPrefixes)) {
109
+ return await fetch(request);
110
+ }
111
+ let path = url.pathname + url.search;
112
+ if (config.proxyPathPrefix !== "") {
113
+ if (!url.pathname.startsWith(config.proxyPathPrefix))
114
+ return await fetch(request);
115
+ if (config.stripProxyPathPrefix) {
116
+ const stripped = url.pathname.slice(config.proxyPathPrefix.length);
117
+ path = (stripped.startsWith("/") ? stripped : `/${stripped}`) + url.search;
118
+ }
119
+ }
120
+ const source = event.clientId === "" ? null : await worker.clients.get(event.clientId);
121
+ const target = config.windowTarget === "request_client"
122
+ ? source
123
+ : runtimeClientId === "" ? null : await worker.clients.get(runtimeClientId);
124
+ if (target === null)
125
+ return new Response("proxy runtime unavailable", { status: 503 });
126
+ let body;
127
+ if (request.method !== "GET" && request.method !== "HEAD") {
128
+ const requestBody = await request.clone().arrayBuffer();
129
+ if (requestBody.byteLength > config.maxRequestBodyBytes)
130
+ return new Response("proxy request too large", { status: 413 });
131
+ body = requestBody;
132
+ }
133
+ const channel = new MessageChannel();
134
+ const response = await new Promise((resolve) => {
135
+ let metadata = null;
136
+ let controller = null;
137
+ let finished = false;
138
+ const finishError = (status, message) => {
139
+ if (finished)
140
+ return;
141
+ finished = true;
142
+ if (controller !== null)
143
+ controller.error(new Error(message));
144
+ else
145
+ resolve(new Response(message, { status }));
146
+ channel.port1.close();
147
+ };
148
+ channel.port1.onmessage = (message) => {
149
+ const value = message.data;
150
+ if (value === null || typeof value !== "object" || finished)
151
+ return;
152
+ if (value.type === "flowersec-proxy:response_meta") {
153
+ if (!Number.isInteger(value.status) || metadata !== null)
154
+ return finishError(502, "invalid proxy response");
155
+ const headers = new Headers();
156
+ if (Array.isArray(value.headers))
157
+ for (const entry of value.headers) {
158
+ if (entry && typeof entry.name === "string" && typeof entry.value === "string")
159
+ headers.append(entry.name, entry.value);
160
+ }
161
+ metadata = { status: value.status, headers };
162
+ const stream = new ReadableStream({
163
+ start(valueController) {
164
+ controller = valueController;
165
+ channel.port1.postMessage({ type: "flowersec-proxy:response_credit" });
166
+ },
167
+ pull() { channel.port1.postMessage({ type: "flowersec-proxy:response_credit" }); },
168
+ cancel() { channel.port1.postMessage({ type: "flowersec-proxy:abort" }); channel.port1.close(); },
169
+ });
170
+ resolve(new Response(stream, { status: metadata.status, headers: metadata.headers }));
171
+ return;
172
+ }
173
+ if (value.type === "flowersec-proxy:response_chunk") {
174
+ if (controller === null || !(value.data instanceof ArrayBuffer))
175
+ return finishError(502, "invalid proxy response");
176
+ controller.enqueue(new Uint8Array(value.data));
177
+ return;
178
+ }
179
+ if (value.type === "flowersec-proxy:response_end") {
180
+ finished = true;
181
+ controller?.close();
182
+ channel.port1.close();
183
+ return;
184
+ }
185
+ if (value.type === "flowersec-proxy:response_error") {
186
+ finishError(Number.isInteger(value.status) ? value.status : 502, typeof value.message === "string" ? value.message : "proxy request failed");
187
+ }
188
+ };
189
+ channel.port1.onmessageerror = () => finishError(502, "proxy request failed");
190
+ const headers = Array.from(request.headers.entries()).map(([name, value]) => ({ name, value }));
191
+ target.postMessage({
192
+ type: config.windowClientMessageType,
193
+ req: {
194
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
195
+ method: request.method,
196
+ path,
197
+ headers,
198
+ response_flow_control: "chunk_credit_v2",
199
+ ...(body === undefined ? {} : { body }),
200
+ },
201
+ }, [channel.port2, ...(body === undefined ? [] : [body])]);
202
+ });
203
+ const injection = config.injectHTML;
204
+ if (injection === null || pathMatches(url.pathname, injection.excludePathPrefixes ?? []))
205
+ return response;
206
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().includes("text/html"))
207
+ return response;
208
+ const bytes = await response.arrayBuffer();
209
+ if (bytes.byteLength > config.maxInjectHTMLBytes)
210
+ return new Response("proxy HTML response too large", { status: 502 });
211
+ const decoder = new TextDecoder();
212
+ let html = decoder.decode(bytes);
213
+ const runtimeGlobal = injection.runtimeGlobal ?? "__flowersecProxyRuntime";
214
+ let markup;
215
+ if ((injection.mode ?? "inline_module") === "inline_module") {
216
+ markup = `<script type="module">import { installWebSocketPatch, disableUpstreamServiceWorkerRegister } from ${JSON.stringify(injection.proxyModuleUrl)};const rt=globalThis[${JSON.stringify(runtimeGlobal)}];if(rt){disableUpstreamServiceWorkerRegister();installWebSocketPatch({runtime:rt});}</script>`;
217
+ }
218
+ else {
219
+ const module = injection.mode === "external_module" ? " type=\"module\"" : "";
220
+ markup = `<script${module} src=${JSON.stringify(injection.scriptUrl)} data-flowersec-runtime-global=${JSON.stringify(runtimeGlobal)}></script>`;
221
+ }
222
+ const location = html.search(/<\/head\s*>/iu);
223
+ html = location >= 0 ? `${html.slice(0, location)}${markup}${html.slice(location)}` : `${markup}${html}`;
224
+ const headers = new Headers(response.headers);
225
+ headers.delete("content-length");
226
+ if (injection.stripValidatorHeaders !== false) {
227
+ headers.delete("etag");
228
+ headers.delete("last-modified");
229
+ }
230
+ if (injection.setNoStore !== false)
231
+ headers.set("cache-control", "no-store");
232
+ return new Response(html, { status: response.status, statusText: response.statusText, headers });
233
+ })().catch((error) => new Response(safeMessage(error), { status: 502 })));
234
+ });
624
235
  }
625
- `;
236
+ export function createProxyServiceWorkerScript(options = {}) {
237
+ const config = normalizeOptions(options);
238
+ return `// Generated by @floegence/flowersec-core/proxy v2\n(${serviceWorkerMain.toString()})(${JSON.stringify(config)});\n`;
626
239
  }