@floegence/flowersec-core 0.19.10 → 0.19.11
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 +6 -1
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +1 -0
- package/dist/client-connect/connectCore.d.ts +3 -0
- package/dist/client-connect/connectCore.js +7 -0
- package/dist/client-connect/transportSecurity.d.ts +19 -0
- package/dist/client-connect/transportSecurity.js +116 -0
- package/dist/facade.d.ts +2 -0
- package/dist/facade.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ client.close();
|
|
|
30
30
|
Node.js:
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
import { connectNode, createNodeReconnectConfig } from "@floegence/flowersec-core/node";
|
|
33
|
+
import { connectNode, createNodeReconnectConfig, RequireTLS } from "@floegence/flowersec-core/node";
|
|
34
34
|
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
35
35
|
|
|
36
36
|
const artifact = await requestConnectArtifact({
|
|
@@ -40,6 +40,7 @@ const artifact = await requestConnectArtifact({
|
|
|
40
40
|
|
|
41
41
|
const client = await connectNode(artifact, {
|
|
42
42
|
origin: "https://your-app.example",
|
|
43
|
+
transportSecurityPolicy: RequireTLS,
|
|
43
44
|
});
|
|
44
45
|
await client.ping();
|
|
45
46
|
client.close();
|
|
@@ -51,12 +52,16 @@ const reconnectConfig = createNodeReconnectConfig({
|
|
|
51
52
|
},
|
|
52
53
|
connect: {
|
|
53
54
|
origin: "https://your-app.example",
|
|
55
|
+
transportSecurityPolicy: RequireTLS,
|
|
54
56
|
},
|
|
55
57
|
});
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
Browser `requestConnectArtifact(...)`, `requestEntryConnectArtifact(...)`, and `ControlplaneRequestError` remain available from `@floegence/flowersec-core/browser` as stable aliases.
|
|
59
61
|
|
|
62
|
+
Use `RequireTLS` for remote deployments. `AllowPlaintextForLoopback` permits only literal loopback
|
|
63
|
+
targets without DNS resolution; `AllowPlaintext` is an explicit acceptance of pre-E2EE metadata exposure.
|
|
64
|
+
|
|
60
65
|
## Docs
|
|
61
66
|
|
|
62
67
|
- Frontend quickstart: `docs/FRONTEND_QUICKSTART.md`
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
2
2
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
3
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
4
|
+
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "../client-connect/transportSecurity.js";
|
|
3
5
|
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
4
6
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
5
7
|
export type { ConnectArtifactRequestConfig, ControlplaneConfig, EntryConnectArtifactRequestConfig, EntryControlplaneConfig, } from "./controlplane.js";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
2
3
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
3
4
|
export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
|
|
4
5
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
@@ -2,6 +2,7 @@ import { type ClientObserverLike } from "../observability/observer.js";
|
|
|
2
2
|
import { type WebSocketLike } from "../ws-client/binaryTransport.js";
|
|
3
3
|
import type { ClientInternal } from "../client.js";
|
|
4
4
|
import type { ConnectScopeResolverMap } from "../connect/internalNormalize.js";
|
|
5
|
+
import { type TransportSecurityPolicy } from "./transportSecurity.js";
|
|
5
6
|
export type ConnectOptionsBase = Readonly<{
|
|
6
7
|
/** Explicit Origin value (required). In browsers this must match window.location.origin. */
|
|
7
8
|
origin: string;
|
|
@@ -25,6 +26,8 @@ export type ConnectOptionsBase = Readonly<{
|
|
|
25
26
|
wsFactory?: (url: string, origin: string) => WebSocketLike;
|
|
26
27
|
/** Optional observer for client metrics. */
|
|
27
28
|
observer?: ClientObserverLike;
|
|
29
|
+
/** Policy evaluated before any WebSocket network activity. */
|
|
30
|
+
transportSecurityPolicy?: TransportSecurityPolicy;
|
|
28
31
|
/** Experimental scope validators keyed by scope name. */
|
|
29
32
|
scopeResolvers?: ConnectScopeResolverMap;
|
|
30
33
|
/** Experimental migration switch for optional scope failures. */
|
|
@@ -10,6 +10,7 @@ import { WebSocketBinaryTransport, WsCloseError } from "../ws-client/binaryTrans
|
|
|
10
10
|
import { OriginMismatchError, WsFactoryRequiredError, classifyConnectError, classifyHandshakeError, createWebSocket, waitOpen, withAbortAndTimeout, } from "./common.js";
|
|
11
11
|
import { prepareChannelId } from "./contract.js";
|
|
12
12
|
import { isTunnelAttachCloseReason } from "./tunnelAttachCloseReason.js";
|
|
13
|
+
import { enforceTransportSecurity } from "./transportSecurity.js";
|
|
13
14
|
export async function connectCore(args) {
|
|
14
15
|
const observer = normalizeObserver(args.opts.observer, { path: args.path });
|
|
15
16
|
const signal = args.opts.signal;
|
|
@@ -44,6 +45,12 @@ export async function connectCore(args) {
|
|
|
44
45
|
const code = args.path === "tunnel" ? "missing_tunnel_url" : "missing_ws_url";
|
|
45
46
|
throw new FlowersecError({ path: args.path, stage: "validate", code, message: "missing websocket url" });
|
|
46
47
|
}
|
|
48
|
+
await enforceTransportSecurity({
|
|
49
|
+
rawUrl: wsUrl,
|
|
50
|
+
path: args.path,
|
|
51
|
+
...(args.opts.transportSecurityPolicy === undefined ? {} : { policy: args.opts.transportSecurityPolicy }),
|
|
52
|
+
...(args.opts.observer === undefined ? {} : { observer: args.opts.observer }),
|
|
53
|
+
});
|
|
47
54
|
const invalidOption = (message) => {
|
|
48
55
|
throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_option", message });
|
|
49
56
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClientPath } from "../client.js";
|
|
2
|
+
import { type ClientObserverLike } from "../observability/observer.js";
|
|
3
|
+
export declare const RequireTLS: "require_tls";
|
|
4
|
+
export declare const AllowPlaintextForLoopback: "allow_plaintext_for_loopback";
|
|
5
|
+
export declare const AllowPlaintext: "allow_plaintext";
|
|
6
|
+
export type TransportSecurityPolicyInput = Readonly<{
|
|
7
|
+
path: ClientPath;
|
|
8
|
+
scheme: "ws" | "wss";
|
|
9
|
+
host: string;
|
|
10
|
+
runtime: "browser" | "node" | "other";
|
|
11
|
+
}>;
|
|
12
|
+
export type TransportSecurityPolicyPreset = typeof RequireTLS | typeof AllowPlaintextForLoopback | typeof AllowPlaintext;
|
|
13
|
+
export type TransportSecurityPolicy = TransportSecurityPolicyPreset | ((input: TransportSecurityPolicyInput) => boolean | Promise<boolean>);
|
|
14
|
+
export declare function enforceTransportSecurity(args: Readonly<{
|
|
15
|
+
rawUrl: string;
|
|
16
|
+
path: ClientPath;
|
|
17
|
+
policy?: TransportSecurityPolicy;
|
|
18
|
+
observer?: ClientObserverLike;
|
|
19
|
+
}>): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { FlowersecError } from "../utils/errors.js";
|
|
2
|
+
import { emitObserverDiagnostic } from "../observability/observer.js";
|
|
3
|
+
export const RequireTLS = "require_tls";
|
|
4
|
+
export const AllowPlaintextForLoopback = "allow_plaintext_for_loopback";
|
|
5
|
+
export const AllowPlaintext = "allow_plaintext";
|
|
6
|
+
export async function enforceTransportSecurity(args) {
|
|
7
|
+
let target;
|
|
8
|
+
try {
|
|
9
|
+
target = parseWebSocketTarget(args.rawUrl);
|
|
10
|
+
}
|
|
11
|
+
catch (cause) {
|
|
12
|
+
throw denied(args.path, cause);
|
|
13
|
+
}
|
|
14
|
+
const input = {
|
|
15
|
+
path: args.path,
|
|
16
|
+
scheme: target.scheme,
|
|
17
|
+
host: target.host,
|
|
18
|
+
runtime: detectRuntime(),
|
|
19
|
+
};
|
|
20
|
+
if (args.policy === undefined) {
|
|
21
|
+
if (target.scheme === "ws") {
|
|
22
|
+
emitObserverDiagnostic(args.observer, {
|
|
23
|
+
path: args.path,
|
|
24
|
+
stage: "validate",
|
|
25
|
+
code_domain: "event",
|
|
26
|
+
code: "plaintext_transport",
|
|
27
|
+
result: "skip",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let allowed = false;
|
|
33
|
+
try {
|
|
34
|
+
if (typeof args.policy === "function") {
|
|
35
|
+
allowed = await args.policy(input);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
allowed = evaluatePreset(args.policy, target);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (cause) {
|
|
42
|
+
throw denied(args.path, cause);
|
|
43
|
+
}
|
|
44
|
+
if (!allowed)
|
|
45
|
+
throw denied(args.path);
|
|
46
|
+
}
|
|
47
|
+
function denied(path, cause) {
|
|
48
|
+
return new FlowersecError({
|
|
49
|
+
path,
|
|
50
|
+
stage: "validate",
|
|
51
|
+
code: "transport_policy_denied",
|
|
52
|
+
message: "transport security policy denied websocket URL",
|
|
53
|
+
...(cause === undefined ? {} : { cause }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function evaluatePreset(policy, target) {
|
|
57
|
+
switch (policy) {
|
|
58
|
+
case RequireTLS:
|
|
59
|
+
return target.scheme === "wss";
|
|
60
|
+
case AllowPlaintextForLoopback:
|
|
61
|
+
return target.scheme === "wss" || isLiteralLoopbackHost(target.host);
|
|
62
|
+
case AllowPlaintext:
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function parseWebSocketTarget(rawUrl) {
|
|
67
|
+
const raw = rawUrl.trim();
|
|
68
|
+
const match = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)(?:[/?#]|$)/.exec(raw);
|
|
69
|
+
const scheme = match?.[1]?.toLowerCase();
|
|
70
|
+
const authority = match?.[2] ?? "";
|
|
71
|
+
if ((scheme !== "ws" && scheme !== "wss") || authority === "" || authority.includes("@")) {
|
|
72
|
+
throw new Error("invalid websocket URL");
|
|
73
|
+
}
|
|
74
|
+
let host;
|
|
75
|
+
if (authority.startsWith("[")) {
|
|
76
|
+
const end = authority.indexOf("]");
|
|
77
|
+
if (end <= 1 || (authority.slice(end + 1) !== "" && !/^:\d+$/.test(authority.slice(end + 1)))) {
|
|
78
|
+
throw new Error("invalid websocket URL");
|
|
79
|
+
}
|
|
80
|
+
host = authority.slice(1, end).toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const pieces = authority.split(":");
|
|
84
|
+
if (pieces.length > 2 || (pieces.length === 2 && !/^\d+$/.test(pieces[1] ?? ""))) {
|
|
85
|
+
throw new Error("invalid websocket URL");
|
|
86
|
+
}
|
|
87
|
+
host = (pieces[0] ?? "").toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
if (host === "")
|
|
90
|
+
throw new Error("invalid websocket URL");
|
|
91
|
+
return { scheme, host };
|
|
92
|
+
}
|
|
93
|
+
function isLiteralLoopbackHost(host) {
|
|
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 = [];
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part))
|
|
102
|
+
return false;
|
|
103
|
+
const value = Number(part);
|
|
104
|
+
if (value > 255)
|
|
105
|
+
return false;
|
|
106
|
+
octets.push(value);
|
|
107
|
+
}
|
|
108
|
+
return octets[0] === 127;
|
|
109
|
+
}
|
|
110
|
+
function detectRuntime() {
|
|
111
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined")
|
|
112
|
+
return "browser";
|
|
113
|
+
if (typeof process !== "undefined" && process.versions?.node != null)
|
|
114
|
+
return "node";
|
|
115
|
+
return "other";
|
|
116
|
+
}
|
package/dist/facade.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type { ClientObserverLike } from "./observability/observer.js";
|
|
|
14
14
|
export type { Client, ClientPath } from "./client.js";
|
|
15
15
|
export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
|
|
16
16
|
export { FlowersecError } from "./utils/errors.js";
|
|
17
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
18
|
+
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
|
|
17
19
|
export type { TunnelConnectOptions } from "./tunnel-client/connect.js";
|
|
18
20
|
export type { DirectConnectOptions } from "./direct-client/connect.js";
|
|
19
21
|
export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
|
package/dist/facade.js
CHANGED
|
@@ -6,6 +6,7 @@ export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
|
6
6
|
export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
7
7
|
export { assertConnectArtifact } from "./connect/artifact.js";
|
|
8
8
|
export { FlowersecError } from "./utils/errors.js";
|
|
9
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
9
10
|
export async function connectTunnel(grant, opts) {
|
|
10
11
|
return await connectTunnelInternal(grant, opts);
|
|
11
12
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./e2ee/secureChannel.js";
|
|
|
12
12
|
export * from "./e2ee/handshake.js";
|
|
13
13
|
export * from "./e2ee/errors.js";
|
|
14
14
|
export * from "./client.js";
|
|
15
|
+
export * from "./client-connect/transportSecurity.js";
|
|
15
16
|
export * from "./observability/index.js";
|
|
16
17
|
export * from "./ws-client/binaryTransport.js";
|
|
17
18
|
export * from "./yamux/index.js";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./e2ee/secureChannel.js";
|
|
|
12
12
|
export * from "./e2ee/handshake.js";
|
|
13
13
|
export * from "./e2ee/errors.js";
|
|
14
14
|
export * from "./client.js";
|
|
15
|
+
export * from "./client-connect/transportSecurity.js";
|
|
15
16
|
export * from "./observability/index.js";
|
|
16
17
|
export * from "./ws-client/binaryTransport.js";
|
|
17
18
|
export * from "./yamux/index.js";
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
|
+
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "../client-connect/transportSecurity.js";
|
|
2
4
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
3
5
|
export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
4
6
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/node/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
2
3
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
3
4
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
4
5
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class AbortError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
export type FlowersecPath = "auto" | "tunnel" | "direct";
|
|
8
8
|
export type FlowersecStage = "validate" | "connect" | "attach" | "handshake" | "secure" | "yamux" | "rpc" | "close";
|
|
9
|
-
export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "tenant_mismatch" | "policy_denied" | "policy_error" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "rpc_failed" | "not_connected";
|
|
9
|
+
export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "transport_policy_denied" | "credential_commit_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "tenant_mismatch" | "policy_denied" | "policy_error" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "rpc_failed" | "not_connected";
|
|
10
10
|
export declare class FlowersecError extends Error {
|
|
11
11
|
readonly code: FlowersecErrorCode;
|
|
12
12
|
readonly stage: FlowersecStage;
|