@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
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
 
@@ -2,11 +2,12 @@ import type { Client } from "../client.js";
2
2
  import type { DirectConnectOptions } from "../direct-client/connect.js";
3
3
  import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
4
4
  import { type ConnectOptions } from "../facade.js";
5
+ import type { ConnectArtifact } from "../connect/artifact.js";
5
6
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
6
7
  import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
7
8
  export type TunnelConnectBrowserOptions = Omit<TunnelConnectOptions, "origin" | "wsFactory">;
8
9
  export type DirectConnectBrowserOptions = Omit<DirectConnectOptions, "origin" | "wsFactory">;
9
10
  export type ConnectBrowserOptions = Omit<ConnectOptions, "origin" | "wsFactory">;
10
- export declare function connectBrowser(input: unknown, opts?: ConnectBrowserOptions): Promise<Client>;
11
+ export declare function connectBrowser(input: ConnectArtifact, opts?: ConnectBrowserOptions): Promise<Client>;
11
12
  export declare function connectTunnelBrowser(grant: ChannelInitGrant, opts?: TunnelConnectBrowserOptions): Promise<Client>;
12
13
  export declare function connectDirectBrowser(info: DirectConnectInfo, opts?: DirectConnectBrowserOptions): Promise<Client>;
@@ -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 { RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/request.js";
8
+ export { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.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 { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
5
5
  export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
@@ -2,7 +2,7 @@ import { type YamuxLimits } from "../yamux/session.js";
2
2
  import { type ClientObserverLike } from "../observability/observer.js";
3
3
  import { type WebSocketLike, type WebSocketLimits } from "../ws-client/binaryTransport.js";
4
4
  import type { ClientInternal } from "../client.js";
5
- import type { ConnectScopeResolverMap } from "../connect/internalNormalize.js";
5
+ import type { ConnectScopeResolverMap } from "../connect/resolveArtifact.js";
6
6
  import { type TransportSecurityPolicy } from "./transportSecurity.js";
7
7
  export type LivenessOptions = Readonly<{
8
8
  intervalMs?: number;
@@ -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) {
@@ -33,5 +33,4 @@ export type DirectClientConnectArtifact = Readonly<{
33
33
  correlation?: CorrelationContext;
34
34
  }>;
35
35
  export type ConnectArtifact = TunnelClientConnectArtifact | DirectClientConnectArtifact;
36
- export declare function hasArtifactOnlyFields(value: Record<string, unknown>): boolean;
37
36
  export declare function assertConnectArtifact(value: unknown): ConnectArtifact;
@@ -6,7 +6,6 @@ const CORRELATION_ID_RE = /^[A-Za-z0-9._~-]{8,128}$/;
6
6
  const encoder = new TextEncoder();
7
7
  const TUNNEL_ARTIFACT_KEYS = new Set(["v", "transport", "tunnel_grant", "scoped", "correlation"]);
8
8
  const DIRECT_ARTIFACT_KEYS = new Set(["v", "transport", "direct_info", "scoped", "correlation"]);
9
- const ARTIFACT_ONLY_KEYS = new Set(["v", "transport", "tunnel_grant", "direct_info", "scoped", "correlation"]);
10
9
  function isRecord(v) {
11
10
  return typeof v === "object" && v != null && !Array.isArray(v);
12
11
  }
@@ -177,9 +176,6 @@ function assertArtifactTransport(value) {
177
176
  throw new Error("bad ConnectArtifact.transport");
178
177
  return value;
179
178
  }
180
- export function hasArtifactOnlyFields(value) {
181
- return Object.keys(value).some((key) => ARTIFACT_ONLY_KEYS.has(key));
182
- }
183
179
  export function assertConnectArtifact(value) {
184
180
  const record = assertArtifactObject(value);
185
181
  if (record.v !== 1)
@@ -1,22 +1,22 @@
1
+ import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
2
+ import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
1
3
  import { type ClientObserverLike } from "../observability/observer.js";
2
- import { type CorrelationContext, type ScopeMetadataEntry } from "./artifact.js";
4
+ import { type ConnectArtifact, type ScopeMetadataEntry } from "./artifact.js";
3
5
  export type ConnectScopeResolver = (entry: ScopeMetadataEntry) => void | Promise<void>;
4
6
  export type ConnectScopeResolverMap = Readonly<Record<string, ConnectScopeResolver>>;
5
- type NormalizeOptions = Readonly<{
7
+ type ResolveOptions = Readonly<{
6
8
  observer?: ClientObserverLike;
7
9
  scopeResolvers?: ConnectScopeResolverMap;
8
10
  relaxedOptionalScopeValidation?: boolean;
9
11
  }>;
10
- export type NormalizedConnectInput = Readonly<{
12
+ export type ResolvedConnectArtifact = Readonly<{
11
13
  kind: "tunnel";
12
- input: unknown;
13
- correlation?: CorrelationContext;
14
+ input: ChannelInitGrant;
14
15
  observer?: ClientObserverLike;
15
16
  }> | Readonly<{
16
17
  kind: "direct";
17
- input: unknown;
18
- correlation?: CorrelationContext;
18
+ input: DirectConnectInfo;
19
19
  observer?: ClientObserverLike;
20
20
  }>;
21
- export declare function normalizeConnectInput(input: unknown, opts?: NormalizeOptions): Promise<NormalizedConnectInput>;
21
+ export declare function resolveConnectArtifact(input: ConnectArtifact, opts?: ResolveOptions): Promise<ResolvedConnectArtifact>;
22
22
  export {};
@@ -0,0 +1,87 @@
1
+ import { emitObserverDiagnostic, normalizeObserver, withObserverContext } from "../observability/observer.js";
2
+ import { FlowersecError } from "../utils/errors.js";
3
+ import { assertConnectArtifact, } from "./artifact.js";
4
+ async function validateArtifactScopes(artifact, opts, observer) {
5
+ const scoped = artifact.scoped ?? [];
6
+ if (scoped.length === 0)
7
+ return;
8
+ const path = artifact.transport;
9
+ for (const entry of scoped) {
10
+ const resolver = opts.scopeResolvers?.[entry.scope];
11
+ if (resolver == null) {
12
+ if (entry.critical) {
13
+ throw new FlowersecError({
14
+ path,
15
+ stage: "validate",
16
+ code: "resolve_failed",
17
+ message: `missing scope resolver for ${entry.scope}@${entry.scope_version}`,
18
+ });
19
+ }
20
+ emitObserverDiagnostic(observer, {
21
+ path,
22
+ stage: "scope",
23
+ code_domain: "event",
24
+ code: "scope_ignored_missing_resolver",
25
+ result: "skip",
26
+ });
27
+ continue;
28
+ }
29
+ try {
30
+ await resolver(entry);
31
+ }
32
+ catch (error) {
33
+ if (!entry.critical && opts.relaxedOptionalScopeValidation === true) {
34
+ emitObserverDiagnostic(observer, {
35
+ path,
36
+ stage: "scope",
37
+ code_domain: "event",
38
+ code: "scope_ignored_relaxed_validation",
39
+ result: "skip",
40
+ });
41
+ continue;
42
+ }
43
+ throw new FlowersecError({
44
+ path,
45
+ stage: "validate",
46
+ code: "resolve_failed",
47
+ message: `scope validation failed for ${entry.scope}@${entry.scope_version}`,
48
+ cause: error,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ export async function resolveConnectArtifact(input, opts = {}) {
54
+ let artifact;
55
+ try {
56
+ artifact = assertConnectArtifact(input);
57
+ }
58
+ catch (error) {
59
+ throw new FlowersecError({
60
+ path: "auto",
61
+ stage: "validate",
62
+ code: "invalid_input",
63
+ message: "invalid ConnectArtifact",
64
+ cause: error,
65
+ });
66
+ }
67
+ const observer = opts.observer == null
68
+ ? undefined
69
+ : normalizeObserver(withObserverContext(opts.observer, {
70
+ path: artifact.transport,
71
+ ...(artifact.correlation?.trace_id === undefined ? {} : { traceId: artifact.correlation.trace_id }),
72
+ ...(artifact.correlation?.session_id === undefined ? {} : { sessionId: artifact.correlation.session_id }),
73
+ }), { path: artifact.transport });
74
+ await validateArtifactScopes(artifact, opts, observer);
75
+ if (artifact.transport === "direct") {
76
+ return {
77
+ kind: "direct",
78
+ input: artifact.direct_info,
79
+ ...(observer === undefined ? {} : { observer }),
80
+ };
81
+ }
82
+ return {
83
+ kind: "tunnel",
84
+ input: artifact.tunnel_grant,
85
+ ...(observer === undefined ? {} : { observer }),
86
+ };
87
+ }
@@ -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
  }),
@@ -1,7 +1,7 @@
1
1
  import { gcm } from "@noble/ciphers/aes";
2
2
  import { concatBytes, readU32be, readU64be, u32be, u64be } from "../utils/bin.js";
3
3
  import { PROTOCOL_VERSION, RECORD_MAGIC, RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
4
- const te = new TextEncoder();
4
+ const recordMagicBytes = new TextEncoder().encode(RECORD_MAGIC);
5
5
  // RecordError marks record parsing or cryptographic failures.
6
6
  export class RecordError extends Error {
7
7
  }
@@ -28,7 +28,7 @@ export function encryptRecord(key, noncePrefix, flags, seq, plaintext, maxRecord
28
28
  if (cipherLen > 0xffffffff)
29
29
  throw new RecordError("record too large");
30
30
  const header = new Uint8Array(4 + 1 + 1 + 8 + 4);
31
- header.set(te.encode(RECORD_MAGIC), 0);
31
+ header.set(recordMagicBytes, 0);
32
32
  header[4] = PROTOCOL_VERSION;
33
33
  header[5] = flags & 0xff;
34
34
  header.set(u64be(seq), 6);
@@ -51,8 +51,12 @@ export function decryptRecord(key, noncePrefix, frame, expectSeq, maxRecordBytes
51
51
  throw new RecordError("record too large");
52
52
  if (frame.length < headerLen)
53
53
  throw new RecordError("record too short");
54
- if (new TextDecoder().decode(frame.slice(0, 4)) !== RECORD_MAGIC)
54
+ if (frame[0] !== recordMagicBytes[0] ||
55
+ frame[1] !== recordMagicBytes[1] ||
56
+ frame[2] !== recordMagicBytes[2] ||
57
+ frame[3] !== recordMagicBytes[3]) {
55
58
  throw new RecordError("bad record magic");
59
+ }
56
60
  if (frame[4] !== PROTOCOL_VERSION)
57
61
  throw new RecordError("bad record version");
58
62
  const flags = frame[5];
@@ -67,7 +71,7 @@ export function decryptRecord(key, noncePrefix, frame, expectSeq, maxRecordBytes
67
71
  throw new RecordError("length mismatch");
68
72
  const nonce = concatBytes([noncePrefix, u64be(seq)]);
69
73
  try {
70
- const plaintext = gcm(key, nonce, frame.slice(0, headerLen)).decrypt(frame.slice(headerLen));
74
+ const plaintext = gcm(key, nonce, frame.subarray(0, headerLen)).decrypt(frame.subarray(headerLen));
71
75
  return { flags, seq, plaintext };
72
76
  }
73
77
  catch (e) {
@@ -88,5 +88,6 @@ export declare class SecureChannel {
88
88
  private reserveSendSeq;
89
89
  private sendLoop;
90
90
  private readLoop;
91
+ private clearKeyMaterial;
91
92
  }
92
93
  export {};
@@ -62,11 +62,11 @@ export class SecureChannel {
62
62
  throw new RangeError("maxOutboundBufferedBytes must be a non-negative safe integer");
63
63
  }
64
64
  this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes : maxOutboundBufferedBytes;
65
- this.sendKey = args.sendKey;
66
- this.recvKey = args.recvKey;
65
+ this.sendKey = args.sendKey.slice();
66
+ this.recvKey = args.recvKey.slice();
67
67
  this.sendNoncePrefix = args.sendNoncePrefix;
68
68
  this.recvNoncePrefix = args.recvNoncePrefix;
69
- this.rekeyBase = args.rekeyBase;
69
+ this.rekeyBase = args.rekeyBase.slice();
70
70
  this.transcriptHash = args.transcriptHash;
71
71
  this.sendDir = args.sendDir;
72
72
  this.recvDir = args.recvDir;
@@ -113,11 +113,16 @@ export class SecureChannel {
113
113
  this.sendClosed = true;
114
114
  this.rejectQueuedSenders(this.sendErr ?? new Error("closed"));
115
115
  this.wakeSendWaiters();
116
- this.transport.close();
117
116
  const ws = this.recvWaiters;
118
117
  this.recvWaiters = [];
119
118
  for (const w of ws)
120
119
  w();
120
+ try {
121
+ this.transport.close();
122
+ }
123
+ finally {
124
+ this.clearKeyMaterial();
125
+ }
121
126
  }
122
127
  // sendPing emits a keepalive record.
123
128
  async sendPing() {
@@ -254,6 +259,8 @@ export class SecureChannel {
254
259
  if (req.kind === "app") {
255
260
  const payload = req.payload ?? new Uint8Array();
256
261
  for (let offset = 0; offset < payload.length; offset += this.outboundRecordChunkBytes) {
262
+ if (this.closed || this.sendClosed)
263
+ throw new Error("closed");
257
264
  const chunk = payload.subarray(offset, Math.min(payload.length, offset + this.outboundRecordChunkBytes));
258
265
  const seq = this.reserveSendSeq();
259
266
  frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_APP, seq, chunk, this.maxRecordBytes);
@@ -290,6 +297,8 @@ export class SecureChannel {
290
297
  try {
291
298
  while (!this.closed) {
292
299
  const frame = await this.transport.readBinary();
300
+ if (this.closed)
301
+ return;
293
302
  const { flags, seq, plaintext } = decryptRecord(this.recvKey, this.recvNoncePrefix, frame, this.recvSeq, this.maxRecordBytes);
294
303
  if (seq >= maxRecordSeq) {
295
304
  throw new RecordSeqExhaustedError();
@@ -325,4 +334,9 @@ export class SecureChannel {
325
334
  this.close();
326
335
  }
327
336
  }
337
+ clearKeyMaterial() {
338
+ this.sendKey.fill(0);
339
+ this.recvKey.fill(0);
340
+ this.rekeyBase.fill(0);
341
+ }
328
342
  }
package/dist/facade.d.ts CHANGED
@@ -17,14 +17,11 @@ 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";
24
24
  export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
25
25
  export declare function connectTunnel(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
26
26
  export declare function connectDirect(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
27
- export declare function connect(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
28
- export declare function connect(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
29
27
  export declare function connect(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
30
- export declare function connect(input: unknown, opts: ConnectOptions): Promise<Client>;
package/dist/facade.js CHANGED
@@ -1,27 +1,21 @@
1
1
  import { connectDirect as connectDirectInternal } from "./direct-client/connect.js";
2
2
  import { connectTunnel as connectTunnelInternal } from "./tunnel-client/connect.js";
3
- import { normalizeConnectInput } from "./connect/internalNormalize.js";
4
- import { withObserverContext } from "./observability/observer.js";
3
+ import { resolveConnectArtifact } from "./connect/resolveArtifact.js";
5
4
  export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
6
5
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
7
6
  export { assertConnectArtifact } from "./connect/artifact.js";
8
7
  export { FlowersecError } from "./utils/errors.js";
9
- export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
8
+ export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
10
9
  export async function connectTunnel(grant, opts) {
11
10
  return await connectTunnelInternal(grant, opts);
12
11
  }
13
12
  export async function connectDirect(info, opts) {
14
13
  return await connectDirectInternal(info, opts);
15
14
  }
15
+ // connect resolves an artifact to its explicit direct or tunnel transport.
16
16
  export async function connect(input, opts) {
17
- const normalized = await normalizeConnectInput(input, opts);
18
- const nextObserver = normalized.observer ??
19
- (normalized.correlation == null
20
- ? opts.observer
21
- : withObserverContext(opts.observer, {
22
- ...(normalized.correlation.trace_id === undefined ? {} : { traceId: normalized.correlation.trace_id }),
23
- ...(normalized.correlation.session_id === undefined ? {} : { sessionId: normalized.correlation.session_id }),
24
- }));
17
+ const normalized = await resolveConnectArtifact(input, opts);
18
+ const nextObserver = normalized.observer ?? opts.observer;
25
19
  const nextOpts = (nextObserver === opts.observer ? opts : { ...opts, observer: nextObserver });
26
20
  if (normalized.kind === "direct") {
27
21
  return await connectDirectInternal(normalized.input, nextOpts);
@@ -5,8 +5,6 @@ import { type ConnectOptions } from "../facade.js";
5
5
  import type { ConnectArtifact } from "../connect/artifact.js";
6
6
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
7
7
  import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
8
- export declare function connectNode(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
9
- export declare function connectNode(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
10
8
  export declare function connectNode(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
11
9
  export declare function connectTunnelNode(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
12
10
  export declare function connectDirectNode(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
@@ -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>;