@floegence/flowersec-core 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/browser/connect.d.ts +2 -1
  3. package/dist/browser/index.d.ts +3 -3
  4. package/dist/browser/index.js +2 -2
  5. package/dist/client-connect/connectCore.d.ts +1 -1
  6. package/dist/client-connect/transportSecurity.d.ts +1 -3
  7. package/dist/client-connect/transportSecurity.js +0 -4
  8. package/dist/connect/artifact.d.ts +0 -1
  9. package/dist/connect/artifact.js +0 -4
  10. package/dist/connect/{internalNormalize.d.ts → resolveArtifact.d.ts} +8 -8
  11. package/dist/connect/resolveArtifact.js +87 -0
  12. package/dist/controlplane/request.d.ts +1 -0
  13. package/dist/controlplane/request.js +77 -2
  14. package/dist/defaults.d.ts +1 -0
  15. package/dist/defaults.js +1 -0
  16. package/dist/e2ee/record.js +8 -4
  17. package/dist/e2ee/secureChannel.d.ts +1 -0
  18. package/dist/e2ee/secureChannel.js +18 -4
  19. package/dist/facade.d.ts +1 -4
  20. package/dist/facade.js +5 -11
  21. package/dist/node/connect.d.ts +0 -2
  22. package/dist/node/index.d.ts +1 -1
  23. package/dist/node/index.js +1 -1
  24. package/dist/proxy/bootstrap.d.ts +5 -24
  25. package/dist/proxy/bootstrap.js +1 -11
  26. package/dist/proxy/constants.d.ts +1 -0
  27. package/dist/proxy/constants.js +1 -0
  28. package/dist/proxy/integration.js +0 -6
  29. package/dist/proxy/preset.d.ts +0 -1
  30. package/dist/proxy/preset.js +0 -9
  31. package/dist/proxy/runtimeScope.js +2 -7
  32. package/dist/proxy/server.d.ts +0 -1
  33. package/dist/proxy/server.js +257 -145
  34. package/dist/tunnel-client/connect.d.ts +1 -1
  35. package/dist/tunnel-client/connect.js +1 -11
  36. package/package.json +4 -6
  37. package/dist/browser/controlplane.d.ts +0 -15
  38. package/dist/browser/controlplane.js +0 -65
  39. package/dist/connect/internalNormalize.js +0 -173
  40. package/dist/proxy/profiles.d.ts +0 -18
  41. package/dist/proxy/profiles.js +0 -71
@@ -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 = {
@@ -6,4 +6,4 @@ export type TunnelConnectOptions = ConnectOptionsBase & Readonly<{
6
6
  /** Optional caller-provided endpoint instance ID (base64url). */
7
7
  endpointInstanceId?: string;
8
8
  }>;
9
- export declare function connectTunnel(grant: unknown, opts: TunnelConnectOptions): Promise<ClientInternal>;
9
+ export declare function connectTunnel(input: unknown, opts: TunnelConnectOptions): Promise<ClientInternal>;
@@ -11,18 +11,8 @@ function isRecord(v) {
11
11
  function hasOwn(o, key) {
12
12
  return Object.prototype.hasOwnProperty.call(o, key);
13
13
  }
14
- function unwrapGrant(v) {
15
- if (!isRecord(v))
16
- return v;
17
- if (hasOwn(v, "grant_client"))
18
- return v["grant_client"];
19
- if (hasOwn(v, "grant_server"))
20
- return v["grant_server"];
21
- return v;
22
- }
23
14
  // connectTunnel attaches to a tunnel and returns an RPC-ready session.
24
- export async function connectTunnel(grant, opts) {
25
- const input = unwrapGrant(grant);
15
+ export async function connectTunnel(input, opts) {
26
16
  if (input == null) {
27
17
  throw new FlowersecError({ stage: "validate", code: "missing_grant", path: "tunnel", message: "missing grant" });
28
18
  }