@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.
- package/README.md +2 -2
- package/dist/browser/connect.d.ts +2 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.js +2 -2
- package/dist/client-connect/connectCore.d.ts +1 -1
- package/dist/client-connect/transportSecurity.d.ts +1 -3
- package/dist/client-connect/transportSecurity.js +0 -4
- package/dist/connect/artifact.d.ts +0 -1
- package/dist/connect/artifact.js +0 -4
- package/dist/connect/{internalNormalize.d.ts → resolveArtifact.d.ts} +8 -8
- package/dist/connect/resolveArtifact.js +87 -0
- package/dist/controlplane/request.d.ts +1 -0
- package/dist/controlplane/request.js +77 -2
- package/dist/defaults.d.ts +1 -0
- package/dist/defaults.js +1 -0
- package/dist/e2ee/record.js +8 -4
- package/dist/e2ee/secureChannel.d.ts +1 -0
- package/dist/e2ee/secureChannel.js +18 -4
- package/dist/facade.d.ts +1 -4
- package/dist/facade.js +5 -11
- package/dist/node/connect.d.ts +0 -2
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/proxy/bootstrap.d.ts +5 -24
- package/dist/proxy/bootstrap.js +1 -11
- package/dist/proxy/constants.d.ts +1 -0
- package/dist/proxy/constants.js +1 -0
- package/dist/proxy/integration.js +0 -6
- package/dist/proxy/preset.d.ts +0 -1
- package/dist/proxy/preset.js +0 -9
- package/dist/proxy/runtimeScope.js +2 -7
- package/dist/proxy/server.d.ts +0 -1
- package/dist/proxy/server.js +257 -145
- package/dist/tunnel-client/connect.d.ts +1 -1
- package/dist/tunnel-client/connect.js +1 -11
- package/package.json +4 -6
- package/dist/browser/controlplane.d.ts +0 -15
- package/dist/browser/controlplane.js +0 -65
- package/dist/connect/internalNormalize.js +0 -173
- package/dist/proxy/profiles.d.ts +0 -18
- package/dist/proxy/profiles.js +0 -71
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ High-level WebSocket connections require TLS by default. Use `AllowPlaintextForL
|
|
|
29
29
|
|
|
30
30
|
## Node Proxy Server
|
|
31
31
|
|
|
32
|
-
The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`.
|
|
32
|
+
The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`. HTTP request and response bodies stream between Flowersec chunk frames and the upstream fetch while remaining bounded per request by `maxBodyBytes`. Proxy sessions admit at most `maxConcurrentStreams` HTTP/WebSocket streams, with a shared default of 64; RPC serving has its own concurrency controls. WebSocket frames remain bounded by `maxWsFrameBytes`; `maxWsQueuedBytes` additionally caps upstream-to-Yamux queued bytes per connection, while the reverse direction waits for each Node `ws` send callback.
|
|
33
33
|
|
|
34
|
-
The defaults keep one
|
|
34
|
+
The defaults keep one queued WebSocket frame plus its proxy header. Raise body, concurrency, or queue limits only when the deployment has matching upstream and memory capacity.
|
|
35
35
|
|
|
36
36
|
## Runtime Boundaries
|
|
37
37
|
|
|
@@ -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:
|
|
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>;
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
2
2
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
3
|
-
export {
|
|
3
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
4
4
|
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
5
5
|
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
6
6
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
7
|
-
export type {
|
|
8
|
-
export {
|
|
7
|
+
export type { 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";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
4
|
-
export {
|
|
4
|
+
export { 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/
|
|
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
|
|
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;
|
package/dist/connect/artifact.js
CHANGED
|
@@ -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
|
|
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
|
|
7
|
+
type ResolveOptions = Readonly<{
|
|
6
8
|
observer?: ClientObserverLike;
|
|
7
9
|
scopeResolvers?: ConnectScopeResolverMap;
|
|
8
10
|
relaxedOptionalScopeValidation?: boolean;
|
|
9
11
|
}>;
|
|
10
|
-
export type
|
|
12
|
+
export type ResolvedConnectArtifact = Readonly<{
|
|
11
13
|
kind: "tunnel";
|
|
12
|
-
input:
|
|
13
|
-
correlation?: CorrelationContext;
|
|
14
|
+
input: ChannelInitGrant;
|
|
14
15
|
observer?: ClientObserverLike;
|
|
15
16
|
}> | Readonly<{
|
|
16
17
|
kind: "direct";
|
|
17
|
-
input:
|
|
18
|
-
correlation?: CorrelationContext;
|
|
18
|
+
input: DirectConnectInfo;
|
|
19
19
|
observer?: ClientObserverLike;
|
|
20
20
|
}>;
|
|
21
|
-
export declare function
|
|
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
|
+
}
|
|
@@ -34,6 +34,76 @@ export function buildControlplaneURL(baseUrl, path) {
|
|
|
34
34
|
return path;
|
|
35
35
|
return `${base.replace(/\/+$/, "")}${path}`;
|
|
36
36
|
}
|
|
37
|
+
function transportPolicyDenied() {
|
|
38
|
+
return new ControlplaneRequestError({
|
|
39
|
+
status: 0,
|
|
40
|
+
code: "transport_policy_denied",
|
|
41
|
+
message: "controlplane transport security policy denied URL",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function validateArtifactURL(rawUrl, allowLoopbackHTTP) {
|
|
45
|
+
let target;
|
|
46
|
+
try {
|
|
47
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(rawUrl)) {
|
|
48
|
+
target = new URL(rawUrl);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const base = globalThis.location?.href;
|
|
52
|
+
if (!base)
|
|
53
|
+
throw new Error("relative URL requires a browser location");
|
|
54
|
+
target = new URL(rawUrl, base);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw transportPolicyDenied();
|
|
59
|
+
}
|
|
60
|
+
if (target.username !== "" || target.password !== "" || target.hash !== "") {
|
|
61
|
+
throw transportPolicyDenied();
|
|
62
|
+
}
|
|
63
|
+
if (target.protocol === "https:")
|
|
64
|
+
return;
|
|
65
|
+
if (target.protocol === "http:"
|
|
66
|
+
&& allowLoopbackHTTP === true
|
|
67
|
+
&& isLiteralLoopbackHost(sourceHostname(rawUrl, target)))
|
|
68
|
+
return;
|
|
69
|
+
throw transportPolicyDenied();
|
|
70
|
+
}
|
|
71
|
+
function sourceHostname(rawUrl, target) {
|
|
72
|
+
const authority = /^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/([^/?#]*)/.exec(rawUrl)?.[1];
|
|
73
|
+
if (authority === undefined)
|
|
74
|
+
return target.hostname;
|
|
75
|
+
const hostPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
76
|
+
if (hostPort.startsWith("[")) {
|
|
77
|
+
const end = hostPort.indexOf("]");
|
|
78
|
+
if (end < 0 || (hostPort.length > end + 1 && !/^:\d+$/.test(hostPort.slice(end + 1)))) {
|
|
79
|
+
return hostPort;
|
|
80
|
+
}
|
|
81
|
+
return hostPort.slice(1, end);
|
|
82
|
+
}
|
|
83
|
+
const colon = hostPort.lastIndexOf(":");
|
|
84
|
+
if (colon < 0)
|
|
85
|
+
return hostPort;
|
|
86
|
+
if (hostPort.indexOf(":") !== colon || !/^\d+$/.test(hostPort.slice(colon + 1)))
|
|
87
|
+
return hostPort;
|
|
88
|
+
return hostPort.slice(0, colon);
|
|
89
|
+
}
|
|
90
|
+
function isLiteralLoopbackHost(rawHost) {
|
|
91
|
+
const host = rawHost.startsWith("[") && rawHost.endsWith("]")
|
|
92
|
+
? rawHost.slice(1, -1).toLowerCase()
|
|
93
|
+
: rawHost.toLowerCase();
|
|
94
|
+
if (host === "localhost" || host === "::1")
|
|
95
|
+
return true;
|
|
96
|
+
const parts = host.split(".");
|
|
97
|
+
if (parts.length !== 4)
|
|
98
|
+
return false;
|
|
99
|
+
const octets = parts.map((part) => {
|
|
100
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part))
|
|
101
|
+
return -1;
|
|
102
|
+
const value = Number(part);
|
|
103
|
+
return value <= 255 ? value : -1;
|
|
104
|
+
});
|
|
105
|
+
return octets.every((value) => value >= 0) && octets[0] === 127;
|
|
106
|
+
}
|
|
37
107
|
function parseMaybeJSON(bodyText) {
|
|
38
108
|
const trimmed = String(bodyText ?? "").trim();
|
|
39
109
|
if (trimmed === "")
|
|
@@ -110,6 +180,7 @@ export async function requestControlplaneJSON(url, init) {
|
|
|
110
180
|
const response = await runFetch(url, {
|
|
111
181
|
...init,
|
|
112
182
|
cache: "no-store",
|
|
183
|
+
redirect: "error",
|
|
113
184
|
});
|
|
114
185
|
let rawBody = "";
|
|
115
186
|
try {
|
|
@@ -164,7 +235,9 @@ function createJSONHeaders(input) {
|
|
|
164
235
|
return headers;
|
|
165
236
|
}
|
|
166
237
|
export async function requestConnectArtifact(config) {
|
|
167
|
-
const
|
|
238
|
+
const url = buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_CONNECT_ARTIFACT_PATH);
|
|
239
|
+
validateArtifactURL(url, config.allowLoopbackHTTP);
|
|
240
|
+
const data = (await requestControlplaneJSON(url, {
|
|
168
241
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
169
242
|
method: "POST",
|
|
170
243
|
credentials: config.credentials ?? "omit",
|
|
@@ -183,7 +256,9 @@ export async function requestEntryConnectArtifact(config) {
|
|
|
183
256
|
throw new Error("entryTicket is required");
|
|
184
257
|
const headers = createJSONHeaders(config.headers);
|
|
185
258
|
headers.set("Authorization", `Bearer ${entryTicket}`);
|
|
186
|
-
const
|
|
259
|
+
const url = buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH);
|
|
260
|
+
validateArtifactURL(url, config.allowLoopbackHTTP);
|
|
261
|
+
const data = (await requestControlplaneJSON(url, {
|
|
187
262
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
188
263
|
method: "POST",
|
|
189
264
|
credentials: config.credentials ?? "omit",
|
package/dist/defaults.d.ts
CHANGED
package/dist/defaults.js
CHANGED
package/dist/e2ee/record.js
CHANGED
|
@@ -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
|
|
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(
|
|
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 (
|
|
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.
|
|
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) {
|
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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);
|
package/dist/node/connect.d.ts
CHANGED
|
@@ -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>;
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
4
4
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
5
5
|
export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export {
|
|
2
|
+
export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
4
4
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
5
5
|
export * from "../endpoint/index.js";
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
2
|
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
3
|
-
import type { ConnectBrowserOptions
|
|
4
|
-
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
3
|
+
import type { ConnectBrowserOptions } from "../browser/connect.js";
|
|
5
4
|
import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
|
|
6
5
|
import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
|
|
7
6
|
import type { ProxyPresetInput } from "./preset.js";
|
|
8
7
|
import { type ProxyRuntime } from "./runtime.js";
|
|
9
|
-
export type
|
|
10
|
-
connect?: TunnelConnectBrowserOptions;
|
|
11
|
-
preset?: ProxyPresetInput;
|
|
12
|
-
runtimeGlobalKey?: string;
|
|
13
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
14
|
-
serviceWorker: ProxyIntegrationServiceWorkerOptions;
|
|
15
|
-
plugins?: readonly ProxyIntegrationPlugin[];
|
|
16
|
-
}>;
|
|
17
|
-
export type ConnectTunnelProxyBrowserHandle = Readonly<{
|
|
8
|
+
export type ConnectArtifactProxyBrowserHandle = Readonly<{
|
|
18
9
|
client: Client;
|
|
19
10
|
runtime: ProxyRuntime;
|
|
20
11
|
dispose: () => Promise<void>;
|
|
@@ -27,15 +18,7 @@ export type ConnectArtifactProxyBrowserOptions = Readonly<{
|
|
|
27
18
|
serviceWorker?: ProxyIntegrationServiceWorkerOptions;
|
|
28
19
|
plugins?: readonly ProxyIntegrationPlugin[];
|
|
29
20
|
}>;
|
|
30
|
-
export type
|
|
31
|
-
connect?: TunnelConnectBrowserOptions;
|
|
32
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
33
|
-
allowedOrigins: RegisterProxyControllerWindowOptions["allowedOrigins"];
|
|
34
|
-
targetWindow?: RegisterProxyControllerWindowOptions["targetWindow"];
|
|
35
|
-
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
36
|
-
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
37
|
-
}>;
|
|
38
|
-
export type ConnectTunnelProxyControllerBrowserHandle = Readonly<{
|
|
21
|
+
export type ConnectArtifactProxyControllerBrowserHandle = Readonly<{
|
|
39
22
|
client: Client;
|
|
40
23
|
runtime: ProxyRuntime;
|
|
41
24
|
dispose: () => void;
|
|
@@ -48,7 +31,5 @@ export type ConnectArtifactProxyControllerBrowserOptions = Readonly<{
|
|
|
48
31
|
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
49
32
|
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
50
33
|
}>;
|
|
51
|
-
export declare function
|
|
52
|
-
export declare function
|
|
53
|
-
export declare function connectTunnelProxyControllerBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
54
|
-
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
34
|
+
export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectArtifactProxyBrowserHandle>;
|
|
35
|
+
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectArtifactProxyControllerBrowserHandle>;
|