@floegence/flowersec-core 0.18.0 → 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.
@@ -0,0 +1,78 @@
1
+ import { resolveConnectArtifact, updateTraceId, } from "../reconnect/artifactControlplane.js";
2
+ import { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
3
+ async function resolveTunnelGrant(config) {
4
+ if (config.getGrant)
5
+ return await config.getGrant();
6
+ if (config.grant)
7
+ return config.grant;
8
+ throw new Error("Tunnel reconnect config requires `getGrant` or `grant`");
9
+ }
10
+ async function resolveDirectInfo(config) {
11
+ if (config.getDirectInfo)
12
+ return await config.getDirectInfo();
13
+ if (config.directInfo)
14
+ return config.directInfo;
15
+ throw new Error("Direct reconnect config requires `getDirectInfo` or `directInfo`");
16
+ }
17
+ export function createTunnelNodeReconnectConfig(config) {
18
+ let traceId = config.artifact?.correlation?.trace_id;
19
+ return {
20
+ ...(config.observer === undefined ? {} : { observer: config.observer }),
21
+ ...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
22
+ connectOnce: async ({ signal, observer }) => {
23
+ if (config.getArtifact || config.artifact || config.artifactControlplane) {
24
+ const artifact = await resolveConnectArtifact(config, traceId, signal);
25
+ if (artifact.transport !== "tunnel") {
26
+ throw new Error("Tunnel reconnect config requires a tunnel ConnectArtifact");
27
+ }
28
+ traceId = updateTraceId(traceId, artifact);
29
+ const connectOptions = {
30
+ ...(config.connect === undefined ? {} : config.connect),
31
+ signal,
32
+ observer,
33
+ };
34
+ return await connectNode(artifact, connectOptions);
35
+ }
36
+ const connectOptions = {
37
+ ...(config.connect === undefined ? {} : config.connect),
38
+ signal,
39
+ observer,
40
+ };
41
+ return await connectTunnelNode(await resolveTunnelGrant(config), connectOptions);
42
+ },
43
+ };
44
+ }
45
+ export function createDirectNodeReconnectConfig(config) {
46
+ let traceId = config.artifact?.correlation?.trace_id;
47
+ return {
48
+ ...(config.observer === undefined ? {} : { observer: config.observer }),
49
+ ...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
50
+ connectOnce: async ({ signal, observer }) => {
51
+ if (config.getArtifact || config.artifact || config.artifactControlplane) {
52
+ const artifact = await resolveConnectArtifact(config, traceId, signal);
53
+ if (artifact.transport !== "direct") {
54
+ throw new Error("Direct reconnect config requires a direct ConnectArtifact");
55
+ }
56
+ traceId = updateTraceId(traceId, artifact);
57
+ const connectOptions = {
58
+ ...(config.connect === undefined ? {} : config.connect),
59
+ signal,
60
+ observer,
61
+ };
62
+ return await connectNode(artifact, connectOptions);
63
+ }
64
+ const connectOptions = {
65
+ ...(config.connect === undefined ? {} : config.connect),
66
+ signal,
67
+ observer,
68
+ };
69
+ return await connectDirectNode(await resolveDirectInfo(config), connectOptions);
70
+ },
71
+ };
72
+ }
73
+ export function createNodeReconnectConfig(config) {
74
+ if (config.mode === "direct") {
75
+ return createDirectNodeReconnectConfig(config);
76
+ }
77
+ return createTunnelNodeReconnectConfig(config);
78
+ }
@@ -2,7 +2,7 @@ import type { ClientPath } from "../client.js";
2
2
  export type ConnectResult = "ok" | "fail";
3
3
  export type ConnectReason = "websocket_error" | "websocket_closed" | "timeout" | "canceled";
4
4
  export type AttachResult = "ok" | "fail";
5
- export type AttachReason = "send_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "role_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "replace_rate_limited" | "attach_failed" | "timeout" | "canceled";
5
+ export type AttachReason = "send_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "role_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "tenant_mismatch" | "policy_denied" | "policy_error" | "replace_rate_limited" | "attach_failed" | "timeout" | "canceled";
6
6
  export type HandshakeResult = "ok" | "fail";
7
7
  export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invalid_suite" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
8
8
  export type WsCloseKind = "local" | "peer_or_error";
@@ -51,12 +51,20 @@ function buildDiagnosticEvent(context, event) {
51
51
  elapsed_ms: Math.max(0, Math.floor(nowMilliseconds() - (context.attemptStartMs ?? nowMilliseconds()))),
52
52
  attempt_seq: Math.max(1, Math.floor(context.attemptSeq ?? 1)),
53
53
  ...(context.traceId === undefined ? {} : { trace_id: context.traceId }),
54
- ...(context.sessionId === undefined ? {} : { session_id: context.sessionId }),
54
+ ...(context.sessionId === undefined
55
+ ? {}
56
+ : { session_id: context.sessionId }),
55
57
  });
56
58
  }
57
59
  function mapConnectDiagnostic(path, result, reason) {
58
60
  if (result === "ok") {
59
- return { path, stage: "connect", code_domain: "event", code: "connect_ok", result: "ok" };
61
+ return {
62
+ path,
63
+ stage: "connect",
64
+ code_domain: "event",
65
+ code: "connect_ok",
66
+ result: "ok",
67
+ };
60
68
  }
61
69
  return {
62
70
  path,
@@ -68,7 +76,13 @@ function mapConnectDiagnostic(path, result, reason) {
68
76
  }
69
77
  function mapAttachDiagnostic(result, reason, path) {
70
78
  if (result === "ok") {
71
- return { path, stage: "attach", code_domain: "event", code: "attach_ok", result: "ok" };
79
+ return {
80
+ path,
81
+ stage: "attach",
82
+ code_domain: "event",
83
+ code: "attach_ok",
84
+ result: "ok",
85
+ };
72
86
  }
73
87
  return {
74
88
  path,
@@ -80,7 +94,13 @@ function mapAttachDiagnostic(result, reason, path) {
80
94
  }
81
95
  function mapHandshakeDiagnostic(path, result, reason) {
82
96
  if (result === "ok") {
83
- return { path, stage: "handshake", code_domain: "event", code: "handshake_ok", result: "ok" };
97
+ return {
98
+ path,
99
+ stage: "handshake",
100
+ code_domain: "event",
101
+ code: "handshake_ok",
102
+ result: "ok",
103
+ };
84
104
  }
85
105
  return {
86
106
  path,
@@ -153,14 +173,20 @@ class ObserverDispatcher {
153
173
  emitDiagnosticEvent(event) {
154
174
  const diagnostic = buildDiagnosticEvent(this[OBSERVER_CONTEXT], event);
155
175
  this.enqueue({
156
- kind: event.code === "diagnostics_overflow" ? "overflow" : event.result === "fail" ? "terminal" : "normal",
176
+ kind: event.code === "diagnostics_overflow"
177
+ ? "overflow"
178
+ : event.result === "fail"
179
+ ? "terminal"
180
+ : "normal",
157
181
  deliver: () => this.observer.onDiagnosticEvent?.(diagnostic),
158
182
  });
159
183
  }
160
184
  enqueueCombined(callback, diagnostic, terminal) {
161
185
  if (callback == null && diagnostic == null)
162
186
  return;
163
- const event = diagnostic == null ? undefined : buildDiagnosticEvent(this[OBSERVER_CONTEXT], diagnostic);
187
+ const event = diagnostic == null
188
+ ? undefined
189
+ : buildDiagnosticEvent(this[OBSERVER_CONTEXT], diagnostic);
164
190
  this.enqueue({
165
191
  kind: terminal ? "terminal" : "normal",
166
192
  deliver: () => {
@@ -277,7 +303,8 @@ export function nowSeconds() {
277
303
  return nowMilliseconds() / 1000;
278
304
  }
279
305
  function nowMilliseconds() {
280
- if (typeof performance !== "undefined" && typeof performance.now === "function") {
306
+ if (typeof performance !== "undefined" &&
307
+ typeof performance.now === "function") {
281
308
  return performance.now();
282
309
  }
283
310
  return Date.now();
@@ -1,5 +1,6 @@
1
1
  import type { Client } from "../client.js";
2
- import type { TunnelConnectBrowserOptions } from "../browser/connect.js";
2
+ import type { ConnectArtifact } from "../connect/artifact.js";
3
+ import type { ConnectBrowserOptions, TunnelConnectBrowserOptions } from "../browser/connect.js";
3
4
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
4
5
  import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
5
6
  import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
@@ -18,6 +19,14 @@ export type ConnectTunnelProxyBrowserHandle = Readonly<{
18
19
  runtime: ProxyRuntime;
19
20
  dispose: () => Promise<void>;
20
21
  }>;
22
+ export type ConnectArtifactProxyBrowserOptions = Readonly<{
23
+ connect?: ConnectBrowserOptions;
24
+ preset?: ProxyPresetInput;
25
+ runtimeGlobalKey?: string;
26
+ runtime?: RegisterProxyIntegrationOptions["runtime"];
27
+ serviceWorker?: ProxyIntegrationServiceWorkerOptions;
28
+ plugins?: readonly ProxyIntegrationPlugin[];
29
+ }>;
21
30
  export type ConnectTunnelProxyControllerBrowserOptions = Readonly<{
22
31
  connect?: TunnelConnectBrowserOptions;
23
32
  runtime?: RegisterProxyIntegrationOptions["runtime"];
@@ -30,5 +39,14 @@ export type ConnectTunnelProxyControllerBrowserHandle = Readonly<{
30
39
  runtime: ProxyRuntime;
31
40
  dispose: () => void;
32
41
  }>;
42
+ export type ConnectArtifactProxyControllerBrowserOptions = Readonly<{
43
+ connect?: ConnectBrowserOptions;
44
+ runtime?: RegisterProxyIntegrationOptions["runtime"];
45
+ allowedOrigins?: RegisterProxyControllerWindowOptions["allowedOrigins"];
46
+ targetWindow?: RegisterProxyControllerWindowOptions["targetWindow"];
47
+ expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
48
+ }>;
33
49
  export declare function connectTunnelProxyBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
50
+ export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectTunnelProxyBrowserHandle>;
34
51
  export declare function connectTunnelProxyControllerBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
52
+ export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
@@ -1,9 +1,28 @@
1
- import { connectTunnelBrowser } from "../browser/connect.js";
1
+ import { connectBrowser, connectTunnelBrowser } from "../browser/connect.js";
2
2
  import { registerProxyIntegration, } from "./integration.js";
3
3
  import { registerProxyControllerWindow } from "./controllerWindow.js";
4
+ import { extractProxyRuntimeScopeV1, resolvePresetInputFromScope, resolveRuntimeLimitsFromScope, resolveRuntimePresetLimits, } from "./runtimeScope.js";
4
5
  import { createProxyRuntime } from "./runtime.js";
5
- export async function connectTunnelProxyBrowser(grant, opts) {
6
- const client = await connectTunnelBrowser(grant, opts.connect ?? {});
6
+ function scopeRuntimeToIntegrationOptions(scope, opts) {
7
+ const runtimeLimits = resolveRuntimeLimitsFromScope(scope, opts.runtime);
8
+ const presetFromScope = resolvePresetInputFromScope(scope, opts.preset);
9
+ const presetFromLimits = presetFromScope ?? resolveRuntimePresetLimits(scope);
10
+ const serviceWorker = opts.serviceWorker ?? (scope.mode === "service_worker" ? {
11
+ scriptUrl: scope.serviceWorker.scriptUrl,
12
+ scope: scope.serviceWorker.scope,
13
+ } : undefined);
14
+ if (!serviceWorker) {
15
+ throw new Error("service worker config is required for proxy runtime mode");
16
+ }
17
+ return {
18
+ ...(presetFromLimits === undefined ? {} : { preset: presetFromLimits }),
19
+ ...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
20
+ ...(runtimeLimits === undefined ? {} : { runtime: runtimeLimits }),
21
+ serviceWorker,
22
+ ...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
23
+ };
24
+ }
25
+ async function connectProxyBrowserClient(client, opts) {
7
26
  const compat = opts;
8
27
  const integrationInput = {
9
28
  client,
@@ -48,8 +67,7 @@ export async function connectTunnelProxyBrowser(grant, opts) {
48
67
  },
49
68
  };
50
69
  }
51
- export async function connectTunnelProxyControllerBrowser(grant, opts) {
52
- const client = await connectTunnelBrowser(grant, opts.connect ?? {});
70
+ function connectProxyControllerClient(client, opts) {
53
71
  let runtime = null;
54
72
  let controller = null;
55
73
  try {
@@ -82,3 +100,28 @@ export async function connectTunnelProxyControllerBrowser(grant, opts) {
82
100
  },
83
101
  };
84
102
  }
103
+ export async function connectTunnelProxyBrowser(grant, opts) {
104
+ const client = await connectTunnelBrowser(grant, opts.connect ?? {});
105
+ return await connectProxyBrowserClient(client, opts);
106
+ }
107
+ export async function connectArtifactProxyBrowser(artifact, opts = {}) {
108
+ const scope = extractProxyRuntimeScopeV1(artifact, "service_worker");
109
+ const client = await connectBrowser(artifact, opts.connect ?? {});
110
+ const nextOpts = scopeRuntimeToIntegrationOptions(scope, opts);
111
+ return await connectProxyBrowserClient(client, nextOpts);
112
+ }
113
+ export async function connectTunnelProxyControllerBrowser(grant, opts) {
114
+ const client = await connectTunnelBrowser(grant, opts.connect ?? {});
115
+ return connectProxyControllerClient(client, opts);
116
+ }
117
+ export async function connectArtifactProxyControllerBrowser(artifact, opts = {}) {
118
+ const scope = extractProxyRuntimeScopeV1(artifact, "controller_bridge");
119
+ const client = await connectBrowser(artifact, opts.connect ?? {});
120
+ const runtime = resolveRuntimeLimitsFromScope(scope, opts.runtime);
121
+ return connectProxyControllerClient(client, {
122
+ ...(runtime === undefined ? {} : { runtime }),
123
+ allowedOrigins: opts.allowedOrigins ?? scope.controllerBridge.allowedOrigins,
124
+ ...(opts.targetWindow === undefined ? {} : { targetWindow: opts.targetWindow }),
125
+ ...(opts.expectedSource === undefined ? {} : { expectedSource: opts.expectedSource }),
126
+ });
127
+ }
@@ -9,6 +9,7 @@ export { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.j
9
9
  export * from "./integration.js";
10
10
  export * from "./controllerGuard.js";
11
11
  export * from "./bootstrap.js";
12
+ export * from "./runtimeScope.js";
12
13
  export * from "./controllerWindow.js";
13
14
  export * from "./appWindow.js";
14
15
  export * from "./wsPatch.js";
@@ -8,6 +8,7 @@ export { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.j
8
8
  export * from "./integration.js";
9
9
  export * from "./controllerGuard.js";
10
10
  export * from "./bootstrap.js";
11
+ export * from "./runtimeScope.js";
11
12
  export * from "./controllerWindow.js";
12
13
  export * from "./appWindow.js";
13
14
  export * from "./wsPatch.js";
@@ -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
+ }
@@ -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;