@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.
- package/README.md +27 -12
- package/dist/browser/controlplane.d.ts +5 -30
- package/dist/browser/controlplane.js +5 -112
- package/dist/browser/reconnectConfig.d.ts +7 -8
- package/dist/browser/reconnectConfig.js +4 -29
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +1 -1
- package/dist/client-connect/tunnelAttachCloseReason.js +4 -1
- 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 +195 -0
- package/dist/e2ee/secureChannel.js +2 -5
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/node/reconnectConfig.d.ts +42 -0
- package/dist/node/reconnectConfig.js +78 -0
- package/dist/observability/observer.d.ts +1 -1
- package/dist/observability/observer.js +34 -7
- package/dist/proxy/bootstrap.d.ts +19 -1
- package/dist/proxy/bootstrap.js +48 -5
- package/dist/proxy/cookieJar.d.ts +3 -2
- package/dist/proxy/cookieJar.js +68 -20
- package/dist/proxy/index.d.ts +1 -0
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/runtime.js +2 -2
- package/dist/proxy/runtimeScope.d.ts +49 -0
- package/dist/proxy/runtimeScope.js +223 -0
- package/dist/proxy/serviceWorker.js +5 -10
- package/dist/reconnect/artifactControlplane.d.ts +13 -0
- package/dist/reconnect/artifactControlplane.js +34 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.js +3 -1
- package/package.json +7 -3
package/dist/node/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
2
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
3
|
+
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
3
4
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
2
|
+
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
3
|
+
import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
4
|
+
import type { ClientObserverLike } from "../observability/observer.js";
|
|
5
|
+
import type { DirectConnectOptions } from "../direct-client/connect.js";
|
|
6
|
+
import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
|
|
7
|
+
import type { AutoReconnectConfig, ConnectConfig as ReconnectConnectConfig } from "../reconnect/index.js";
|
|
8
|
+
import { type ArtifactAwareReconnectConfig, type ArtifactFactoryArgs } from "../reconnect/artifactControlplane.js";
|
|
9
|
+
import type { RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/index.js";
|
|
10
|
+
type SharedReconnectOptions = Readonly<{
|
|
11
|
+
observer?: ClientObserverLike;
|
|
12
|
+
autoReconnect?: AutoReconnectConfig;
|
|
13
|
+
}>;
|
|
14
|
+
type TunnelReconnectConnectOptions = Omit<TunnelConnectOptions, "observer" | "signal">;
|
|
15
|
+
type DirectReconnectConnectOptions = Omit<DirectConnectOptions, "observer" | "signal">;
|
|
16
|
+
type ArtifactAwareTunnelReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
17
|
+
artifact?: ConnectArtifact;
|
|
18
|
+
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
19
|
+
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
20
|
+
}>;
|
|
21
|
+
type ArtifactAwareDirectReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
22
|
+
artifact?: ConnectArtifact;
|
|
23
|
+
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
24
|
+
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
25
|
+
}>;
|
|
26
|
+
export type TunnelNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareTunnelReconnectConfig & Readonly<{
|
|
27
|
+
mode?: "tunnel";
|
|
28
|
+
connect?: TunnelReconnectConnectOptions;
|
|
29
|
+
grant?: ChannelInitGrant;
|
|
30
|
+
getGrant?: () => Promise<ChannelInitGrant>;
|
|
31
|
+
}>;
|
|
32
|
+
export type DirectNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareDirectReconnectConfig & Readonly<{
|
|
33
|
+
mode: "direct";
|
|
34
|
+
connect?: DirectReconnectConnectOptions;
|
|
35
|
+
directInfo?: DirectConnectInfo;
|
|
36
|
+
getDirectInfo?: () => Promise<DirectConnectInfo>;
|
|
37
|
+
}>;
|
|
38
|
+
export type NodeReconnectConfig = TunnelNodeReconnectConfig | DirectNodeReconnectConfig;
|
|
39
|
+
export declare function createTunnelNodeReconnectConfig(config: TunnelNodeReconnectConfig): ReconnectConnectConfig;
|
|
40
|
+
export declare function createDirectNodeReconnectConfig(config: DirectNodeReconnectConfig): ReconnectConnectConfig;
|
|
41
|
+
export declare function createNodeReconnectConfig(config: NodeReconnectConfig): ReconnectConnectConfig;
|
|
42
|
+
export {};
|
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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"
|
|
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
|
|
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" &&
|
|
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 {
|
|
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>;
|
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare class CookieJar {
|
|
2
2
|
private readonly cookies;
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
private nextCreatedAtSeq;
|
|
4
|
+
setCookie(setCookieHeader: string, requestPath?: string): void;
|
|
5
|
+
updateFromSetCookieHeaders(headers: readonly string[], requestPath?: string): void;
|
|
5
6
|
getCookieHeader(path: string): string;
|
|
6
7
|
}
|
package/dist/proxy/cookieJar.js
CHANGED
|
@@ -11,9 +11,54 @@ function parseCookieNameValue(s) {
|
|
|
11
11
|
return null;
|
|
12
12
|
return { name, value };
|
|
13
13
|
}
|
|
14
|
+
function cookieStorageKey(name, path) {
|
|
15
|
+
return `${name}\u0000${path}`;
|
|
16
|
+
}
|
|
17
|
+
function requestPathOnly(path) {
|
|
18
|
+
const raw = path.trim();
|
|
19
|
+
if (!raw.startsWith("/"))
|
|
20
|
+
return "/";
|
|
21
|
+
const q = raw.indexOf("?");
|
|
22
|
+
const out = q >= 0 ? raw.slice(0, q) : raw;
|
|
23
|
+
return out === "" ? "/" : out;
|
|
24
|
+
}
|
|
25
|
+
function defaultCookiePathFromRequestPath(requestPath) {
|
|
26
|
+
const path = requestPathOnly(requestPath);
|
|
27
|
+
if (path === "/")
|
|
28
|
+
return "/";
|
|
29
|
+
const lastSlash = path.lastIndexOf("/");
|
|
30
|
+
if (lastSlash <= 0)
|
|
31
|
+
return "/";
|
|
32
|
+
return path.slice(0, lastSlash);
|
|
33
|
+
}
|
|
34
|
+
function normalizeCookiePath(pathAttr, requestPath) {
|
|
35
|
+
const path = pathAttr?.trim() ?? "";
|
|
36
|
+
if (path === "" || !path.startsWith("/"))
|
|
37
|
+
return defaultCookiePathFromRequestPath(requestPath);
|
|
38
|
+
return path;
|
|
39
|
+
}
|
|
40
|
+
function pathMatchesCookiePath(requestPath, cookiePath) {
|
|
41
|
+
const path = requestPathOnly(requestPath);
|
|
42
|
+
if (cookiePath === "/")
|
|
43
|
+
return true;
|
|
44
|
+
if (path === cookiePath)
|
|
45
|
+
return true;
|
|
46
|
+
if (!path.startsWith(cookiePath))
|
|
47
|
+
return false;
|
|
48
|
+
if (cookiePath.endsWith("/"))
|
|
49
|
+
return true;
|
|
50
|
+
return path.charAt(cookiePath.length) === "/";
|
|
51
|
+
}
|
|
52
|
+
function compareCookiesForHeader(a, b) {
|
|
53
|
+
const pathLenDiff = b.path.length - a.path.length;
|
|
54
|
+
if (pathLenDiff !== 0)
|
|
55
|
+
return pathLenDiff;
|
|
56
|
+
return a.createdAtSeq - b.createdAtSeq;
|
|
57
|
+
}
|
|
14
58
|
export class CookieJar {
|
|
15
59
|
cookies = new Map();
|
|
16
|
-
|
|
60
|
+
nextCreatedAtSeq = 0;
|
|
61
|
+
setCookie(setCookieHeader, requestPath = "/") {
|
|
17
62
|
const raw = setCookieHeader.trim();
|
|
18
63
|
if (raw === "")
|
|
19
64
|
return;
|
|
@@ -23,15 +68,16 @@ export class CookieJar {
|
|
|
23
68
|
const nv = parseCookieNameValue(parts[0] ?? "");
|
|
24
69
|
if (nv == null)
|
|
25
70
|
return;
|
|
26
|
-
let
|
|
71
|
+
let pathAttr;
|
|
27
72
|
let expiresAtMs;
|
|
73
|
+
let maxAgeSeen = false;
|
|
28
74
|
for (let i = 1; i < parts.length; i++) {
|
|
29
75
|
const p = parts[i];
|
|
30
76
|
const lower = p.toLowerCase();
|
|
31
77
|
if (lower.startsWith("path=")) {
|
|
32
78
|
const v = p.slice("path=".length).trim();
|
|
33
79
|
if (v !== "")
|
|
34
|
-
|
|
80
|
+
pathAttr = v;
|
|
35
81
|
continue;
|
|
36
82
|
}
|
|
37
83
|
if (lower.startsWith("max-age=")) {
|
|
@@ -39,14 +85,13 @@ export class CookieJar {
|
|
|
39
85
|
const n = Number.parseInt(v, 10);
|
|
40
86
|
if (!Number.isFinite(n))
|
|
41
87
|
continue;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
expiresAtMs = nowMs() + n * 1000;
|
|
88
|
+
maxAgeSeen = true;
|
|
89
|
+
expiresAtMs = n <= 0 ? 0 : nowMs() + n * 1000;
|
|
47
90
|
continue;
|
|
48
91
|
}
|
|
49
92
|
if (lower.startsWith("expires=")) {
|
|
93
|
+
if (maxAgeSeen)
|
|
94
|
+
continue;
|
|
50
95
|
const v = p.slice("expires=".length).trim();
|
|
51
96
|
const t = Date.parse(v);
|
|
52
97
|
if (!Number.isFinite(t))
|
|
@@ -55,34 +100,37 @@ export class CookieJar {
|
|
|
55
100
|
continue;
|
|
56
101
|
}
|
|
57
102
|
}
|
|
58
|
-
|
|
103
|
+
const path = normalizeCookiePath(pathAttr, requestPath);
|
|
104
|
+
const key = cookieStorageKey(nv.name, path);
|
|
59
105
|
if (expiresAtMs != null && expiresAtMs <= nowMs()) {
|
|
60
|
-
this.cookies.delete(
|
|
106
|
+
this.cookies.delete(key);
|
|
61
107
|
return;
|
|
62
108
|
}
|
|
109
|
+
const createdAtSeq = this.cookies.get(key)?.createdAtSeq ?? this.nextCreatedAtSeq++;
|
|
63
110
|
if (expiresAtMs == null) {
|
|
64
|
-
this.cookies.set(
|
|
111
|
+
this.cookies.set(key, { name: nv.name, value: nv.value, path, createdAtSeq });
|
|
65
112
|
}
|
|
66
113
|
else {
|
|
67
|
-
this.cookies.set(
|
|
114
|
+
this.cookies.set(key, { name: nv.name, value: nv.value, path, expiresAtMs, createdAtSeq });
|
|
68
115
|
}
|
|
69
116
|
}
|
|
70
|
-
updateFromSetCookieHeaders(headers) {
|
|
117
|
+
updateFromSetCookieHeaders(headers, requestPath = "/") {
|
|
71
118
|
for (const h of headers)
|
|
72
|
-
this.setCookie(h);
|
|
119
|
+
this.setCookie(h, requestPath);
|
|
73
120
|
}
|
|
74
121
|
getCookieHeader(path) {
|
|
75
122
|
const now = nowMs();
|
|
76
|
-
const
|
|
77
|
-
for (const c of this.cookies.
|
|
123
|
+
const matched = [];
|
|
124
|
+
for (const [key, c] of this.cookies.entries()) {
|
|
78
125
|
if (c.expiresAtMs != null && c.expiresAtMs <= now) {
|
|
79
|
-
this.cookies.delete(
|
|
126
|
+
this.cookies.delete(key);
|
|
80
127
|
continue;
|
|
81
128
|
}
|
|
82
|
-
if (
|
|
129
|
+
if (!pathMatchesCookiePath(path, c.path))
|
|
83
130
|
continue;
|
|
84
|
-
|
|
131
|
+
matched.push(c);
|
|
85
132
|
}
|
|
86
|
-
|
|
133
|
+
matched.sort(compareCookiesForHeader);
|
|
134
|
+
return matched.map((c) => `${c.name}=${c.value}`).join("; ");
|
|
87
135
|
}
|
|
88
136
|
}
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/proxy/index.js
CHANGED
|
@@ -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";
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -102,7 +102,7 @@ export function createProxyRuntime(opts) {
|
|
|
102
102
|
ctl?.postMessage({ type: "flowersec-proxy:register-runtime" });
|
|
103
103
|
}
|
|
104
104
|
catch {
|
|
105
|
-
// Best-effort:
|
|
105
|
+
// Best-effort: controllerchange will retry once the active Service Worker is ready.
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
const onMessage = (ev) => {
|
|
@@ -168,7 +168,7 @@ export function createProxyRuntime(opts) {
|
|
|
168
168
|
const status = Math.max(0, Math.floor(respMeta.status ?? 502));
|
|
169
169
|
const rawHeaders = Array.isArray(respMeta.headers) ? respMeta.headers : [];
|
|
170
170
|
const { passthrough, setCookie } = filterResponseHeaders(rawHeaders, { extraAllowed: extraResponseHeaders });
|
|
171
|
-
cookieJar.updateFromSetCookieHeaders(setCookie);
|
|
171
|
+
cookieJar.updateFromSetCookieHeaders(setCookie, path);
|
|
172
172
|
port.postMessage({ type: "flowersec-proxy:response_meta", status, headers: passthrough });
|
|
173
173
|
const chunks = await readChunkFrames(reader, maxChunkBytes, maxBodyBytes);
|
|
174
174
|
for await (const chunk of chunks) {
|
|
@@ -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 {};
|