@floegence/flowersec-core 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,7 +60,9 @@ const reconnectConfig = createNodeReconnectConfig({
60
60
  Browser `requestConnectArtifact(...)`, `requestEntryConnectArtifact(...)`, and `ControlplaneRequestError` remain available from `@floegence/flowersec-core/browser` as stable aliases.
61
61
 
62
62
  High-level connects use `RequireTLS` by default. `AllowPlaintextForLoopback` permits only literal loopback
63
- targets without DNS resolution; `AllowPlaintext` is an explicit acceptance of pre-E2EE metadata exposure.
63
+ targets without DNS resolution. Deliberate non-loopback `ws://` connections must use
64
+ `createNetworkPlaintextPolicy(...)` with exact canonical IP literals and
65
+ `PlaintextRiskAcceptance.acceptPreE2ECredentialExposure`. The unrestricted `AllowPlaintext` preset is deprecated.
64
66
 
65
67
  ## Node endpoint and controlplane
66
68
 
@@ -1,7 +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
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
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
7
  export type { ConnectArtifactRequestConfig, ControlplaneConfig, EntryConnectArtifactRequestConfig, EntryControlplaneConfig, } from "./controlplane.js";
@@ -1,5 +1,5 @@
1
1
  export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
2
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
3
3
  export { assertConnectArtifact } from "../connect/artifact.js";
4
4
  export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
5
5
  export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
@@ -2,7 +2,16 @@ 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. */
5
6
  export declare const AllowPlaintext: "allow_plaintext";
7
+ export declare const PlaintextRiskAcceptance: {
8
+ readonly acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure";
9
+ };
10
+ export type PlaintextRiskAcceptance = typeof PlaintextRiskAcceptance[keyof typeof PlaintextRiskAcceptance];
11
+ export type NetworkPlaintextPolicyOptions = Readonly<{
12
+ allowedHosts: readonly string[];
13
+ riskAcceptance: PlaintextRiskAcceptance;
14
+ }>;
6
15
  export type TransportSecurityPolicyInput = Readonly<{
7
16
  path: ClientPath;
8
17
  scheme: "ws" | "wss";
@@ -11,6 +20,7 @@ export type TransportSecurityPolicyInput = Readonly<{
11
20
  }>;
12
21
  export type TransportSecurityPolicyPreset = typeof RequireTLS | typeof AllowPlaintextForLoopback | typeof AllowPlaintext;
13
22
  export type TransportSecurityPolicy = TransportSecurityPolicyPreset | ((input: TransportSecurityPolicyInput) => boolean | Promise<boolean>);
23
+ export declare function createNetworkPlaintextPolicy(options: NetworkPlaintextPolicyOptions): TransportSecurityPolicy;
14
24
  export declare function enforceTransportSecurity(args: Readonly<{
15
25
  rawUrl: string;
16
26
  path: ClientPath;
@@ -2,7 +2,21 @@ 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. */
5
6
  export const AllowPlaintext = "allow_plaintext";
7
+ export const PlaintextRiskAcceptance = {
8
+ acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure",
9
+ };
10
+ export function createNetworkPlaintextPolicy(options) {
11
+ if (options?.riskAcceptance !== PlaintextRiskAcceptance.acceptPreE2ECredentialExposure) {
12
+ throw new Error("network plaintext policy requires explicit pre-E2EE credential exposure acceptance");
13
+ }
14
+ if (!Array.isArray(options.allowedHosts) || options.allowedHosts.length === 0) {
15
+ throw new Error("network plaintext policy requires at least one allowed host");
16
+ }
17
+ const allowedHosts = new Set(options.allowedHosts.map(canonicalNetworkPlaintextHost));
18
+ return (input) => input.scheme === "wss" || (input.scheme === "ws" && allowedHosts.has(input.host));
19
+ }
6
20
  export async function enforceTransportSecurity(args) {
7
21
  let target;
8
22
  try {
@@ -105,6 +119,81 @@ function isLiteralLoopbackHost(host) {
105
119
  }
106
120
  return octets[0] === 127;
107
121
  }
122
+ function canonicalNetworkPlaintextHost(rawHost) {
123
+ const host = String(rawHost ?? "").trim();
124
+ if (host === "" || host !== host.toLowerCase() || /[@/?#%\[\]]/.test(host)) {
125
+ throw new Error(`invalid network plaintext allowed host ${JSON.stringify(rawHost)}`);
126
+ }
127
+ if (host.includes(":")) {
128
+ return canonicalNetworkIPv6Host(host);
129
+ }
130
+ const octets = canonicalIPv4Octets(host);
131
+ const firstOctet = octets[0];
132
+ const secondOctet = octets[1];
133
+ if (firstOctet == null || secondOctet == null) {
134
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(rawHost)}`);
135
+ }
136
+ if (firstOctet === 127 || (firstOctet === 169 && secondOctet === 254) || firstOctet >= 224 || octets.every((value) => value === 0)) {
137
+ throw new Error(`network plaintext allowed host must be a non-loopback unicast IP literal: ${JSON.stringify(rawHost)}`);
138
+ }
139
+ return host;
140
+ }
141
+ function canonicalIPv4Octets(host) {
142
+ const parts = host.split(".");
143
+ if (parts.length !== 4) {
144
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
145
+ }
146
+ const octets = parts.map((part) => {
147
+ if (!/^(0|[1-9]\d{0,2})$/.test(part)) {
148
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
149
+ }
150
+ const value = Number(part);
151
+ if (value > 255) {
152
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
153
+ }
154
+ return value;
155
+ });
156
+ return octets;
157
+ }
158
+ function canonicalNetworkIPv6Host(host) {
159
+ let parsed;
160
+ try {
161
+ parsed = new URL(`http://[${host}]/`);
162
+ }
163
+ catch {
164
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
165
+ }
166
+ const canonical = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
167
+ if (canonical !== host) {
168
+ throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
169
+ }
170
+ const words = expandIPv6Words(canonical);
171
+ const unspecified = words.every((word) => word === 0);
172
+ const loopback = words.slice(0, 7).every((word) => word === 0) && words[7] === 1;
173
+ const mappedIPv4 = words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff;
174
+ const multicast = (words[0] & 0xff00) === 0xff00;
175
+ const linkLocal = (words[0] & 0xffc0) === 0xfe80;
176
+ if (unspecified || loopback || mappedIPv4 || multicast || linkLocal) {
177
+ throw new Error(`network plaintext allowed host must be a non-loopback unicast IP literal: ${JSON.stringify(host)}`);
178
+ }
179
+ return canonical;
180
+ }
181
+ function expandIPv6Words(host) {
182
+ const halves = host.split("::");
183
+ if (halves.length > 2)
184
+ throw new Error("invalid IPv6 host");
185
+ const left = halves[0] === "" ? [] : halves[0].split(":");
186
+ const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":");
187
+ const missing = 8 - left.length - right.length;
188
+ if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1)) {
189
+ throw new Error("invalid IPv6 host");
190
+ }
191
+ const words = [...left, ...Array.from({ length: missing }, () => "0"), ...right].map((part) => Number.parseInt(part, 16));
192
+ if (words.length !== 8 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 0xffff)) {
193
+ throw new Error("invalid IPv6 host");
194
+ }
195
+ return words;
196
+ }
108
197
  function detectRuntime() {
109
198
  if (typeof window !== "undefined" && typeof window.document !== "undefined")
110
199
  return "browser";
package/dist/facade.d.ts CHANGED
@@ -17,8 +17,8 @@ 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, RequireTLS, } from "./client-connect/transportSecurity.js";
21
- export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
20
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
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;
package/dist/facade.js CHANGED
@@ -6,7 +6,7 @@ export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
6
6
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
7
7
  export { assertConnectArtifact } from "./connect/artifact.js";
8
8
  export { FlowersecError } from "./utils/errors.js";
9
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
9
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
10
10
  export async function connectTunnel(grant, opts) {
11
11
  return await connectTunnelInternal(grant, opts);
12
12
  }
@@ -1,6 +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
+ export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
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";
6
6
  export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
@@ -1,5 +1,5 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
- export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
2
+ export { AllowPlaintext, 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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -120,8 +120,8 @@
120
120
  "scripts": {
121
121
  "build": "tsc -p tsconfig.build.json",
122
122
  "bench": "vitest bench --run",
123
- "test": "vitest run",
124
- "test:coverage": "vitest run --coverage",
123
+ "test": "npm run build && vitest run",
124
+ "test:coverage": "npm run build && vitest run --coverage",
125
125
  "lint": "eslint .",
126
126
  "verify:package": "node ./scripts/verify-package-exports.mjs",
127
127
  "prepack": "npm run build",