@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 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`. Request bodies remain bounded per request by `maxBodyBytes`; `maxBufferedRequestBodyBytes` additionally caps the total buffered request-body bytes owned by one proxy session. 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.
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 session request-body budget equal to `maxBodyBytes` and one queued WebSocket frame plus its proxy header. Raise either limit only when the deployment has a matching memory budget.
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 === "")
@@ -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 { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
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 { ConnectArtifactRequestConfig, ControlplaneConfig, EntryConnectArtifactRequestConfig, EntryControlplaneConfig, } from "./controlplane.js";
8
- export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
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";
@@ -1,5 +1,5 @@
1
1
  export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
2
+ export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
3
3
  export { assertConnectArtifact } from "../connect/artifact.js";
4
- export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
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 | typeof AllowPlaintext;
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) {
@@ -7,6 +7,7 @@ export type ControlplaneBaseConfig = Readonly<{
7
7
  credentials?: RequestCredentials;
8
8
  fetch?: FetchLike;
9
9
  signal?: AbortSignal;
10
+ allowLoopbackHTTP?: boolean;
10
11
  }>;
11
12
  export type ArtifactRequestCorrelation = Readonly<{
12
13
  traceId?: string;
@@ -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 data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_CONNECT_ARTIFACT_PATH), {
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 data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH), {
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",
@@ -35,6 +35,7 @@ export declare const SDK_DEFAULTS: Readonly<{
35
35
  maxChunkBytes: number;
36
36
  maxBodyBytes: number;
37
37
  maxWsFrameBytes: number;
38
+ maxConcurrentStreams: 64;
38
39
  defaultTimeoutMs: 30000;
39
40
  maxTimeoutMs: 300000;
40
41
  }>;
package/dist/defaults.js CHANGED
@@ -35,6 +35,7 @@ export const SDK_DEFAULTS = Object.freeze({
35
35
  maxChunkBytes: 256 * 1024,
36
36
  maxBodyBytes: 64 * 1024 * 1024,
37
37
  maxWsFrameBytes: 1024 * 1024,
38
+ maxConcurrentStreams: 64,
38
39
  defaultTimeoutMs: 30_000,
39
40
  maxTimeoutMs: 300_000,
40
41
  }),
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 { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
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 { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
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
  }
@@ -1,5 +1,5 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
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";
@@ -1,5 +1,5 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
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, TunnelConnectBrowserOptions } from "../browser/connect.js";
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 ConnectTunnelProxyBrowserOptions = Readonly<{
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 ConnectTunnelProxyControllerBrowserOptions = Readonly<{
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 connectTunnelProxyBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
52
- export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
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>;
@@ -1,4 +1,4 @@
1
- import { connectBrowser, connectTunnelBrowser } from "../browser/connect.js";
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;
@@ -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) {
@@ -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;
@@ -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, resolveNamedProxyPreset, } from "./preset.js";
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
- try {
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)
@@ -9,7 +9,6 @@ export type ProxyServerOptions = Readonly<{
9
9
  maxJsonFrameBytes?: number;
10
10
  maxChunkBytes?: number;
11
11
  maxBodyBytes?: number;
12
- maxBufferedRequestBodyBytes?: number;
13
12
  maxWsFrameBytes?: number;
14
13
  maxWsQueuedBytes?: number;
15
14
  defaultTimeoutMs?: number;
@@ -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 requestBodyBudget = createRequestBodyBudget(compiled.maxBufferedRequestBodyBytes);
11
- const active = new Set();
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(() => active.delete(task));
23
- active.add(task);
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 (active.size >= compiled.maxConcurrentStreams) {
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, requestBodyBudget, signal)
30
+ const task = serveProxyStreamCompiled(accepted.kind, accepted.stream, compiled, signal)
35
31
  .catch(() => { })
36
- .finally(() => active.delete(task));
37
- active.add(task);
32
+ .finally(() => activeProxy.delete(task));
33
+ activeProxy.add(task);
38
34
  }
39
35
  }
40
36
  finally {
41
- await Promise.allSettled(active);
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, createRequestBodyBudget(compiled.maxBufferedRequestBodyBytes), signal);
51
+ return serveProxyStreamCompiled(kind, stream, compiled, signal);
56
52
  }
57
- async function serveProxyStreamCompiled(kind, stream, options, requestBodyBudget, signal) {
53
+ async function serveProxyStreamCompiled(kind, stream, options, signal) {
58
54
  try {
59
55
  if (kind === PROXY_KIND_HTTP1)
60
- await serveHTTP(stream, options, requestBodyBudget, signal);
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, requestBodyBudget, signal) {
72
- const reader = createByteReader(stream, signal === undefined ? {} : { signal });
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 bufferedBody = await readBody(reader, options.maxChunkBytes, options.maxBodyBytes, requestBodyBudget);
79
- try {
80
- if (signal?.aborted)
81
- throw new ProxyServerError("canceled", "proxy request canceled");
82
- const headers = requestHeaders(meta.headers, options);
83
- applyExternalOrigin(headers, meta.external_origin);
84
- const target = new URL(options.upstream);
85
- target.pathname = path.pathname;
86
- target.search = path.search;
87
- target.hash = "";
88
- const timeoutMs = resolveTimeout(meta.timeout_ms, options);
89
- const controller = new AbortController();
90
- const onAbort = () => controller.abort(new ProxyServerError("canceled", "proxy request canceled"));
91
- signal?.addEventListener("abort", onAbort, { once: true });
92
- if (signal?.aborted)
93
- onAbort();
94
- const timer = timeoutMs > 0
95
- ? setTimeout(() => controller.abort(new ProxyServerError("timeout", "upstream request timeout")), timeoutMs)
96
- : undefined;
97
- try {
98
- const method = meta.method.trim().toUpperCase();
99
- let response;
100
- try {
101
- if (controller.signal.aborted)
102
- throw controller.signal.reason;
103
- response = await options.fetch(target, {
104
- method,
105
- headers,
106
- redirect: "manual",
107
- signal: controller.signal,
108
- ...((method === "GET" || method === "HEAD")
109
- ? {}
110
- : { body: bufferedBody.bytes.buffer }),
111
- });
112
- }
113
- finally {
114
- bufferedBody.release();
115
- }
116
- const responseHeaders = collectResponseHeaders(response.headers, options);
117
- const responseMeta = {
118
- v: PROXY_PROTOCOL_VERSION,
119
- request_id: requestId,
120
- ok: true,
121
- status: response.status,
122
- headers: responseHeaders,
123
- };
124
- await writeJsonFrame(stream, responseMeta);
125
- if (response.body == null) {
126
- await stream.write(u32be(0));
127
- return;
128
- }
129
- const bodyReader = response.body.getReader();
130
- let total = 0;
131
- while (true) {
132
- const next = await bodyReader.read();
133
- if (next.done)
134
- break;
135
- const bytes = next.value;
136
- total += bytes.length;
137
- if (total > options.maxBodyBytes)
138
- throw new ProxyServerError("response_body_too_large", "response body too large");
139
- for (let offset = 0; offset < bytes.length; offset += options.maxChunkBytes) {
140
- const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + options.maxChunkBytes));
141
- await stream.write(u32be(chunk.length));
142
- await stream.write(chunk);
143
- }
144
- }
145
- await stream.write(u32be(0));
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
- finally {
148
- if (timer != null)
149
- clearTimeout(timer);
150
- signal?.removeEventListener("abort", onAbort);
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
- finally {
154
- bufferedBody.release();
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
- await writeHTTPError(stream, requestId, classifyHTTPError(error));
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, 64, "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 createRequestBodyBudget(maxBytes) {
470
- let usedBytes = 0;
471
- return {
472
- reserve: (bytes) => {
473
- const next = usedBytes + bytes;
474
- if (!Number.isSafeInteger(next) || next > maxBytes) {
475
- throw new ProxyServerError("resource_exhausted", "proxy request body buffer exhausted");
476
- }
477
- usedBytes = next;
478
- },
479
- release: (bytes) => {
480
- usedBytes = Math.max(0, usedBytes - bytes);
481
- },
482
- };
483
- }
484
- async function readBody(reader, maxChunkBytes, maxBodyBytes, budget) {
485
- const chunks = [];
514
+ function createStreamingRequestBody(reader, maxChunkBytes, maxBodyBytes, onFailure) {
486
515
  let total = 0;
487
- let handedOff = false;
488
- try {
489
- while (true) {
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
- break;
493
- if (length > maxChunkBytes)
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
- const next = total + length;
496
- if (!Number.isSafeInteger(next) || next > maxBodyBytes) {
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
- budget.reserve(length);
500
- total = next;
501
- chunks.push(await reader.readExactly(length));
557
+ const chunk = await reader.readExactly(length);
558
+ total = nextTotal;
559
+ return chunk;
502
560
  }
503
- const body = new Uint8Array(total);
504
- let offset = 0;
505
- for (const chunk of chunks) {
506
- body.set(chunk, offset);
507
- offset += chunk.length;
561
+ catch (cause) {
562
+ throw fail(cause);
508
563
  }
509
- let released = false;
510
- handedOff = true;
511
- return {
512
- bytes: body,
513
- release: () => {
514
- if (released)
515
- return;
516
- released = true;
517
- budget.release(total);
518
- },
519
- };
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
- finally {
522
- if (!handedOff)
523
- budget.release(total);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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;
@@ -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
- }