@floegence/flowersec-core 0.17.2 → 0.19.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 +34 -9
- package/dist/browser/controlplane.d.ts +5 -18
- package/dist/browser/controlplane.js +17 -67
- package/dist/browser/index.d.ts +4 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/reconnectConfig.d.ts +15 -2
- package/dist/browser/reconnectConfig.js +43 -12
- package/dist/client-connect/connectCore.d.ts +5 -0
- package/dist/client-connect/connectCore.js +1 -1
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +1 -1
- package/dist/client-connect/tunnelAttachCloseReason.js +4 -1
- package/dist/connect/artifact.d.ts +37 -0
- package/dist/connect/artifact.js +217 -0
- package/dist/connect/internalNormalize.d.ts +22 -0
- package/dist/connect/internalNormalize.js +173 -0
- package/dist/controlplane/index.d.ts +2 -0
- package/dist/controlplane/index.js +1 -0
- package/dist/controlplane/request.d.ts +50 -0
- package/dist/controlplane/request.js +136 -0
- package/dist/e2ee/secureChannel.js +2 -5
- package/dist/facade.d.ts +4 -0
- package/dist/facade.js +15 -47
- package/dist/node/connect.d.ts +3 -0
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +2 -0
- package/dist/node/reconnectConfig.d.ts +42 -0
- package/dist/node/reconnectConfig.js +78 -0
- package/dist/observability/observer.d.ts +28 -2
- package/dist/observability/observer.js +296 -13
- package/dist/proxy/bootstrap.d.ts +22 -4
- package/dist/proxy/bootstrap.js +51 -6
- package/dist/proxy/index.d.ts +3 -1
- package/dist/proxy/index.js +2 -1
- package/dist/proxy/integration.d.ts +3 -3
- package/dist/proxy/integration.js +21 -10
- package/dist/proxy/preset.d.ts +31 -0
- package/dist/proxy/preset.js +133 -0
- package/dist/proxy/profiles.d.ts +2 -0
- package/dist/proxy/profiles.js +40 -17
- package/dist/proxy/runtimeScope.d.ts +49 -0
- package/dist/proxy/runtimeScope.js +223 -0
- package/dist/reconnect/artifactControlplane.d.ts +13 -0
- package/dist/reconnect/artifactControlplane.js +34 -0
- package/dist/reconnect/index.d.ts +1 -1
- package/dist/reconnect/index.js +13 -6
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.js +3 -1
- package/package.json +7 -3
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { DEFAULT_MAX_JSON_FRAME_BYTES } from "../framing/jsonframe.js";
|
|
2
|
+
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_BYTES } from "./constants.js";
|
|
3
|
+
const PRESET_ID_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
4
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
5
|
+
max_json_frame_bytes: DEFAULT_MAX_JSON_FRAME_BYTES,
|
|
6
|
+
max_chunk_bytes: DEFAULT_MAX_CHUNK_BYTES,
|
|
7
|
+
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
|
|
8
|
+
max_ws_frame_bytes: DEFAULT_MAX_WS_FRAME_BYTES,
|
|
9
|
+
});
|
|
10
|
+
export const DEFAULT_PROXY_PRESET_MANIFEST = Object.freeze({
|
|
11
|
+
v: 1,
|
|
12
|
+
preset_id: "default",
|
|
13
|
+
limits: {
|
|
14
|
+
max_json_frame_bytes: DEFAULT_MAX_JSON_FRAME_BYTES,
|
|
15
|
+
max_chunk_bytes: DEFAULT_MAX_CHUNK_BYTES,
|
|
16
|
+
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
|
|
17
|
+
max_ws_frame_bytes: DEFAULT_MAX_WS_FRAME_BYTES,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
export const CODESERVER_PROXY_PRESET_MANIFEST = Object.freeze({
|
|
21
|
+
v: 1,
|
|
22
|
+
preset_id: "codeserver",
|
|
23
|
+
deprecated: true,
|
|
24
|
+
limits: {
|
|
25
|
+
max_json_frame_bytes: DEFAULT_MAX_JSON_FRAME_BYTES,
|
|
26
|
+
max_chunk_bytes: DEFAULT_MAX_CHUNK_BYTES,
|
|
27
|
+
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
|
|
28
|
+
max_ws_frame_bytes: 32 * 1024 * 1024,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
function isRecord(v) {
|
|
32
|
+
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
33
|
+
}
|
|
34
|
+
function assertNoUnknownFields(kind, value, allowed) {
|
|
35
|
+
const allowedSet = new Set(allowed);
|
|
36
|
+
for (const key of Object.keys(value)) {
|
|
37
|
+
if (!allowedSet.has(key))
|
|
38
|
+
throw new Error(`bad ${kind}.${key}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assertPositiveInt(name, value) {
|
|
42
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
43
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function assertLimits(value) {
|
|
48
|
+
if (!isRecord(value))
|
|
49
|
+
throw new Error("bad ProxyPresetManifest.limits");
|
|
50
|
+
assertNoUnknownFields("ProxyPresetManifest.limits", value, [
|
|
51
|
+
"max_json_frame_bytes",
|
|
52
|
+
"max_chunk_bytes",
|
|
53
|
+
"max_body_bytes",
|
|
54
|
+
"max_ws_frame_bytes",
|
|
55
|
+
"timeout_ms",
|
|
56
|
+
]);
|
|
57
|
+
const out = {};
|
|
58
|
+
if (value.max_json_frame_bytes !== undefined)
|
|
59
|
+
out.max_json_frame_bytes = assertPositiveInt("max_json_frame_bytes", value.max_json_frame_bytes);
|
|
60
|
+
if (value.max_chunk_bytes !== undefined)
|
|
61
|
+
out.max_chunk_bytes = assertPositiveInt("max_chunk_bytes", value.max_chunk_bytes);
|
|
62
|
+
if (value.max_body_bytes !== undefined)
|
|
63
|
+
out.max_body_bytes = assertPositiveInt("max_body_bytes", value.max_body_bytes);
|
|
64
|
+
if (value.max_ws_frame_bytes !== undefined)
|
|
65
|
+
out.max_ws_frame_bytes = assertPositiveInt("max_ws_frame_bytes", value.max_ws_frame_bytes);
|
|
66
|
+
if (value.timeout_ms !== undefined)
|
|
67
|
+
out.timeout_ms = assertPositiveInt("timeout_ms", value.timeout_ms);
|
|
68
|
+
return Object.freeze(out);
|
|
69
|
+
}
|
|
70
|
+
export function assertProxyPresetManifest(value) {
|
|
71
|
+
if (!isRecord(value))
|
|
72
|
+
throw new Error("bad ProxyPresetManifest");
|
|
73
|
+
assertNoUnknownFields("ProxyPresetManifest", value, ["v", "preset_id", "deprecated", "limits"]);
|
|
74
|
+
if (value.v !== 1)
|
|
75
|
+
throw new Error("bad ProxyPresetManifest.v");
|
|
76
|
+
if (typeof value.preset_id !== "string" || !PRESET_ID_RE.test(value.preset_id)) {
|
|
77
|
+
throw new Error("bad ProxyPresetManifest.preset_id");
|
|
78
|
+
}
|
|
79
|
+
if (value.deprecated !== undefined && typeof value.deprecated !== "boolean") {
|
|
80
|
+
throw new Error("bad ProxyPresetManifest.deprecated");
|
|
81
|
+
}
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
v: 1,
|
|
84
|
+
preset_id: value.preset_id,
|
|
85
|
+
...(value.deprecated === undefined ? {} : { deprecated: value.deprecated }),
|
|
86
|
+
limits: assertLimits(value.limits),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function resolveNamedProxyPreset(name) {
|
|
90
|
+
switch (String(name ?? "").trim()) {
|
|
91
|
+
case "":
|
|
92
|
+
case "default":
|
|
93
|
+
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
94
|
+
case "codeserver":
|
|
95
|
+
return CODESERVER_PROXY_PRESET_MANIFEST;
|
|
96
|
+
default:
|
|
97
|
+
throw new Error(`unknown proxy preset: ${name}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function toResolved(manifest) {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
v: 1,
|
|
103
|
+
preset_id: manifest.preset_id,
|
|
104
|
+
deprecated: manifest.deprecated === true,
|
|
105
|
+
limits: Object.freeze({
|
|
106
|
+
max_json_frame_bytes: manifest.limits.max_json_frame_bytes ?? DEFAULT_LIMITS.max_json_frame_bytes,
|
|
107
|
+
max_chunk_bytes: manifest.limits.max_chunk_bytes ?? DEFAULT_LIMITS.max_chunk_bytes,
|
|
108
|
+
max_body_bytes: manifest.limits.max_body_bytes ?? DEFAULT_LIMITS.max_body_bytes,
|
|
109
|
+
max_ws_frame_bytes: manifest.limits.max_ws_frame_bytes ?? DEFAULT_LIMITS.max_ws_frame_bytes,
|
|
110
|
+
...(manifest.limits.timeout_ms === undefined ? {} : { timeout_ms: manifest.limits.timeout_ms }),
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function resolveProxyPreset(input) {
|
|
115
|
+
if (input == null)
|
|
116
|
+
return toResolved(DEFAULT_PROXY_PRESET_MANIFEST);
|
|
117
|
+
if (isRecord(input) && ("v" in input || "presetId" in input || "limits" in input)) {
|
|
118
|
+
return toResolved(assertProxyPresetManifest(input));
|
|
119
|
+
}
|
|
120
|
+
const limits = assertLimits(input);
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
v: 1,
|
|
123
|
+
preset_id: "custom",
|
|
124
|
+
deprecated: false,
|
|
125
|
+
limits: Object.freeze({
|
|
126
|
+
max_json_frame_bytes: limits.max_json_frame_bytes ?? DEFAULT_LIMITS.max_json_frame_bytes,
|
|
127
|
+
max_chunk_bytes: limits.max_chunk_bytes ?? DEFAULT_LIMITS.max_chunk_bytes,
|
|
128
|
+
max_body_bytes: limits.max_body_bytes ?? DEFAULT_LIMITS.max_body_bytes,
|
|
129
|
+
max_ws_frame_bytes: limits.max_ws_frame_bytes ?? DEFAULT_LIMITS.max_ws_frame_bytes,
|
|
130
|
+
...(limits.timeout_ms === undefined ? {} : { timeout_ms: limits.timeout_ms }),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
}
|
package/dist/proxy/profiles.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProxyPresetManifest } from "./preset.js";
|
|
1
2
|
export type ProxyProfile = Readonly<{
|
|
2
3
|
maxJsonFrameBytes: number;
|
|
3
4
|
maxChunkBytes: number;
|
|
@@ -21,3 +22,4 @@ export declare const PROXY_PROFILE_CODESERVER: Readonly<{
|
|
|
21
22
|
timeoutMs: number;
|
|
22
23
|
}>;
|
|
23
24
|
export declare function resolveProxyProfile(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyProfile;
|
|
25
|
+
export declare function profileToPresetManifest(profile?: ProxyProfileName | Partial<ProxyProfile>): ProxyPresetManifest;
|
package/dist/proxy/profiles.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Keep aligned with redeven/redeven-agent production profile.
|
|
15
|
-
maxWsFrameBytes: 32 * 1024 * 1024,
|
|
16
|
-
timeoutMs: 0,
|
|
17
|
-
});
|
|
1
|
+
import { CODESERVER_PROXY_PRESET_MANIFEST, DEFAULT_PROXY_PRESET_MANIFEST, resolveProxyPreset, } from "./preset.js";
|
|
2
|
+
function toLegacyProfile(manifest) {
|
|
3
|
+
const resolved = resolveProxyPreset(manifest);
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
maxJsonFrameBytes: resolved.limits.max_json_frame_bytes,
|
|
6
|
+
maxChunkBytes: resolved.limits.max_chunk_bytes,
|
|
7
|
+
maxBodyBytes: resolved.limits.max_body_bytes,
|
|
8
|
+
maxWsFrameBytes: resolved.limits.max_ws_frame_bytes,
|
|
9
|
+
timeoutMs: resolved.limits.timeout_ms ?? 0,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
const DEFAULT_PROFILE = toLegacyProfile(DEFAULT_PROXY_PRESET_MANIFEST);
|
|
13
|
+
const CODESERVER_PROFILE = toLegacyProfile(CODESERVER_PROXY_PRESET_MANIFEST);
|
|
18
14
|
export const PROXY_PROFILE_DEFAULT = DEFAULT_PROFILE;
|
|
19
15
|
export const PROXY_PROFILE_CODESERVER = CODESERVER_PROFILE;
|
|
20
16
|
function normalizeSafeInt(name, value) {
|
|
@@ -52,3 +48,30 @@ export function resolveProxyProfile(profile) {
|
|
|
52
48
|
timeoutMs: normalizeSafeInt("timeoutMs", profile.timeoutMs ?? base.timeoutMs),
|
|
53
49
|
});
|
|
54
50
|
}
|
|
51
|
+
export function profileToPresetManifest(profile) {
|
|
52
|
+
if (profile == null)
|
|
53
|
+
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
54
|
+
if (typeof profile === "string") {
|
|
55
|
+
switch (profile) {
|
|
56
|
+
case "default":
|
|
57
|
+
return DEFAULT_PROXY_PRESET_MANIFEST;
|
|
58
|
+
case "codeserver":
|
|
59
|
+
return CODESERVER_PROXY_PRESET_MANIFEST;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`unknown proxy profile: ${profile}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const resolved = resolveProxyProfile(profile);
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
v: 1,
|
|
67
|
+
preset_id: "legacy-profile",
|
|
68
|
+
deprecated: true,
|
|
69
|
+
limits: {
|
|
70
|
+
...(resolved.maxJsonFrameBytes > 0 ? { max_json_frame_bytes: resolved.maxJsonFrameBytes } : {}),
|
|
71
|
+
...(resolved.maxChunkBytes > 0 ? { max_chunk_bytes: resolved.maxChunkBytes } : {}),
|
|
72
|
+
...(resolved.maxBodyBytes > 0 ? { max_body_bytes: resolved.maxBodyBytes } : {}),
|
|
73
|
+
...(resolved.maxWsFrameBytes > 0 ? { max_ws_frame_bytes: resolved.maxWsFrameBytes } : {}),
|
|
74
|
+
...(resolved.timeoutMs > 0 ? { timeout_ms: resolved.timeoutMs } : {}),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConnectArtifact, ScopePayload } from "../connect/artifact.js";
|
|
2
|
+
import { assertProxyPresetManifest, type ProxyPresetInput, type ProxyPresetLimits } from "./preset.js";
|
|
3
|
+
export type ProxyPresetSnapshotV1 = ReturnType<typeof assertProxyPresetManifest>;
|
|
4
|
+
type ProxyRuntimeScopeBaseV1 = Readonly<{
|
|
5
|
+
appBasePath?: string;
|
|
6
|
+
preset: Readonly<{
|
|
7
|
+
presetId: string;
|
|
8
|
+
snapshot?: ProxyPresetSnapshotV1;
|
|
9
|
+
}>;
|
|
10
|
+
limits?: Readonly<{
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
maxJsonFrameBytes?: number;
|
|
13
|
+
maxChunkBytes?: number;
|
|
14
|
+
maxBodyBytes?: number;
|
|
15
|
+
maxWsFrameBytes?: number;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
18
|
+
export type ProxyRuntimeServiceWorkerScopeV1 = ProxyRuntimeScopeBaseV1 & Readonly<{
|
|
19
|
+
mode: "service_worker";
|
|
20
|
+
serviceWorker: Readonly<{
|
|
21
|
+
scriptUrl: string;
|
|
22
|
+
scope: string;
|
|
23
|
+
}>;
|
|
24
|
+
}>;
|
|
25
|
+
export type ProxyRuntimeControllerBridgeScopeV1 = ProxyRuntimeScopeBaseV1 & Readonly<{
|
|
26
|
+
mode: "controller_bridge";
|
|
27
|
+
controllerBridge: Readonly<{
|
|
28
|
+
allowedOrigins: readonly string[];
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
export type ProxyRuntimeScopeV1 = ProxyRuntimeServiceWorkerScopeV1 | ProxyRuntimeControllerBridgeScopeV1;
|
|
32
|
+
export declare function assertProxyRuntimeScopeV1(payload: ScopePayload): ProxyRuntimeScopeV1;
|
|
33
|
+
export declare function extractProxyRuntimeScopeV1(artifact: ConnectArtifact, mode: ProxyRuntimeScopeV1["mode"]): ProxyRuntimeScopeV1;
|
|
34
|
+
export declare function resolveRuntimeLimitsFromScope(scope: ProxyRuntimeScopeV1, overrides: Readonly<{
|
|
35
|
+
maxJsonFrameBytes?: number;
|
|
36
|
+
maxChunkBytes?: number;
|
|
37
|
+
maxBodyBytes?: number;
|
|
38
|
+
maxWsFrameBytes?: number;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}> | undefined): Readonly<{
|
|
41
|
+
maxJsonFrameBytes?: number;
|
|
42
|
+
maxChunkBytes?: number;
|
|
43
|
+
maxBodyBytes?: number;
|
|
44
|
+
maxWsFrameBytes?: number;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
}> | undefined;
|
|
47
|
+
export declare function resolvePresetInputFromScope(scope: ProxyRuntimeScopeV1, presetOverride: ProxyPresetInput | undefined): ProxyPresetInput | undefined;
|
|
48
|
+
export declare function resolveRuntimePresetLimits(scope: ProxyRuntimeScopeV1): ProxyPresetLimits | undefined;
|
|
49
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type ClientObserverLike } from "../observability/observer.js";
|
|
3
3
|
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
|
4
4
|
export type AutoReconnectConfig = Readonly<{
|
|
5
5
|
enabled?: boolean;
|
package/dist/reconnect/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withObserverContext } from "../observability/observer.js";
|
|
1
2
|
function normalizeAutoReconnect(cfg) {
|
|
2
3
|
if (!cfg?.enabled) {
|
|
3
4
|
return {
|
|
@@ -59,6 +60,7 @@ export function createReconnectManager() {
|
|
|
59
60
|
let retryTimer = null;
|
|
60
61
|
let retryResolve = null;
|
|
61
62
|
let attemptAbort = null;
|
|
63
|
+
let attemptSeq = 0;
|
|
62
64
|
const cancelRetrySleep = () => {
|
|
63
65
|
if (retryTimer) {
|
|
64
66
|
clearTimeout(retryTimer);
|
|
@@ -90,6 +92,7 @@ export function createReconnectManager() {
|
|
|
90
92
|
active = null;
|
|
91
93
|
activeConnectPromise = null;
|
|
92
94
|
token += 1;
|
|
95
|
+
attemptSeq = 0;
|
|
93
96
|
if (s.client) {
|
|
94
97
|
try {
|
|
95
98
|
s.client.close();
|
|
@@ -138,9 +141,9 @@ export function createReconnectManager() {
|
|
|
138
141
|
// connectWithRetry updates state; keep errors observable via state().
|
|
139
142
|
});
|
|
140
143
|
};
|
|
141
|
-
const createObserver = (t, cfg) => {
|
|
144
|
+
const createObserver = (t, cfg, currentAttemptSeq) => {
|
|
142
145
|
const user = cfg.observer;
|
|
143
|
-
return {
|
|
146
|
+
return withObserverContext({
|
|
144
147
|
onConnect: (...args) => user?.onConnect?.(...args),
|
|
145
148
|
onAttach: (...args) => user?.onAttach?.(...args),
|
|
146
149
|
onHandshake: (...args) => user?.onHandshake?.(...args),
|
|
@@ -156,12 +159,14 @@ export function createReconnectManager() {
|
|
|
156
159
|
},
|
|
157
160
|
onRpcCall: (...args) => user?.onRpcCall?.(...args),
|
|
158
161
|
onRpcNotify: (...args) => user?.onRpcNotify?.(...args),
|
|
159
|
-
}
|
|
162
|
+
}, {
|
|
163
|
+
attemptSeq: currentAttemptSeq,
|
|
164
|
+
});
|
|
160
165
|
};
|
|
161
|
-
const connectOnce = async (t, cfg) => {
|
|
166
|
+
const connectOnce = async (t, cfg, currentAttemptSeq) => {
|
|
162
167
|
abortActiveAttempt();
|
|
163
168
|
attemptAbort = new AbortController();
|
|
164
|
-
return await cfg.connectOnce({ signal: attemptAbort.signal, observer: createObserver(t, cfg) ?? {} });
|
|
169
|
+
return await cfg.connectOnce({ signal: attemptAbort.signal, observer: createObserver(t, cfg, currentAttemptSeq) ?? {} });
|
|
165
170
|
};
|
|
166
171
|
const connectWithRetry = async (t, cfg) => {
|
|
167
172
|
const ar = normalizeAutoReconnect(cfg.autoReconnect);
|
|
@@ -172,8 +177,9 @@ export function createReconnectManager() {
|
|
|
172
177
|
if (active !== cfg)
|
|
173
178
|
return;
|
|
174
179
|
attempts += 1;
|
|
180
|
+
attemptSeq += 1;
|
|
175
181
|
try {
|
|
176
|
-
const client = await connectOnce(t, cfg);
|
|
182
|
+
const client = await connectOnce(t, cfg, attemptSeq);
|
|
177
183
|
if (t !== token) {
|
|
178
184
|
try {
|
|
179
185
|
client.close();
|
|
@@ -218,6 +224,7 @@ export function createReconnectManager() {
|
|
|
218
224
|
token += 1;
|
|
219
225
|
const t = token;
|
|
220
226
|
active = cfg;
|
|
227
|
+
attemptSeq = 0;
|
|
221
228
|
if (s.client) {
|
|
222
229
|
try {
|
|
223
230
|
s.client.close();
|
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" | "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;
|
package/dist/utils/errors.js
CHANGED
|
@@ -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 !== ""
|
|
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.
|
|
3
|
+
"version": "0.19.0",
|
|
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": "
|
|
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": "^
|
|
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",
|