@floegence/flowersec-core 0.18.0 → 0.19.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.
@@ -0,0 +1,223 @@
1
+ import { assertProxyPresetManifest, resolveNamedProxyPreset, } from "./preset.js";
2
+ const RUNTIME_SCOPE_NAME = "proxy.runtime";
3
+ const PRESET_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;
4
+ const MAX_RUNTIME_PAYLOAD_BYTES = 8 * 1024;
5
+ const MAX_RUNTIME_DEPTH = 8;
6
+ const MAX_RUNTIME_FIELDS = 64;
7
+ const MAX_RUNTIME_SNAPSHOT_BYTES = 4 * 1024;
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value != null && !Array.isArray(value);
10
+ }
11
+ function assertNoUnknownFields(kind, value, allowed) {
12
+ const allowedSet = new Set(allowed);
13
+ for (const key of Object.keys(value)) {
14
+ if (!allowedSet.has(key))
15
+ throw new Error(`bad ${kind}.${key}`);
16
+ }
17
+ }
18
+ function assertPositiveInt(name, value) {
19
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
20
+ throw new Error(`bad ${name}`);
21
+ }
22
+ return value;
23
+ }
24
+ function utf8Len(value) {
25
+ return new TextEncoder().encode(value).length;
26
+ }
27
+ function maxContainerDepth(value) {
28
+ if (Array.isArray(value)) {
29
+ let best = 1;
30
+ for (const entry of value)
31
+ best = Math.max(best, 1 + maxContainerDepth(entry));
32
+ return best;
33
+ }
34
+ if (isRecord(value)) {
35
+ let best = 1;
36
+ for (const entry of Object.values(value))
37
+ best = Math.max(best, 1 + maxContainerDepth(entry));
38
+ return best;
39
+ }
40
+ return 0;
41
+ }
42
+ function countFields(value) {
43
+ if (Array.isArray(value))
44
+ return value.reduce((total, entry) => total + countFields(entry), 0);
45
+ if (isRecord(value)) {
46
+ return Object.entries(value).reduce((total, [, entry]) => total + 1 + countFields(entry), 0);
47
+ }
48
+ return 0;
49
+ }
50
+ function assertRuntimePayloadEnvelope(payload) {
51
+ if (!isRecord(payload))
52
+ throw new Error("bad proxy.runtime.payload");
53
+ if (utf8Len(JSON.stringify(payload)) > MAX_RUNTIME_PAYLOAD_BYTES)
54
+ throw new Error("bad proxy.runtime.payload");
55
+ if (maxContainerDepth(payload) > MAX_RUNTIME_DEPTH)
56
+ throw new Error("bad proxy.runtime.payload");
57
+ if (countFields(payload) > MAX_RUNTIME_FIELDS)
58
+ throw new Error("bad proxy.runtime.payload");
59
+ return payload;
60
+ }
61
+ function assertServiceWorkerConfig(value) {
62
+ if (!isRecord(value))
63
+ throw new Error("bad proxy.runtime.serviceWorker");
64
+ assertNoUnknownFields("proxy.runtime.serviceWorker", value, ["scriptUrl", "scope"]);
65
+ const scriptUrl = String(value.scriptUrl ?? "").trim();
66
+ const scope = String(value.scope ?? "").trim();
67
+ if (scriptUrl === "")
68
+ throw new Error("bad proxy.runtime.serviceWorker.scriptUrl");
69
+ if (scope === "")
70
+ throw new Error("bad proxy.runtime.serviceWorker.scope");
71
+ return Object.freeze({ scriptUrl, scope });
72
+ }
73
+ function assertAllowedOrigins(value) {
74
+ if (!Array.isArray(value))
75
+ throw new Error("bad proxy.runtime.controllerBridge.allowedOrigins");
76
+ const out = [];
77
+ const seen = new Set();
78
+ for (const entry of value) {
79
+ const normalized = String(entry ?? "").trim();
80
+ if (normalized === "" || seen.has(normalized))
81
+ continue;
82
+ seen.add(normalized);
83
+ out.push(normalized);
84
+ }
85
+ if (out.length === 0)
86
+ throw new Error("bad proxy.runtime.controllerBridge.allowedOrigins");
87
+ return Object.freeze(out);
88
+ }
89
+ function assertControllerBridgeConfig(value) {
90
+ if (!isRecord(value))
91
+ throw new Error("bad proxy.runtime.controllerBridge");
92
+ assertNoUnknownFields("proxy.runtime.controllerBridge", value, ["allowedOrigins"]);
93
+ return Object.freeze({
94
+ allowedOrigins: assertAllowedOrigins(value.allowedOrigins),
95
+ });
96
+ }
97
+ function assertLimits(value) {
98
+ if (!isRecord(value))
99
+ throw new Error("bad proxy.runtime.limits");
100
+ assertNoUnknownFields("proxy.runtime.limits", value, [
101
+ "timeoutMs",
102
+ "maxJsonFrameBytes",
103
+ "maxChunkBytes",
104
+ "maxBodyBytes",
105
+ "maxWsFrameBytes",
106
+ ]);
107
+ const out = {};
108
+ if (value.timeoutMs !== undefined)
109
+ out.timeoutMs = assertPositiveInt("proxy.runtime.limits.timeoutMs", value.timeoutMs);
110
+ if (value.maxJsonFrameBytes !== undefined) {
111
+ out.maxJsonFrameBytes = assertPositiveInt("proxy.runtime.limits.maxJsonFrameBytes", value.maxJsonFrameBytes);
112
+ }
113
+ if (value.maxChunkBytes !== undefined)
114
+ out.maxChunkBytes = assertPositiveInt("proxy.runtime.limits.maxChunkBytes", value.maxChunkBytes);
115
+ if (value.maxBodyBytes !== undefined)
116
+ out.maxBodyBytes = assertPositiveInt("proxy.runtime.limits.maxBodyBytes", value.maxBodyBytes);
117
+ if (value.maxWsFrameBytes !== undefined)
118
+ out.maxWsFrameBytes = assertPositiveInt("proxy.runtime.limits.maxWsFrameBytes", value.maxWsFrameBytes);
119
+ return Object.freeze(out);
120
+ }
121
+ function assertPreset(value) {
122
+ if (!isRecord(value))
123
+ throw new Error("bad proxy.runtime.preset");
124
+ assertNoUnknownFields("proxy.runtime.preset", value, ["presetId", "snapshot"]);
125
+ const presetId = String(value.presetId ?? "").trim();
126
+ if (!PRESET_ID_RE.test(presetId))
127
+ throw new Error("bad proxy.runtime.preset.presetId");
128
+ if (value.snapshot === undefined) {
129
+ return Object.freeze({ presetId });
130
+ }
131
+ const snapshot = assertProxyPresetManifest(value.snapshot);
132
+ if (utf8Len(JSON.stringify(snapshot)) > MAX_RUNTIME_SNAPSHOT_BYTES)
133
+ throw new Error("bad proxy.runtime.preset.snapshot");
134
+ return Object.freeze({ presetId, snapshot });
135
+ }
136
+ export function assertProxyRuntimeScopeV1(payload) {
137
+ const value = assertRuntimePayloadEnvelope(payload);
138
+ assertNoUnknownFields("proxy.runtime", value, [
139
+ "mode",
140
+ "appBasePath",
141
+ "serviceWorker",
142
+ "controllerBridge",
143
+ "preset",
144
+ "limits",
145
+ ]);
146
+ const mode = value.mode;
147
+ if (mode !== "service_worker" && mode !== "controller_bridge") {
148
+ throw new Error("bad proxy.runtime.mode");
149
+ }
150
+ const appBasePath = value.appBasePath === undefined ? undefined : String(value.appBasePath ?? "").trim();
151
+ if (value.appBasePath !== undefined && appBasePath === "") {
152
+ throw new Error("bad proxy.runtime.appBasePath");
153
+ }
154
+ const preset = assertPreset(value.preset);
155
+ const limits = value.limits === undefined ? undefined : assertLimits(value.limits);
156
+ if (mode === "service_worker") {
157
+ if (value.controllerBridge !== undefined)
158
+ throw new Error("bad proxy.runtime.controllerBridge");
159
+ const serviceWorker = assertServiceWorkerConfig(value.serviceWorker);
160
+ return Object.freeze({
161
+ mode,
162
+ ...(appBasePath === undefined ? {} : { appBasePath }),
163
+ serviceWorker,
164
+ preset,
165
+ ...(limits === undefined ? {} : { limits }),
166
+ });
167
+ }
168
+ if (value.serviceWorker !== undefined)
169
+ throw new Error("bad proxy.runtime.serviceWorker");
170
+ const controllerBridge = assertControllerBridgeConfig(value.controllerBridge);
171
+ return Object.freeze({
172
+ mode,
173
+ ...(appBasePath === undefined ? {} : { appBasePath }),
174
+ controllerBridge,
175
+ preset,
176
+ ...(limits === undefined ? {} : { limits }),
177
+ });
178
+ }
179
+ export function extractProxyRuntimeScopeV1(artifact, mode) {
180
+ const scoped = artifact.scoped ?? [];
181
+ const entry = scoped.find((item) => item.scope === RUNTIME_SCOPE_NAME);
182
+ if (!entry) {
183
+ throw new Error("missing proxy.runtime@1 scope");
184
+ }
185
+ if (entry.scope_version !== 1) {
186
+ throw new Error(`unsupported proxy.runtime scope_version: ${entry.scope_version}`);
187
+ }
188
+ const scope = assertProxyRuntimeScopeV1(entry.payload);
189
+ if (scope.mode !== mode) {
190
+ throw new Error(`proxy.runtime mode mismatch: expected ${mode}`);
191
+ }
192
+ return scope;
193
+ }
194
+ export function resolveRuntimeLimitsFromScope(scope, overrides) {
195
+ const merged = {
196
+ ...(scope.limits ?? {}),
197
+ ...(overrides ?? {}),
198
+ };
199
+ return Object.keys(merged).length === 0 ? undefined : merged;
200
+ }
201
+ export function resolvePresetInputFromScope(scope, presetOverride) {
202
+ if (presetOverride !== undefined)
203
+ return presetOverride;
204
+ if (scope.preset.snapshot)
205
+ return scope.preset.snapshot;
206
+ try {
207
+ return resolveNamedProxyPreset(scope.preset.presetId);
208
+ }
209
+ catch {
210
+ return undefined;
211
+ }
212
+ }
213
+ export function resolveRuntimePresetLimits(scope) {
214
+ if (scope.limits === undefined)
215
+ return undefined;
216
+ return Object.freeze({
217
+ ...(scope.limits.maxJsonFrameBytes === undefined ? {} : { max_json_frame_bytes: scope.limits.maxJsonFrameBytes }),
218
+ ...(scope.limits.maxChunkBytes === undefined ? {} : { max_chunk_bytes: scope.limits.maxChunkBytes }),
219
+ ...(scope.limits.maxBodyBytes === undefined ? {} : { max_body_bytes: scope.limits.maxBodyBytes }),
220
+ ...(scope.limits.maxWsFrameBytes === undefined ? {} : { max_ws_frame_bytes: scope.limits.maxWsFrameBytes }),
221
+ ...(scope.limits.timeoutMs === undefined ? {} : { timeout_ms: scope.limits.timeoutMs }),
222
+ });
223
+ }
@@ -159,6 +159,7 @@ self.addEventListener("install", (event) => {
159
159
  });
160
160
 
161
161
  self.addEventListener("activate", (event) => {
162
+ runtimeClientId = null;
162
163
  event.waitUntil(self.clients.claim());
163
164
  });
164
165
 
@@ -207,16 +208,10 @@ async function getWindowClient(preferredClientId) {
207
208
  return cs.length > 0 ? cs[0] : null;
208
209
  }
209
210
 
210
- if (runtimeClientId) {
211
- const c = await self.clients.get(runtimeClientId);
212
- if (c) return c;
213
- runtimeClientId = null;
214
- }
215
- const cs = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
216
- if (cs.length > 0) {
217
- runtimeClientId = cs[0].id;
218
- return cs[0];
219
- }
211
+ if (!runtimeClientId) return null;
212
+ const c = await self.clients.get(runtimeClientId);
213
+ if (c) return c;
214
+ runtimeClientId = null;
220
215
  return null;
221
216
  }
222
217
 
@@ -0,0 +1,13 @@
1
+ import type { ConnectArtifact } from "../connect/artifact.js";
2
+ import { type RequestConnectArtifactInput, type RequestEntryConnectArtifactInput } from "../controlplane/index.js";
3
+ export type ArtifactFactoryArgs = Readonly<{
4
+ traceId?: string;
5
+ signal?: AbortSignal;
6
+ }>;
7
+ export type ArtifactAwareReconnectConfig = Readonly<{
8
+ artifact?: ConnectArtifact;
9
+ getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
10
+ artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
11
+ }>;
12
+ export declare function resolveConnectArtifact(config: ArtifactAwareReconnectConfig, traceId?: string, signal?: AbortSignal): Promise<ConnectArtifact>;
13
+ export declare function updateTraceId(current: string | undefined, artifact: ConnectArtifact): string | undefined;
@@ -0,0 +1,34 @@
1
+ import { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/index.js";
2
+ export async function resolveConnectArtifact(config, traceId, signal) {
3
+ if (config.getArtifact) {
4
+ return await config.getArtifact({
5
+ ...(traceId === undefined ? {} : { traceId }),
6
+ ...(signal === undefined ? {} : { signal }),
7
+ });
8
+ }
9
+ if (config.artifact)
10
+ return config.artifact;
11
+ if (config.artifactControlplane) {
12
+ const correlation = traceId === undefined
13
+ ? config.artifactControlplane.correlation
14
+ : { traceId };
15
+ if ("entryTicket" in config.artifactControlplane) {
16
+ const input = {
17
+ ...config.artifactControlplane,
18
+ ...(correlation === undefined ? {} : { correlation }),
19
+ ...(signal === undefined ? {} : { signal }),
20
+ };
21
+ return await requestEntryConnectArtifact(input);
22
+ }
23
+ const input = {
24
+ ...config.artifactControlplane,
25
+ ...(correlation === undefined ? {} : { correlation }),
26
+ ...(signal === undefined ? {} : { signal }),
27
+ };
28
+ return await requestConnectArtifact(input);
29
+ }
30
+ throw new Error("Artifact reconnect config requires `getArtifact`, `artifact`, or `artifactControlplane`");
31
+ }
32
+ export function updateTraceId(current, artifact) {
33
+ return artifact.correlation?.trace_id ?? current;
34
+ }
@@ -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" | "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" | "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;
@@ -17,7 +17,9 @@ export class FlowersecError extends Error {
17
17
  cause;
18
18
  constructor(args) {
19
19
  const prefix = `${args.path} ${args.stage} (${args.code})`;
20
- const message = args.message != null && args.message !== "" ? `${prefix}: ${args.message}` : prefix;
20
+ const message = args.message != null && args.message !== ""
21
+ ? `${prefix}: ${args.message}`
22
+ : prefix;
21
23
  super(message, args.cause !== undefined ? { cause: args.cause } : undefined);
22
24
  this.name = "FlowersecError";
23
25
  this.code = args.code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/browser/index.d.ts",
37
37
  "default": "./dist/browser/index.js"
38
38
  },
39
+ "./controlplane": {
40
+ "types": "./dist/controlplane/index.d.ts",
41
+ "default": "./dist/controlplane/index.js"
42
+ },
39
43
  "./framing": {
40
44
  "types": "./dist/framing/index.d.ts",
41
45
  "default": "./dist/framing/index.js"
@@ -102,7 +106,7 @@
102
106
  }
103
107
  },
104
108
  "engines": {
105
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
109
+ "node": ">=24.0.0"
106
110
  },
107
111
  "scripts": {
108
112
  "build": "tsc -p tsconfig.build.json",
@@ -121,7 +125,7 @@
121
125
  "ws": "^8.18.0"
122
126
  },
123
127
  "devDependencies": {
124
- "@types/node": "^22.10.0",
128
+ "@types/node": "^24.0.0",
125
129
  "@types/ws": "^8.5.12",
126
130
  "@typescript-eslint/eslint-plugin": "^8.18.0",
127
131
  "@typescript-eslint/parser": "^8.18.0",