@floegence/flowersec-core 0.25.0 → 0.26.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.
- package/README.md +2 -2
- package/dist/browser/controlplane.d.ts +1 -1
- package/dist/browser/controlplane.js +0 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.js +2 -2
- package/dist/client-connect/transportSecurity.d.ts +1 -3
- package/dist/client-connect/transportSecurity.js +0 -4
- package/dist/controlplane/request.d.ts +1 -0
- package/dist/controlplane/request.js +77 -2
- package/dist/defaults.d.ts +1 -0
- package/dist/defaults.js +1 -0
- package/dist/facade.d.ts +1 -1
- package/dist/facade.js +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/proxy/bootstrap.d.ts +5 -24
- package/dist/proxy/bootstrap.js +1 -11
- package/dist/proxy/constants.d.ts +1 -0
- package/dist/proxy/constants.js +1 -0
- package/dist/proxy/integration.js +0 -6
- package/dist/proxy/preset.d.ts +0 -1
- package/dist/proxy/preset.js +0 -9
- package/dist/proxy/runtimeScope.js +2 -7
- package/dist/proxy/server.d.ts +0 -1
- package/dist/proxy/server.js +257 -145
- package/package.json +1 -1
- package/dist/proxy/profiles.d.ts +0 -18
- package/dist/proxy/profiles.js +0 -71
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ High-level WebSocket connections require TLS by default. Use `AllowPlaintextForL
|
|
|
29
29
|
|
|
30
30
|
## Node Proxy Server
|
|
31
31
|
|
|
32
|
-
The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`.
|
|
32
|
+
The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`. HTTP request and response bodies stream between Flowersec chunk frames and the upstream fetch while remaining bounded per request by `maxBodyBytes`. Proxy sessions admit at most `maxConcurrentStreams` HTTP/WebSocket streams, with a shared default of 64; RPC serving has its own concurrency controls. WebSocket frames remain bounded by `maxWsFrameBytes`; `maxWsQueuedBytes` additionally caps upstream-to-Yamux queued bytes per connection, while the reverse direction waits for each Node `ws` send callback.
|
|
33
33
|
|
|
34
|
-
The defaults keep one
|
|
34
|
+
The defaults keep one queued WebSocket frame plus its proxy header. Raise body, concurrency, or queue limits only when the deployment has matching upstream and memory capacity.
|
|
35
35
|
|
|
36
36
|
## Runtime Boundaries
|
|
37
37
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ChannelInitGrant } from "../facade.js";
|
|
2
2
|
import type { ControlplaneBaseConfig as SharedControlplaneBaseConfig, RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/request.js";
|
|
3
|
-
export { ControlplaneRequestError, requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
|
|
4
3
|
type BaseControlplaneRequestConfig = SharedControlplaneBaseConfig & Readonly<{
|
|
5
4
|
endpointId: string;
|
|
6
5
|
payload?: Record<string, unknown>;
|
|
@@ -13,3 +12,4 @@ export type ConnectArtifactRequestConfig = RequestConnectArtifactInput;
|
|
|
13
12
|
export type EntryConnectArtifactRequestConfig = RequestEntryConnectArtifactInput;
|
|
14
13
|
export declare function requestChannelGrant(config: ControlplaneConfig): Promise<ChannelInitGrant>;
|
|
15
14
|
export declare function requestEntryChannelGrant(config: EntryControlplaneConfig): Promise<ChannelInitGrant>;
|
|
15
|
+
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { assertChannelInitGrant } from "../facade.js";
|
|
2
2
|
import { requestControlplaneJSON, } from "../controlplane/request.js";
|
|
3
|
-
export { ControlplaneRequestError, requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
|
|
4
3
|
function buildURL(baseUrl, path) {
|
|
5
4
|
const base = String(baseUrl ?? "").trim();
|
|
6
5
|
if (base === "")
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
2
2
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
3
|
-
export {
|
|
3
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
4
4
|
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
5
5
|
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
6
6
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
7
|
-
export type {
|
|
8
|
-
export {
|
|
7
|
+
export type { ControlplaneConfig, EntryControlplaneConfig } from "./controlplane.js";
|
|
8
|
+
export { requestChannelGrant, requestEntryChannelGrant, } from "./controlplane.js";
|
|
9
9
|
export type { BrowserReconnectConfig, DirectBrowserReconnectConfig, TunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
10
10
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
4
|
-
export {
|
|
4
|
+
export { requestChannelGrant, requestEntryChannelGrant, } from "./controlplane.js";
|
|
5
5
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
@@ -2,8 +2,6 @@ import type { ClientPath } from "../client.js";
|
|
|
2
2
|
import { type ClientObserverLike } from "../observability/observer.js";
|
|
3
3
|
export declare const RequireTLS: "require_tls";
|
|
4
4
|
export declare const AllowPlaintextForLoopback: "allow_plaintext_for_loopback";
|
|
5
|
-
/** @deprecated Use RequireTLS, AllowPlaintextForLoopback, or createNetworkPlaintextPolicy. */
|
|
6
|
-
export declare const AllowPlaintext: "allow_plaintext";
|
|
7
5
|
export declare const PlaintextRiskAcceptance: {
|
|
8
6
|
readonly acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure";
|
|
9
7
|
};
|
|
@@ -18,7 +16,7 @@ export type TransportSecurityPolicyInput = Readonly<{
|
|
|
18
16
|
host: string;
|
|
19
17
|
runtime: "browser" | "node" | "other";
|
|
20
18
|
}>;
|
|
21
|
-
export type TransportSecurityPolicyPreset = typeof RequireTLS | typeof AllowPlaintextForLoopback
|
|
19
|
+
export type TransportSecurityPolicyPreset = typeof RequireTLS | typeof AllowPlaintextForLoopback;
|
|
22
20
|
export type TransportSecurityPolicy = TransportSecurityPolicyPreset | ((input: TransportSecurityPolicyInput) => boolean | Promise<boolean>);
|
|
23
21
|
export declare function createNetworkPlaintextPolicy(options: NetworkPlaintextPolicyOptions): TransportSecurityPolicy;
|
|
24
22
|
export declare function enforceTransportSecurity(args: Readonly<{
|
|
@@ -2,8 +2,6 @@ import { FlowersecError } from "../utils/errors.js";
|
|
|
2
2
|
import { emitObserverDiagnostic } from "../observability/observer.js";
|
|
3
3
|
export const RequireTLS = "require_tls";
|
|
4
4
|
export const AllowPlaintextForLoopback = "allow_plaintext_for_loopback";
|
|
5
|
-
/** @deprecated Use RequireTLS, AllowPlaintextForLoopback, or createNetworkPlaintextPolicy. */
|
|
6
|
-
export const AllowPlaintext = "allow_plaintext";
|
|
7
5
|
export const PlaintextRiskAcceptance = {
|
|
8
6
|
acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure",
|
|
9
7
|
};
|
|
@@ -71,8 +69,6 @@ function evaluatePreset(policy, target) {
|
|
|
71
69
|
return target.scheme === "wss";
|
|
72
70
|
case AllowPlaintextForLoopback:
|
|
73
71
|
return target.scheme === "wss" || isLiteralLoopbackHost(target.host);
|
|
74
|
-
case AllowPlaintext:
|
|
75
|
-
return true;
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
function parseWebSocketTarget(rawUrl) {
|
|
@@ -34,6 +34,76 @@ export function buildControlplaneURL(baseUrl, path) {
|
|
|
34
34
|
return path;
|
|
35
35
|
return `${base.replace(/\/+$/, "")}${path}`;
|
|
36
36
|
}
|
|
37
|
+
function transportPolicyDenied() {
|
|
38
|
+
return new ControlplaneRequestError({
|
|
39
|
+
status: 0,
|
|
40
|
+
code: "transport_policy_denied",
|
|
41
|
+
message: "controlplane transport security policy denied URL",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function validateArtifactURL(rawUrl, allowLoopbackHTTP) {
|
|
45
|
+
let target;
|
|
46
|
+
try {
|
|
47
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(rawUrl)) {
|
|
48
|
+
target = new URL(rawUrl);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const base = globalThis.location?.href;
|
|
52
|
+
if (!base)
|
|
53
|
+
throw new Error("relative URL requires a browser location");
|
|
54
|
+
target = new URL(rawUrl, base);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw transportPolicyDenied();
|
|
59
|
+
}
|
|
60
|
+
if (target.username !== "" || target.password !== "" || target.hash !== "") {
|
|
61
|
+
throw transportPolicyDenied();
|
|
62
|
+
}
|
|
63
|
+
if (target.protocol === "https:")
|
|
64
|
+
return;
|
|
65
|
+
if (target.protocol === "http:"
|
|
66
|
+
&& allowLoopbackHTTP === true
|
|
67
|
+
&& isLiteralLoopbackHost(sourceHostname(rawUrl, target)))
|
|
68
|
+
return;
|
|
69
|
+
throw transportPolicyDenied();
|
|
70
|
+
}
|
|
71
|
+
function sourceHostname(rawUrl, target) {
|
|
72
|
+
const authority = /^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/([^/?#]*)/.exec(rawUrl)?.[1];
|
|
73
|
+
if (authority === undefined)
|
|
74
|
+
return target.hostname;
|
|
75
|
+
const hostPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
76
|
+
if (hostPort.startsWith("[")) {
|
|
77
|
+
const end = hostPort.indexOf("]");
|
|
78
|
+
if (end < 0 || (hostPort.length > end + 1 && !/^:\d+$/.test(hostPort.slice(end + 1)))) {
|
|
79
|
+
return hostPort;
|
|
80
|
+
}
|
|
81
|
+
return hostPort.slice(1, end);
|
|
82
|
+
}
|
|
83
|
+
const colon = hostPort.lastIndexOf(":");
|
|
84
|
+
if (colon < 0)
|
|
85
|
+
return hostPort;
|
|
86
|
+
if (hostPort.indexOf(":") !== colon || !/^\d+$/.test(hostPort.slice(colon + 1)))
|
|
87
|
+
return hostPort;
|
|
88
|
+
return hostPort.slice(0, colon);
|
|
89
|
+
}
|
|
90
|
+
function isLiteralLoopbackHost(rawHost) {
|
|
91
|
+
const host = rawHost.startsWith("[") && rawHost.endsWith("]")
|
|
92
|
+
? rawHost.slice(1, -1).toLowerCase()
|
|
93
|
+
: rawHost.toLowerCase();
|
|
94
|
+
if (host === "localhost" || host === "::1")
|
|
95
|
+
return true;
|
|
96
|
+
const parts = host.split(".");
|
|
97
|
+
if (parts.length !== 4)
|
|
98
|
+
return false;
|
|
99
|
+
const octets = parts.map((part) => {
|
|
100
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part))
|
|
101
|
+
return -1;
|
|
102
|
+
const value = Number(part);
|
|
103
|
+
return value <= 255 ? value : -1;
|
|
104
|
+
});
|
|
105
|
+
return octets.every((value) => value >= 0) && octets[0] === 127;
|
|
106
|
+
}
|
|
37
107
|
function parseMaybeJSON(bodyText) {
|
|
38
108
|
const trimmed = String(bodyText ?? "").trim();
|
|
39
109
|
if (trimmed === "")
|
|
@@ -110,6 +180,7 @@ export async function requestControlplaneJSON(url, init) {
|
|
|
110
180
|
const response = await runFetch(url, {
|
|
111
181
|
...init,
|
|
112
182
|
cache: "no-store",
|
|
183
|
+
redirect: "error",
|
|
113
184
|
});
|
|
114
185
|
let rawBody = "";
|
|
115
186
|
try {
|
|
@@ -164,7 +235,9 @@ function createJSONHeaders(input) {
|
|
|
164
235
|
return headers;
|
|
165
236
|
}
|
|
166
237
|
export async function requestConnectArtifact(config) {
|
|
167
|
-
const
|
|
238
|
+
const url = buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_CONNECT_ARTIFACT_PATH);
|
|
239
|
+
validateArtifactURL(url, config.allowLoopbackHTTP);
|
|
240
|
+
const data = (await requestControlplaneJSON(url, {
|
|
168
241
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
169
242
|
method: "POST",
|
|
170
243
|
credentials: config.credentials ?? "omit",
|
|
@@ -183,7 +256,9 @@ export async function requestEntryConnectArtifact(config) {
|
|
|
183
256
|
throw new Error("entryTicket is required");
|
|
184
257
|
const headers = createJSONHeaders(config.headers);
|
|
185
258
|
headers.set("Authorization", `Bearer ${entryTicket}`);
|
|
186
|
-
const
|
|
259
|
+
const url = buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH);
|
|
260
|
+
validateArtifactURL(url, config.allowLoopbackHTTP);
|
|
261
|
+
const data = (await requestControlplaneJSON(url, {
|
|
187
262
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
188
263
|
method: "POST",
|
|
189
264
|
credentials: config.credentials ?? "omit",
|
package/dist/defaults.d.ts
CHANGED
package/dist/defaults.js
CHANGED
package/dist/facade.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type { WebSocketLimits } from "./ws-client/binaryTransport.js";
|
|
|
17
17
|
export type { YamuxLimits } from "./yamux/session.js";
|
|
18
18
|
export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
|
|
19
19
|
export { FlowersecError } from "./utils/errors.js";
|
|
20
|
-
export {
|
|
20
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
21
21
|
export type { NetworkPlaintextPolicyOptions, TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
|
|
22
22
|
export type { TunnelConnectOptions } from "./tunnel-client/connect.js";
|
|
23
23
|
export type { DirectConnectOptions } from "./direct-client/connect.js";
|
package/dist/facade.js
CHANGED
|
@@ -6,7 +6,7 @@ export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
|
6
6
|
export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
7
7
|
export { assertConnectArtifact } from "./connect/artifact.js";
|
|
8
8
|
export { FlowersecError } from "./utils/errors.js";
|
|
9
|
-
export {
|
|
9
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
10
10
|
export async function connectTunnel(grant, opts) {
|
|
11
11
|
return await connectTunnelInternal(grant, opts);
|
|
12
12
|
}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
4
4
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
5
5
|
export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
4
4
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
5
5
|
export * from "../endpoint/index.js";
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
2
|
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
3
|
-
import type { ConnectBrowserOptions
|
|
4
|
-
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
3
|
+
import type { ConnectBrowserOptions } from "../browser/connect.js";
|
|
5
4
|
import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
|
|
6
5
|
import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
|
|
7
6
|
import type { ProxyPresetInput } from "./preset.js";
|
|
8
7
|
import { type ProxyRuntime } from "./runtime.js";
|
|
9
|
-
export type
|
|
10
|
-
connect?: TunnelConnectBrowserOptions;
|
|
11
|
-
preset?: ProxyPresetInput;
|
|
12
|
-
runtimeGlobalKey?: string;
|
|
13
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
14
|
-
serviceWorker: ProxyIntegrationServiceWorkerOptions;
|
|
15
|
-
plugins?: readonly ProxyIntegrationPlugin[];
|
|
16
|
-
}>;
|
|
17
|
-
export type ConnectTunnelProxyBrowserHandle = Readonly<{
|
|
8
|
+
export type ConnectArtifactProxyBrowserHandle = Readonly<{
|
|
18
9
|
client: Client;
|
|
19
10
|
runtime: ProxyRuntime;
|
|
20
11
|
dispose: () => Promise<void>;
|
|
@@ -27,15 +18,7 @@ export type ConnectArtifactProxyBrowserOptions = Readonly<{
|
|
|
27
18
|
serviceWorker?: ProxyIntegrationServiceWorkerOptions;
|
|
28
19
|
plugins?: readonly ProxyIntegrationPlugin[];
|
|
29
20
|
}>;
|
|
30
|
-
export type
|
|
31
|
-
connect?: TunnelConnectBrowserOptions;
|
|
32
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
33
|
-
allowedOrigins: RegisterProxyControllerWindowOptions["allowedOrigins"];
|
|
34
|
-
targetWindow?: RegisterProxyControllerWindowOptions["targetWindow"];
|
|
35
|
-
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
36
|
-
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
37
|
-
}>;
|
|
38
|
-
export type ConnectTunnelProxyControllerBrowserHandle = Readonly<{
|
|
21
|
+
export type ConnectArtifactProxyControllerBrowserHandle = Readonly<{
|
|
39
22
|
client: Client;
|
|
40
23
|
runtime: ProxyRuntime;
|
|
41
24
|
dispose: () => void;
|
|
@@ -48,7 +31,5 @@ export type ConnectArtifactProxyControllerBrowserOptions = Readonly<{
|
|
|
48
31
|
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
49
32
|
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
50
33
|
}>;
|
|
51
|
-
export declare function
|
|
52
|
-
export declare function
|
|
53
|
-
export declare function connectTunnelProxyControllerBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
54
|
-
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
34
|
+
export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectArtifactProxyBrowserHandle>;
|
|
35
|
+
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectArtifactProxyControllerBrowserHandle>;
|
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectBrowser
|
|
1
|
+
import { connectBrowser } from "../browser/connect.js";
|
|
2
2
|
import { registerProxyIntegration, } from "./integration.js";
|
|
3
3
|
import { registerProxyControllerWindow } from "./controllerWindow.js";
|
|
4
4
|
import { extractProxyRuntimeScopeV1, resolvePresetInputFromScope, resolveRuntimeLimitsFromScope, resolveRuntimePresetLimits, } from "./runtimeScope.js";
|
|
@@ -23,12 +23,10 @@ function scopeRuntimeToIntegrationOptions(scope, opts) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
async function connectProxyBrowserClient(client, opts) {
|
|
26
|
-
const compat = opts;
|
|
27
26
|
const integrationInput = {
|
|
28
27
|
client,
|
|
29
28
|
serviceWorker: opts.serviceWorker,
|
|
30
29
|
...(opts.preset === undefined ? {} : { preset: opts.preset }),
|
|
31
|
-
...(compat.profile === undefined ? {} : { profile: compat.profile }),
|
|
32
30
|
...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
|
|
33
31
|
...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
|
|
34
32
|
...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
|
|
@@ -101,20 +99,12 @@ function connectProxyControllerClient(client, opts) {
|
|
|
101
99
|
},
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
|
-
export async function connectTunnelProxyBrowser(grant, opts) {
|
|
105
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
106
|
-
return await connectProxyBrowserClient(client, opts);
|
|
107
|
-
}
|
|
108
102
|
export async function connectArtifactProxyBrowser(artifact, opts = {}) {
|
|
109
103
|
const scope = extractProxyRuntimeScopeV1(artifact, "service_worker");
|
|
110
104
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
111
105
|
const nextOpts = scopeRuntimeToIntegrationOptions(scope, opts);
|
|
112
106
|
return await connectProxyBrowserClient(client, nextOpts);
|
|
113
107
|
}
|
|
114
|
-
export async function connectTunnelProxyControllerBrowser(grant, opts) {
|
|
115
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
116
|
-
return connectProxyControllerClient(client, opts);
|
|
117
|
-
}
|
|
118
108
|
export async function connectArtifactProxyControllerBrowser(artifact, opts = {}) {
|
|
119
109
|
const scope = extractProxyRuntimeScopeV1(artifact, "controller_bridge");
|
|
120
110
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
@@ -4,3 +4,4 @@ export declare const PROXY_KIND_WS: "flowersec-proxy/ws";
|
|
|
4
4
|
export declare const DEFAULT_MAX_CHUNK_BYTES: number;
|
|
5
5
|
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
6
6
|
export declare const DEFAULT_MAX_WS_FRAME_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_MAX_CONCURRENT_STREAMS: 64;
|
package/dist/proxy/constants.js
CHANGED
|
@@ -5,3 +5,4 @@ export const PROXY_KIND_WS = "flowersec-proxy/ws";
|
|
|
5
5
|
export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
|
|
6
6
|
export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
|
|
7
7
|
export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
|
|
8
|
+
export const DEFAULT_MAX_CONCURRENT_STREAMS = SDK_DEFAULTS.proxy.maxConcurrentStreams;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { profileToPresetManifest } from "./profiles.js";
|
|
2
1
|
import { resolveProxyPreset } from "./preset.js";
|
|
3
2
|
import { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.js";
|
|
4
3
|
import { createProxyRuntime, ensureServiceWorkerRuntimeRegistered } from "./runtime.js";
|
|
@@ -165,13 +164,8 @@ function shouldIgnoreMismatch(plugins, ctx) {
|
|
|
165
164
|
return false;
|
|
166
165
|
}
|
|
167
166
|
function resolveIntegrationPreset(opts) {
|
|
168
|
-
if (opts.preset !== undefined && opts.profile !== undefined) {
|
|
169
|
-
throw new Error("preset and deprecated profile cannot be used together");
|
|
170
|
-
}
|
|
171
167
|
if (opts.preset !== undefined)
|
|
172
168
|
return resolveProxyPreset(opts.preset);
|
|
173
|
-
if (opts.profile !== undefined)
|
|
174
|
-
return resolveProxyPreset(profileToPresetManifest(opts.profile));
|
|
175
169
|
return resolveProxyPreset();
|
|
176
170
|
}
|
|
177
171
|
function buildRuntimeOptions(preset, runtime) {
|
package/dist/proxy/preset.d.ts
CHANGED
|
@@ -26,5 +26,4 @@ export type ResolvedProxyPreset = Readonly<{
|
|
|
26
26
|
export declare const DEFAULT_PROXY_PRESET_MANIFEST: ProxyPresetManifest;
|
|
27
27
|
export type ProxyPresetInput = ProxyPresetManifest | Partial<ProxyPresetLimits>;
|
|
28
28
|
export declare function assertProxyPresetManifest(value: unknown): ProxyPresetManifest;
|
|
29
|
-
export declare function resolveNamedProxyPreset(name: string): ProxyPresetManifest;
|
|
30
29
|
export declare function resolveProxyPreset(input?: ProxyPresetInput): ResolvedProxyPreset;
|
package/dist/proxy/preset.js
CHANGED
|
@@ -75,15 +75,6 @@ export function assertProxyPresetManifest(value) {
|
|
|
75
75
|
limits: assertLimits(value.limits),
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
export function resolveNamedProxyPreset(name) {
|
|
79
|
-
switch (String(name ?? "").trim()) {
|
|
80
|
-
case "":
|
|
81
|
-
case "default":
|
|
82
|
-
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
83
|
-
default:
|
|
84
|
-
throw new Error(`unknown proxy preset: ${name}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
78
|
function toResolved(manifest) {
|
|
88
79
|
return Object.freeze({
|
|
89
80
|
v: 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertProxyPresetManifest,
|
|
1
|
+
import { assertProxyPresetManifest, } from "./preset.js";
|
|
2
2
|
const RUNTIME_SCOPE_NAME = "proxy.runtime";
|
|
3
3
|
const PRESET_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
4
4
|
const MAX_RUNTIME_PAYLOAD_BYTES = 8 * 1024;
|
|
@@ -203,12 +203,7 @@ export function resolvePresetInputFromScope(scope, presetOverride) {
|
|
|
203
203
|
return presetOverride;
|
|
204
204
|
if (scope.preset.snapshot)
|
|
205
205
|
return scope.preset.snapshot;
|
|
206
|
-
|
|
207
|
-
return resolveNamedProxyPreset(scope.preset.presetId);
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
206
|
+
throw new Error("proxy.runtime preset snapshot is required");
|
|
212
207
|
}
|
|
213
208
|
export function resolveRuntimePresetLimits(scope) {
|
|
214
209
|
if (scope.limits === undefined)
|
package/dist/proxy/server.d.ts
CHANGED
package/dist/proxy/server.js
CHANGED
|
@@ -3,42 +3,38 @@ import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../
|
|
|
3
3
|
import { RpcRouter, RpcServer } from "../rpc/server.js";
|
|
4
4
|
import { createByteReader } from "../streamio/index.js";
|
|
5
5
|
import { readU32be, u32be } from "../utils/bin.js";
|
|
6
|
-
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";
|
|
6
|
+
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_CONCURRENT_STREAMS, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION, } from "./constants.js";
|
|
7
7
|
import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders, isSafeHeaderValue, isValidHeaderName, normalizeHeaderName } from "./headerPolicy.js";
|
|
8
8
|
export async function serveProxySession(session, options, signal) {
|
|
9
9
|
const compiled = compileOptions(options);
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const activeProxy = new Set();
|
|
11
|
+
const activeRPC = new Set();
|
|
12
12
|
try {
|
|
13
13
|
while (!signal?.aborted) {
|
|
14
14
|
const accepted = await session.acceptStream(signal === undefined ? {} : { signal });
|
|
15
15
|
if (accepted.kind === "rpc") {
|
|
16
|
-
if (active.size >= compiled.maxConcurrentStreams) {
|
|
17
|
-
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
16
|
const task = serveRPCStream(accepted.stream, options.rpcRouter ?? new RpcRouter(), options.rpcServerOptions, signal)
|
|
21
17
|
.catch(() => { })
|
|
22
|
-
.finally(() =>
|
|
23
|
-
|
|
18
|
+
.finally(() => activeRPC.delete(task));
|
|
19
|
+
activeRPC.add(task);
|
|
24
20
|
continue;
|
|
25
21
|
}
|
|
26
22
|
if (accepted.kind !== PROXY_KIND_HTTP1 && accepted.kind !== PROXY_KIND_WS) {
|
|
27
23
|
await accepted.stream.reset(new Error(`unsupported proxy stream kind ${accepted.kind}`));
|
|
28
24
|
continue;
|
|
29
25
|
}
|
|
30
|
-
if (
|
|
26
|
+
if (activeProxy.size >= compiled.maxConcurrentStreams) {
|
|
31
27
|
await accepted.stream.reset(new Error("proxy stream concurrency exhausted"));
|
|
32
28
|
continue;
|
|
33
29
|
}
|
|
34
|
-
const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled,
|
|
30
|
+
const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
|
|
35
31
|
.catch(() => { })
|
|
36
|
-
.finally(() =>
|
|
37
|
-
|
|
32
|
+
.finally(() => activeProxy.delete(task));
|
|
33
|
+
activeProxy.add(task);
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
finally {
|
|
41
|
-
await Promise.allSettled(
|
|
37
|
+
await Promise.allSettled([...activeProxy, ...activeRPC]);
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
async function serveRPCStream(stream, router, options, signal) {
|
|
@@ -52,12 +48,12 @@ async function serveRPCStream(stream, router, options, signal) {
|
|
|
52
48
|
}
|
|
53
49
|
export function serveProxyStream(kind, stream, options, signal) {
|
|
54
50
|
const compiled = compileOptions(options);
|
|
55
|
-
return serveProxyStreamCompiled(kind, stream, compiled,
|
|
51
|
+
return serveProxyStreamCompiled(kind, stream, compiled, signal);
|
|
56
52
|
}
|
|
57
|
-
async function serveProxyStreamCompiled(kind, stream, options,
|
|
53
|
+
async function serveProxyStreamCompiled(kind, stream, options, signal) {
|
|
58
54
|
try {
|
|
59
55
|
if (kind === PROXY_KIND_HTTP1)
|
|
60
|
-
await serveHTTP(stream, options,
|
|
56
|
+
await serveHTTP(stream, options, signal);
|
|
61
57
|
else
|
|
62
58
|
await serveWebSocket(stream, options, signal);
|
|
63
59
|
}
|
|
@@ -68,94 +64,145 @@ async function serveProxyStreamCompiled(kind, stream, options, requestBodyBudget
|
|
|
68
64
|
catch { /* The peer may already have reset the stream. */ }
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
|
-
async function serveHTTP(stream, options,
|
|
72
|
-
const reader = createByteReader(stream
|
|
67
|
+
async function serveHTTP(stream, options, signal) {
|
|
68
|
+
const reader = createByteReader(stream);
|
|
73
69
|
let requestId = "unknown";
|
|
70
|
+
let responseStarted = false;
|
|
71
|
+
let streamReset = false;
|
|
72
|
+
let requestBody;
|
|
73
|
+
let responseBodyReader;
|
|
74
|
+
let controller;
|
|
75
|
+
let timer;
|
|
76
|
+
let removeAbortListener;
|
|
77
|
+
const resetStream = async (error) => {
|
|
78
|
+
if (streamReset)
|
|
79
|
+
return;
|
|
80
|
+
streamReset = true;
|
|
81
|
+
try {
|
|
82
|
+
await stream.reset(error);
|
|
83
|
+
}
|
|
84
|
+
catch { /* The peer may already have reset the stream. */ }
|
|
85
|
+
};
|
|
74
86
|
try {
|
|
75
87
|
const meta = assertHTTPRequestMeta(await readJsonFrame(reader, options.maxJsonFrameBytes));
|
|
76
88
|
requestId = meta.request_id;
|
|
77
89
|
const path = parsePath(meta.path);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (signal
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
90
|
+
const knownRequestLength = contentLength(meta.headers, "request");
|
|
91
|
+
if (knownRequestLength != null && knownRequestLength > options.maxBodyBytes) {
|
|
92
|
+
throw new ProxyServerError("request_body_too_large", "request body too large");
|
|
93
|
+
}
|
|
94
|
+
if (signal?.aborted)
|
|
95
|
+
throw new ProxyServerError("canceled", "proxy request canceled");
|
|
96
|
+
const headers = requestHeaders(meta.headers, options);
|
|
97
|
+
applyExternalOrigin(headers, meta.external_origin);
|
|
98
|
+
const target = new URL(options.upstream);
|
|
99
|
+
target.pathname = path.pathname;
|
|
100
|
+
target.search = path.search;
|
|
101
|
+
target.hash = "";
|
|
102
|
+
controller = new AbortController();
|
|
103
|
+
const abortUpstream = (error) => {
|
|
104
|
+
if (!controller.signal.aborted)
|
|
105
|
+
controller.abort(error);
|
|
106
|
+
requestBody?.abort(error);
|
|
107
|
+
if (responseStarted)
|
|
108
|
+
void resetStream(error);
|
|
109
|
+
};
|
|
110
|
+
const onAbort = () => abortUpstream(new ProxyServerError("canceled", "proxy request canceled"));
|
|
111
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
112
|
+
removeAbortListener = () => signal?.removeEventListener("abort", onAbort);
|
|
113
|
+
if (signal?.aborted)
|
|
114
|
+
onAbort();
|
|
115
|
+
const timeoutMs = resolveTimeout(meta.timeout_ms, options);
|
|
116
|
+
timer = timeoutMs > 0
|
|
117
|
+
? setTimeout(() => abortUpstream(new ProxyServerError("timeout", "upstream request timeout")), timeoutMs)
|
|
118
|
+
: undefined;
|
|
119
|
+
requestBody = createStreamingRequestBody(reader, options.maxChunkBytes, options.maxBodyBytes, abortUpstream);
|
|
120
|
+
const method = meta.method.trim().toUpperCase();
|
|
121
|
+
const requestInit = {
|
|
122
|
+
method,
|
|
123
|
+
headers,
|
|
124
|
+
redirect: "manual",
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
};
|
|
127
|
+
if (method === "GET" || method === "HEAD") {
|
|
128
|
+
requestBody.drain();
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
requestInit.body = requestBody.stream;
|
|
132
|
+
requestInit.duplex = "half";
|
|
133
|
+
}
|
|
134
|
+
if (controller.signal.aborted)
|
|
135
|
+
throw controller.signal.reason;
|
|
136
|
+
const response = await options.fetch(target, requestInit);
|
|
137
|
+
if (requestBody.error != null)
|
|
138
|
+
throw requestBody.error;
|
|
139
|
+
requestBody.drain();
|
|
140
|
+
const knownResponseLength = contentLength(response.headers, "response");
|
|
141
|
+
if (knownResponseLength != null && knownResponseLength > options.maxBodyBytes) {
|
|
142
|
+
await response.body?.cancel();
|
|
143
|
+
throw new ProxyServerError("response_body_too_large", "response body too large");
|
|
144
|
+
}
|
|
145
|
+
const responseHeaders = collectResponseHeaders(response.headers, options);
|
|
146
|
+
const responseMeta = {
|
|
147
|
+
v: PROXY_PROTOCOL_VERSION,
|
|
148
|
+
request_id: requestId,
|
|
149
|
+
ok: true,
|
|
150
|
+
status: response.status,
|
|
151
|
+
headers: responseHeaders,
|
|
152
|
+
};
|
|
153
|
+
await writeJsonFrame(stream, responseMeta);
|
|
154
|
+
responseStarted = true;
|
|
155
|
+
if (response.body == null) {
|
|
156
|
+
await stream.write(u32be(0));
|
|
157
|
+
const requestError = await requestBody.completion;
|
|
158
|
+
if (requestError != null)
|
|
159
|
+
throw requestError;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
responseBodyReader = response.body.getReader();
|
|
163
|
+
let total = 0;
|
|
164
|
+
while (true) {
|
|
165
|
+
const next = await Promise.race([
|
|
166
|
+
responseBodyReader.read(),
|
|
167
|
+
requestBody.failure.then((error) => { throw error; }),
|
|
168
|
+
]);
|
|
169
|
+
if (next.done)
|
|
170
|
+
break;
|
|
171
|
+
const bytes = next.value;
|
|
172
|
+
const nextTotal = total + bytes.length;
|
|
173
|
+
if (!Number.isSafeInteger(nextTotal) || nextTotal > options.maxBodyBytes) {
|
|
174
|
+
throw new ProxyServerError("response_body_too_large", "response body too large");
|
|
146
175
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
total = nextTotal;
|
|
177
|
+
for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
|
|
178
|
+
const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
|
|
179
|
+
await stream.write(u32be(chunk.length));
|
|
180
|
+
await stream.write(chunk);
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
await stream.write(u32be(0));
|
|
184
|
+
const requestError = await requestBody.completion;
|
|
185
|
+
if (requestError != null)
|
|
186
|
+
throw requestError;
|
|
156
187
|
}
|
|
157
188
|
catch (error) {
|
|
158
|
-
|
|
189
|
+
const effective = requestBody?.error ?? controller?.signal.reason ?? asError(error);
|
|
190
|
+
if (controller != null && !controller.signal.aborted)
|
|
191
|
+
controller.abort(effective);
|
|
192
|
+
if (responseStarted)
|
|
193
|
+
await resetStream(effective);
|
|
194
|
+
else
|
|
195
|
+
await writeHTTPError(stream, requestId, classifyHTTPError(effective));
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (timer != null)
|
|
199
|
+
clearTimeout(timer);
|
|
200
|
+
removeAbortListener?.();
|
|
201
|
+
requestBody?.stop();
|
|
202
|
+
try {
|
|
203
|
+
await responseBodyReader?.cancel();
|
|
204
|
+
}
|
|
205
|
+
catch { /* The upstream body may already be closed. */ }
|
|
159
206
|
}
|
|
160
207
|
}
|
|
161
208
|
async function serveWebSocket(stream, options, signal) {
|
|
@@ -354,7 +401,6 @@ function compileOptions(input) {
|
|
|
354
401
|
const maxJsonFrameBytes = positive(input.maxJsonFrameBytes, DEFAULT_MAX_JSON_FRAME_BYTES, "maxJsonFrameBytes");
|
|
355
402
|
const maxChunkBytes = positive(input.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES, "maxChunkBytes");
|
|
356
403
|
const maxBodyBytes = positive(input.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes");
|
|
357
|
-
const maxBufferedRequestBodyBytes = positive(input.maxBufferedRequestBodyBytes, maxBodyBytes, "maxBufferedRequestBodyBytes");
|
|
358
404
|
const maxWsFrameBytes = positive(input.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES, "maxWsFrameBytes");
|
|
359
405
|
const defaultMaxWsQueuedBytes = maxWsFrameBytes <= Number.MAX_SAFE_INTEGER - 5
|
|
360
406
|
? maxWsFrameBytes + 5
|
|
@@ -372,12 +418,11 @@ function compileOptions(input) {
|
|
|
372
418
|
maxJsonFrameBytes,
|
|
373
419
|
maxChunkBytes,
|
|
374
420
|
maxBodyBytes,
|
|
375
|
-
maxBufferedRequestBodyBytes,
|
|
376
421
|
maxWsFrameBytes,
|
|
377
422
|
maxWsQueuedBytes,
|
|
378
423
|
defaultTimeoutMs,
|
|
379
424
|
maxTimeoutMs,
|
|
380
|
-
maxConcurrentStreams: positive(input.maxConcurrentStreams,
|
|
425
|
+
maxConcurrentStreams: positive(input.maxConcurrentStreams, DEFAULT_MAX_CONCURRENT_STREAMS, "maxConcurrentStreams"),
|
|
381
426
|
extraRequestHeaders: input.extraRequestHeaders ?? [],
|
|
382
427
|
extraResponseHeaders: input.extraResponseHeaders ?? [],
|
|
383
428
|
blockedResponseHeaders: normalizeNames(input.blockedResponseHeaders),
|
|
@@ -466,62 +511,129 @@ function applyExternalOrigin(headers, input) {
|
|
|
466
511
|
headers.set("x-forwarded-proto", new URL(external).protocol.slice(0, -1));
|
|
467
512
|
headers.set("host", new URL(external).host);
|
|
468
513
|
}
|
|
469
|
-
function
|
|
470
|
-
let usedBytes = 0;
|
|
471
|
-
return {
|
|
472
|
-
reserve: (bytes) => {
|
|
473
|
-
const next = usedBytes + bytes;
|
|
474
|
-
if (!Number.isSafeInteger(next) || next > maxBytes) {
|
|
475
|
-
throw new ProxyServerError("resource_exhausted", "proxy request body buffer exhausted");
|
|
476
|
-
}
|
|
477
|
-
usedBytes = next;
|
|
478
|
-
},
|
|
479
|
-
release: (bytes) => {
|
|
480
|
-
usedBytes = Math.max(0, usedBytes - bytes);
|
|
481
|
-
},
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
async function readBody(reader, maxChunkBytes, maxBodyBytes, budget) {
|
|
485
|
-
const chunks = [];
|
|
514
|
+
function createStreamingRequestBody(reader, maxChunkBytes, maxBodyBytes, onFailure) {
|
|
486
515
|
let total = 0;
|
|
487
|
-
let
|
|
488
|
-
|
|
489
|
-
|
|
516
|
+
let finished = false;
|
|
517
|
+
let draining = false;
|
|
518
|
+
let readChain = Promise.resolve();
|
|
519
|
+
let error;
|
|
520
|
+
let streamController;
|
|
521
|
+
let resolveFailure;
|
|
522
|
+
const failure = new Promise((resolve) => { resolveFailure = resolve; });
|
|
523
|
+
let resolveCompletion;
|
|
524
|
+
const completion = new Promise((resolve) => { resolveCompletion = resolve; });
|
|
525
|
+
const fail = (cause) => {
|
|
526
|
+
if (error != null)
|
|
527
|
+
return error;
|
|
528
|
+
const raw = asError(cause);
|
|
529
|
+
error = raw instanceof ProxyServerError
|
|
530
|
+
? raw
|
|
531
|
+
: new ProxyServerError("request_body_invalid", raw.message);
|
|
532
|
+
finished = true;
|
|
533
|
+
try {
|
|
534
|
+
streamController?.error(error);
|
|
535
|
+
}
|
|
536
|
+
catch { /* The body consumer may already be gone. */ }
|
|
537
|
+
resolveFailure(error);
|
|
538
|
+
resolveCompletion(error);
|
|
539
|
+
onFailure(error);
|
|
540
|
+
return error;
|
|
541
|
+
};
|
|
542
|
+
const readChunkUnserialized = async () => {
|
|
543
|
+
try {
|
|
490
544
|
const length = readU32be(await reader.readExactly(4), 0);
|
|
491
|
-
if (length === 0)
|
|
492
|
-
|
|
493
|
-
|
|
545
|
+
if (length === 0) {
|
|
546
|
+
finished = true;
|
|
547
|
+
resolveCompletion(undefined);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (length > maxChunkBytes) {
|
|
494
551
|
throw new ProxyServerError("request_body_too_large", "request chunk too large");
|
|
495
|
-
|
|
496
|
-
|
|
552
|
+
}
|
|
553
|
+
const nextTotal = total + length;
|
|
554
|
+
if (!Number.isSafeInteger(nextTotal) || nextTotal > maxBodyBytes) {
|
|
497
555
|
throw new ProxyServerError("request_body_too_large", "request body too large");
|
|
498
556
|
}
|
|
499
|
-
|
|
500
|
-
total =
|
|
501
|
-
|
|
557
|
+
const chunk = await reader.readExactly(length);
|
|
558
|
+
total = nextTotal;
|
|
559
|
+
return chunk;
|
|
502
560
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
for (const chunk of chunks) {
|
|
506
|
-
body.set(chunk, offset);
|
|
507
|
-
offset += chunk.length;
|
|
561
|
+
catch (cause) {
|
|
562
|
+
throw fail(cause);
|
|
508
563
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
564
|
+
};
|
|
565
|
+
const readChunk = () => {
|
|
566
|
+
const next = readChain.then(readChunkUnserialized);
|
|
567
|
+
readChain = next.then(() => { }, () => { });
|
|
568
|
+
return next;
|
|
569
|
+
};
|
|
570
|
+
const drain = () => {
|
|
571
|
+
if (draining || finished)
|
|
572
|
+
return;
|
|
573
|
+
draining = true;
|
|
574
|
+
try {
|
|
575
|
+
streamController?.close();
|
|
576
|
+
}
|
|
577
|
+
catch { /* A pending pull will observe draining below. */ }
|
|
578
|
+
void (async () => {
|
|
579
|
+
while (!finished)
|
|
580
|
+
await readChunk();
|
|
581
|
+
})().catch(() => { });
|
|
582
|
+
};
|
|
583
|
+
const stream = new ReadableStream({
|
|
584
|
+
start(controller) { streamController = controller; },
|
|
585
|
+
async pull(controller) {
|
|
586
|
+
if (finished) {
|
|
587
|
+
if (error == null)
|
|
588
|
+
controller.close();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const chunk = await readChunk();
|
|
592
|
+
if (draining)
|
|
593
|
+
return;
|
|
594
|
+
if (chunk == null)
|
|
595
|
+
controller.close();
|
|
596
|
+
else
|
|
597
|
+
controller.enqueue(chunk);
|
|
598
|
+
},
|
|
599
|
+
cancel() { drain(); },
|
|
600
|
+
}, { highWaterMark: 0 });
|
|
601
|
+
return {
|
|
602
|
+
stream,
|
|
603
|
+
failure,
|
|
604
|
+
completion,
|
|
605
|
+
get error() { return error; },
|
|
606
|
+
drain,
|
|
607
|
+
abort: (cause) => { if (!finished)
|
|
608
|
+
fail(cause); },
|
|
609
|
+
stop: () => {
|
|
610
|
+
if (finished)
|
|
611
|
+
return;
|
|
612
|
+
finished = true;
|
|
613
|
+
try {
|
|
614
|
+
streamController?.close();
|
|
615
|
+
}
|
|
616
|
+
catch { /* The fetch implementation may own the stream. */ }
|
|
617
|
+
resolveCompletion(undefined);
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function contentLength(headers, side) {
|
|
622
|
+
const rawValues = headers instanceof Headers
|
|
623
|
+
? (headers.get("content-length") == null ? [] : [headers.get("content-length")])
|
|
624
|
+
: headers
|
|
625
|
+
.filter((header) => normalizeHeaderName(header.name) === "content-length")
|
|
626
|
+
.map((header) => header.value.trim());
|
|
627
|
+
if (rawValues.length === 0)
|
|
628
|
+
return undefined;
|
|
629
|
+
if (rawValues.length !== 1 || !/^(0|[1-9][0-9]*)$/.test(rawValues[0])) {
|
|
630
|
+
throw new ProxyServerError(side === "request" ? "invalid_request_meta" : "upstream_request_failed", `invalid ${side} content-length`);
|
|
520
631
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
632
|
+
const value = Number(rawValues[0]);
|
|
633
|
+
if (!Number.isSafeInteger(value)) {
|
|
634
|
+
throw new ProxyServerError(side === "request" ? "invalid_request_meta" : "upstream_request_failed", `invalid ${side} content-length`);
|
|
524
635
|
}
|
|
636
|
+
return value;
|
|
525
637
|
}
|
|
526
638
|
async function writeHTTPError(stream, requestId, code) {
|
|
527
639
|
const meta = {
|
package/package.json
CHANGED
package/dist/proxy/profiles.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type ProxyPresetManifest } from "./preset.js";
|
|
2
|
-
export type ProxyProfile = Readonly<{
|
|
3
|
-
maxJsonFrameBytes: number;
|
|
4
|
-
maxChunkBytes: number;
|
|
5
|
-
maxBodyBytes: number;
|
|
6
|
-
maxWsFrameBytes: number;
|
|
7
|
-
timeoutMs: number;
|
|
8
|
-
}>;
|
|
9
|
-
export type ProxyProfileName = "default";
|
|
10
|
-
export declare const PROXY_PROFILE_DEFAULT: Readonly<{
|
|
11
|
-
maxJsonFrameBytes: number;
|
|
12
|
-
maxChunkBytes: number;
|
|
13
|
-
maxBodyBytes: number;
|
|
14
|
-
maxWsFrameBytes: number;
|
|
15
|
-
timeoutMs: number;
|
|
16
|
-
}>;
|
|
17
|
-
export declare function resolveProxyProfile(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyProfile;
|
|
18
|
-
export declare function profileToPresetManifest(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyPresetManifest;
|
package/dist/proxy/profiles.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_PROXY_PRESET_MANIFEST, resolveProxyPreset, } from "./preset.js";
|
|
2
|
-
function toLegacyProfile(manifest) {
|
|
3
|
-
const resolved = resolveProxyPreset(manifest);
|
|
4
|
-
return Object.freeze({
|
|
5
|
-
maxJsonFrameBytes: resolved.limits.max_json_frame_bytes,
|
|
6
|
-
maxChunkBytes: resolved.limits.max_chunk_bytes,
|
|
7
|
-
maxBodyBytes: resolved.limits.max_body_bytes,
|
|
8
|
-
maxWsFrameBytes: resolved.limits.max_ws_frame_bytes,
|
|
9
|
-
timeoutMs: resolved.limits.timeout_ms ?? 0,
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
const DEFAULT_PROFILE = toLegacyProfile(DEFAULT_PROXY_PRESET_MANIFEST);
|
|
13
|
-
export const PROXY_PROFILE_DEFAULT = DEFAULT_PROFILE;
|
|
14
|
-
function normalizeSafeInt(name, value) {
|
|
15
|
-
if (!Number.isFinite(value))
|
|
16
|
-
throw new Error(`${name} must be a finite number`);
|
|
17
|
-
const n = Math.floor(value);
|
|
18
|
-
if (!Number.isSafeInteger(n))
|
|
19
|
-
throw new Error(`${name} must be a safe integer`);
|
|
20
|
-
if (n < 0)
|
|
21
|
-
throw new Error(`${name} must be >= 0`);
|
|
22
|
-
return n;
|
|
23
|
-
}
|
|
24
|
-
function resolveNamedProfile(name) {
|
|
25
|
-
switch (name) {
|
|
26
|
-
case "default":
|
|
27
|
-
return DEFAULT_PROFILE;
|
|
28
|
-
default:
|
|
29
|
-
throw new Error(`unknown proxy profile: ${name}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export function resolveProxyProfile(profile) {
|
|
33
|
-
if (profile == null)
|
|
34
|
-
return DEFAULT_PROFILE;
|
|
35
|
-
if (typeof profile === "string") {
|
|
36
|
-
return resolveNamedProfile(profile);
|
|
37
|
-
}
|
|
38
|
-
const base = DEFAULT_PROFILE;
|
|
39
|
-
return Object.freeze({
|
|
40
|
-
maxJsonFrameBytes: normalizeSafeInt("maxJsonFrameBytes", profile.maxJsonFrameBytes ?? base.maxJsonFrameBytes),
|
|
41
|
-
maxChunkBytes: normalizeSafeInt("maxChunkBytes", profile.maxChunkBytes ?? base.maxChunkBytes),
|
|
42
|
-
maxBodyBytes: normalizeSafeInt("maxBodyBytes", profile.maxBodyBytes ?? base.maxBodyBytes),
|
|
43
|
-
maxWsFrameBytes: normalizeSafeInt("maxWsFrameBytes", profile.maxWsFrameBytes ?? base.maxWsFrameBytes),
|
|
44
|
-
timeoutMs: normalizeSafeInt("timeoutMs", profile.timeoutMs ?? base.timeoutMs),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
export function profileToPresetManifest(profile) {
|
|
48
|
-
if (profile == null)
|
|
49
|
-
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
50
|
-
if (typeof profile === "string") {
|
|
51
|
-
switch (profile) {
|
|
52
|
-
case "default":
|
|
53
|
-
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
54
|
-
default:
|
|
55
|
-
throw new Error(`unknown proxy profile: ${profile}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const resolved = resolveProxyProfile(profile);
|
|
59
|
-
return Object.freeze({
|
|
60
|
-
v: 1,
|
|
61
|
-
preset_id: "legacy-profile",
|
|
62
|
-
deprecated: true,
|
|
63
|
-
limits: {
|
|
64
|
-
...(resolved.maxJsonFrameBytes > 0 ? { max_json_frame_bytes: resolved.maxJsonFrameBytes } : {}),
|
|
65
|
-
...(resolved.maxChunkBytes > 0 ? { max_chunk_bytes: resolved.maxChunkBytes } : {}),
|
|
66
|
-
...(resolved.maxBodyBytes > 0 ? { max_body_bytes: resolved.maxBodyBytes } : {}),
|
|
67
|
-
...(resolved.maxWsFrameBytes > 0 ? { max_ws_frame_bytes: resolved.maxWsFrameBytes } : {}),
|
|
68
|
-
...(resolved.timeoutMs > 0 ? { timeout_ms: resolved.timeoutMs } : {}),
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
}
|