@floegence/flowersec-core 0.24.0 → 0.26.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 +6 -0
- package/dist/browser/controlplane.d.ts +1 -1
- package/dist/browser/controlplane.js +0 -1
- package/dist/browser/index.d.ts +3 -3
- package/dist/browser/index.js +2 -2
- package/dist/client-connect/transportSecurity.d.ts +1 -3
- package/dist/client-connect/transportSecurity.js +1 -5
- package/dist/controlplane/request.d.ts +1 -0
- package/dist/controlplane/request.js +77 -2
- package/dist/defaults.d.ts +1 -0
- package/dist/defaults.js +1 -0
- package/dist/endpoint/index.js +185 -33
- package/dist/facade.d.ts +1 -1
- package/dist/facade.js +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/proxy/appWindow.js +93 -7
- package/dist/proxy/bootstrap.d.ts +5 -24
- package/dist/proxy/bootstrap.js +1 -11
- package/dist/proxy/constants.d.ts +1 -0
- package/dist/proxy/constants.js +1 -0
- package/dist/proxy/controllerWindow.js +219 -78
- package/dist/proxy/integration.js +0 -6
- package/dist/proxy/portStream.d.ts +2 -1
- package/dist/proxy/portStream.js +173 -74
- package/dist/proxy/preset.d.ts +0 -1
- package/dist/proxy/preset.js +0 -9
- package/dist/proxy/runtimeScope.js +2 -7
- package/dist/proxy/server.d.ts +1 -0
- package/dist/proxy/server.js +466 -113
- package/dist/proxy/windowBridgeProtocol.d.ts +4 -3
- package/dist/proxy/windowBridgeProtocol.js +1 -1
- package/package.json +1 -1
- package/dist/proxy/profiles.d.ts +0 -18
- package/dist/proxy/profiles.js +0 -71
package/dist/proxy/appWindow.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServiceWorkerControllerGuard, } from "./controllerGuard.js";
|
|
2
2
|
import { createMessagePortBackedStream } from "./portStream.js";
|
|
3
|
-
import { PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE, PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE,
|
|
3
|
+
import { PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE, PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, } from "./windowBridgeProtocol.js";
|
|
4
4
|
function resolveTargetWindow(raw) {
|
|
5
5
|
const target = raw ?? globalThis.window;
|
|
6
6
|
if (target == null)
|
|
@@ -59,6 +59,8 @@ export function registerProxyAppWindow(opts) {
|
|
|
59
59
|
const targetWindow = resolveTargetWindow(opts.targetWindow);
|
|
60
60
|
const controllerWindow = resolveControllerWindow(targetWindow, opts.controllerWindow);
|
|
61
61
|
const capabilityNonce = normalizeCapabilityNonce(opts.capabilityNonce);
|
|
62
|
+
const activeWebSocketBridges = new Set();
|
|
63
|
+
let disposed = false;
|
|
62
64
|
const sw = targetWindow.navigator?.serviceWorker;
|
|
63
65
|
const onServiceWorkerMessage = (ev) => {
|
|
64
66
|
const data = ev.data;
|
|
@@ -90,15 +92,32 @@ export function registerProxyAppWindow(opts) {
|
|
|
90
92
|
: { maxWsBufferedAmountBytes: opts.maxWsBufferedAmountBytes }),
|
|
91
93
|
},
|
|
92
94
|
openWebSocketStream: async (path, wsOpts = {}) => {
|
|
95
|
+
if (disposed)
|
|
96
|
+
throw new Error("proxy app Window bridge is disposed");
|
|
93
97
|
const channel = new MessageChannel();
|
|
94
98
|
const port = channel.port1;
|
|
95
99
|
port.start?.();
|
|
96
100
|
return await new Promise((resolve, reject) => {
|
|
97
101
|
let settled = false;
|
|
102
|
+
let terminal = false;
|
|
103
|
+
let stream = null;
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
activeWebSocketBridges.delete(bridge);
|
|
106
|
+
if (wsOpts.signal != null)
|
|
107
|
+
wsOpts.signal.removeEventListener("abort", onAbort);
|
|
108
|
+
};
|
|
109
|
+
const finishTerminal = () => {
|
|
110
|
+
if (terminal)
|
|
111
|
+
return false;
|
|
112
|
+
terminal = true;
|
|
113
|
+
cleanup();
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
98
116
|
const finishReject = (error) => {
|
|
99
117
|
if (settled)
|
|
100
118
|
return;
|
|
101
119
|
settled = true;
|
|
120
|
+
finishTerminal();
|
|
102
121
|
try {
|
|
103
122
|
port.close();
|
|
104
123
|
}
|
|
@@ -107,16 +126,76 @@ export function registerProxyAppWindow(opts) {
|
|
|
107
126
|
}
|
|
108
127
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
109
128
|
};
|
|
129
|
+
const disposeBridge = (error) => {
|
|
130
|
+
if (!finishTerminal())
|
|
131
|
+
return;
|
|
132
|
+
if (stream != null) {
|
|
133
|
+
try {
|
|
134
|
+
void Promise.resolve(stream.reset(error)).catch(() => {
|
|
135
|
+
// The bridge is already terminal.
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// The bridge is already terminal.
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
port.postMessage({
|
|
145
|
+
type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
|
|
146
|
+
message: error.message,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Best-effort.
|
|
151
|
+
}
|
|
152
|
+
if (!settled) {
|
|
153
|
+
settled = true;
|
|
154
|
+
reject(error);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
port.close();
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Best-effort.
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const bridge = { dispose: disposeBridge };
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
const reason = wsOpts.signal?.reason;
|
|
166
|
+
disposeBridge(reason instanceof Error ? reason : new Error(String(reason ?? "aborted")));
|
|
167
|
+
};
|
|
168
|
+
activeWebSocketBridges.add(bridge);
|
|
110
169
|
const finishResolve = (ack) => {
|
|
111
|
-
if (settled)
|
|
170
|
+
if (settled || terminal)
|
|
112
171
|
return;
|
|
113
|
-
settled = true;
|
|
114
172
|
const capabilities = Array.isArray(ack.capabilities)
|
|
115
173
|
? ack.capabilities.filter((value) => typeof value === "string")
|
|
116
174
|
: [];
|
|
117
|
-
|
|
175
|
+
if (!capabilities.includes(PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY)) {
|
|
176
|
+
try {
|
|
177
|
+
port.postMessage({
|
|
178
|
+
type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
|
|
179
|
+
message: "proxy Window bridge capability mismatch",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// The capability error remains authoritative.
|
|
184
|
+
}
|
|
185
|
+
finishReject(new Error("proxy Window bridge does not support bidirectional stream acknowledgements"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (disposed) {
|
|
189
|
+
disposeBridge(new Error("proxy app Window bridge is disposed"));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
settled = true;
|
|
193
|
+
stream = createMessagePortBackedStream(port, {
|
|
194
|
+
maxBufferedBytes: opts.maxWsBufferedAmountBytes ?? 4 * (1 << 20),
|
|
195
|
+
onTerminal: finishTerminal,
|
|
196
|
+
});
|
|
118
197
|
resolve({
|
|
119
|
-
stream
|
|
198
|
+
stream,
|
|
120
199
|
protocol: String(ack.protocol ?? ""),
|
|
121
200
|
});
|
|
122
201
|
};
|
|
@@ -135,15 +214,16 @@ export function registerProxyAppWindow(opts) {
|
|
|
135
214
|
};
|
|
136
215
|
if (wsOpts.signal != null) {
|
|
137
216
|
if (wsOpts.signal.aborted) {
|
|
138
|
-
|
|
217
|
+
onAbort();
|
|
139
218
|
return;
|
|
140
219
|
}
|
|
141
|
-
wsOpts.signal.addEventListener("abort",
|
|
220
|
+
wsOpts.signal.addEventListener("abort", onAbort, { once: true });
|
|
142
221
|
}
|
|
143
222
|
try {
|
|
144
223
|
controllerWindow.postMessage({
|
|
145
224
|
type: PROXY_WINDOW_WS_OPEN_MSG_TYPE,
|
|
146
225
|
path,
|
|
226
|
+
capabilities: [PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY],
|
|
147
227
|
...(wsOpts.protocols === undefined ? {} : { protocols: wsOpts.protocols }),
|
|
148
228
|
...(capabilityNonce === "" ? {} : { capabilityNonce }),
|
|
149
229
|
}, controllerOrigin, [channel.port2]);
|
|
@@ -157,7 +237,13 @@ export function registerProxyAppWindow(opts) {
|
|
|
157
237
|
return {
|
|
158
238
|
runtime,
|
|
159
239
|
dispose: () => {
|
|
240
|
+
if (disposed)
|
|
241
|
+
return;
|
|
242
|
+
disposed = true;
|
|
160
243
|
sw?.removeEventListener("message", onServiceWorkerMessage);
|
|
244
|
+
const error = new Error("proxy app Window bridge is disposed");
|
|
245
|
+
for (const bridge of [...activeWebSocketBridges])
|
|
246
|
+
bridge.dispose(error);
|
|
161
247
|
},
|
|
162
248
|
};
|
|
163
249
|
}
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
2
|
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
3
|
-
import type { ConnectBrowserOptions
|
|
4
|
-
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
3
|
+
import type { ConnectBrowserOptions } from "../browser/connect.js";
|
|
5
4
|
import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
|
|
6
5
|
import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
|
|
7
6
|
import type { ProxyPresetInput } from "./preset.js";
|
|
8
7
|
import { type ProxyRuntime } from "./runtime.js";
|
|
9
|
-
export type
|
|
10
|
-
connect?: TunnelConnectBrowserOptions;
|
|
11
|
-
preset?: ProxyPresetInput;
|
|
12
|
-
runtimeGlobalKey?: string;
|
|
13
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
14
|
-
serviceWorker: ProxyIntegrationServiceWorkerOptions;
|
|
15
|
-
plugins?: readonly ProxyIntegrationPlugin[];
|
|
16
|
-
}>;
|
|
17
|
-
export type ConnectTunnelProxyBrowserHandle = Readonly<{
|
|
8
|
+
export type ConnectArtifactProxyBrowserHandle = Readonly<{
|
|
18
9
|
client: Client;
|
|
19
10
|
runtime: ProxyRuntime;
|
|
20
11
|
dispose: () => Promise<void>;
|
|
@@ -27,15 +18,7 @@ export type ConnectArtifactProxyBrowserOptions = Readonly<{
|
|
|
27
18
|
serviceWorker?: ProxyIntegrationServiceWorkerOptions;
|
|
28
19
|
plugins?: readonly ProxyIntegrationPlugin[];
|
|
29
20
|
}>;
|
|
30
|
-
export type
|
|
31
|
-
connect?: TunnelConnectBrowserOptions;
|
|
32
|
-
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
33
|
-
allowedOrigins: RegisterProxyControllerWindowOptions["allowedOrigins"];
|
|
34
|
-
targetWindow?: RegisterProxyControllerWindowOptions["targetWindow"];
|
|
35
|
-
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
36
|
-
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
37
|
-
}>;
|
|
38
|
-
export type ConnectTunnelProxyControllerBrowserHandle = Readonly<{
|
|
21
|
+
export type ConnectArtifactProxyControllerBrowserHandle = Readonly<{
|
|
39
22
|
client: Client;
|
|
40
23
|
runtime: ProxyRuntime;
|
|
41
24
|
dispose: () => void;
|
|
@@ -48,7 +31,5 @@ export type ConnectArtifactProxyControllerBrowserOptions = Readonly<{
|
|
|
48
31
|
expectedSource?: RegisterProxyControllerWindowOptions["expectedSource"];
|
|
49
32
|
capabilityNonce?: RegisterProxyControllerWindowOptions["capabilityNonce"];
|
|
50
33
|
}>;
|
|
51
|
-
export declare function
|
|
52
|
-
export declare function
|
|
53
|
-
export declare function connectTunnelProxyControllerBrowser(grant: ChannelInitGrant, opts: ConnectTunnelProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
54
|
-
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectTunnelProxyControllerBrowserHandle>;
|
|
34
|
+
export declare function connectArtifactProxyBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyBrowserOptions): Promise<ConnectArtifactProxyBrowserHandle>;
|
|
35
|
+
export declare function connectArtifactProxyControllerBrowser(artifact: ConnectArtifact, opts?: ConnectArtifactProxyControllerBrowserOptions): Promise<ConnectArtifactProxyControllerBrowserHandle>;
|
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectBrowser
|
|
1
|
+
import { connectBrowser } from "../browser/connect.js";
|
|
2
2
|
import { registerProxyIntegration, } from "./integration.js";
|
|
3
3
|
import { registerProxyControllerWindow } from "./controllerWindow.js";
|
|
4
4
|
import { extractProxyRuntimeScopeV1, resolvePresetInputFromScope, resolveRuntimeLimitsFromScope, resolveRuntimePresetLimits, } from "./runtimeScope.js";
|
|
@@ -23,12 +23,10 @@ function scopeRuntimeToIntegrationOptions(scope, opts) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
async function connectProxyBrowserClient(client, opts) {
|
|
26
|
-
const compat = opts;
|
|
27
26
|
const integrationInput = {
|
|
28
27
|
client,
|
|
29
28
|
serviceWorker: opts.serviceWorker,
|
|
30
29
|
...(opts.preset === undefined ? {} : { preset: opts.preset }),
|
|
31
|
-
...(compat.profile === undefined ? {} : { profile: compat.profile }),
|
|
32
30
|
...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
|
|
33
31
|
...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
|
|
34
32
|
...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
|
|
@@ -101,20 +99,12 @@ function connectProxyControllerClient(client, opts) {
|
|
|
101
99
|
},
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
|
-
export async function connectTunnelProxyBrowser(grant, opts) {
|
|
105
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
106
|
-
return await connectProxyBrowserClient(client, opts);
|
|
107
|
-
}
|
|
108
102
|
export async function connectArtifactProxyBrowser(artifact, opts = {}) {
|
|
109
103
|
const scope = extractProxyRuntimeScopeV1(artifact, "service_worker");
|
|
110
104
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
111
105
|
const nextOpts = scopeRuntimeToIntegrationOptions(scope, opts);
|
|
112
106
|
return await connectProxyBrowserClient(client, nextOpts);
|
|
113
107
|
}
|
|
114
|
-
export async function connectTunnelProxyControllerBrowser(grant, opts) {
|
|
115
|
-
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
116
|
-
return connectProxyControllerClient(client, opts);
|
|
117
|
-
}
|
|
118
108
|
export async function connectArtifactProxyControllerBrowser(artifact, opts = {}) {
|
|
119
109
|
const scope = extractProxyRuntimeScopeV1(artifact, "controller_bridge");
|
|
120
110
|
const client = await connectBrowser(artifact, opts.connect ?? {});
|
|
@@ -4,3 +4,4 @@ export declare const PROXY_KIND_WS: "flowersec-proxy/ws";
|
|
|
4
4
|
export declare const DEFAULT_MAX_CHUNK_BYTES: number;
|
|
5
5
|
export declare const DEFAULT_MAX_BODY_BYTES: number;
|
|
6
6
|
export declare const DEFAULT_MAX_WS_FRAME_BYTES: number;
|
|
7
|
+
export declare const DEFAULT_MAX_CONCURRENT_STREAMS: 64;
|
package/dist/proxy/constants.js
CHANGED
|
@@ -5,3 +5,4 @@ export const PROXY_KIND_WS = "flowersec-proxy/ws";
|
|
|
5
5
|
export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
|
|
6
6
|
export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
|
|
7
7
|
export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
|
|
8
|
+
export const DEFAULT_MAX_CONCURRENT_STREAMS = SDK_DEFAULTS.proxy.maxConcurrentStreams;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE,
|
|
1
|
+
import { PROXY_WINDOW_FETCH_MSG_TYPE, PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, PROXY_WINDOW_WS_ERROR_MSG_TYPE, PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY, PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE, PROXY_WINDOW_WS_OPEN_MSG_TYPE, } from "./windowBridgeProtocol.js";
|
|
2
2
|
function normalizeOrigins(origins) {
|
|
3
3
|
const out = [];
|
|
4
4
|
const seen = new Set();
|
|
@@ -56,14 +56,45 @@ function withTrustedExternalOrigin(req, rawOrigin) {
|
|
|
56
56
|
? sanitized
|
|
57
57
|
: { ...sanitized, external_origin: externalOrigin };
|
|
58
58
|
}
|
|
59
|
-
function
|
|
59
|
+
function validWriteId(value) {
|
|
60
|
+
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
61
|
+
}
|
|
62
|
+
function bridgeWebSocket(runtime, msg, port, onTerminal) {
|
|
63
|
+
const capabilities = Array.isArray(msg.capabilities)
|
|
64
|
+
? msg.capabilities.filter((value) => typeof value === "string")
|
|
65
|
+
: [];
|
|
66
|
+
if (!capabilities.includes(PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY)) {
|
|
67
|
+
try {
|
|
68
|
+
port.postMessage({
|
|
69
|
+
type: PROXY_WINDOW_WS_ERROR_MSG_TYPE,
|
|
70
|
+
message: "proxy Window bridge requires bidirectional stream acknowledgements",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best-effort.
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
port.close();
|
|
78
|
+
}
|
|
79
|
+
onTerminal();
|
|
80
|
+
return { dispose: () => { }, isTerminal: () => true };
|
|
81
|
+
}
|
|
60
82
|
const ac = new AbortController();
|
|
61
83
|
let terminal = false;
|
|
84
|
+
let terminalError = null;
|
|
62
85
|
let acceptingWrites = true;
|
|
63
86
|
let stream = null;
|
|
64
|
-
let
|
|
65
|
-
let
|
|
87
|
+
let pendingInboundWriteId = null;
|
|
88
|
+
let pendingOutboundAcknowledgement = null;
|
|
89
|
+
let nextWriteId = 1;
|
|
66
90
|
const maxBufferedBytes = runtime.limits.maxWsBufferedAmountBytes ?? 4 * (1 << 20);
|
|
91
|
+
let terminalNotified = false;
|
|
92
|
+
const notifyTerminal = () => {
|
|
93
|
+
if (terminalNotified)
|
|
94
|
+
return;
|
|
95
|
+
terminalNotified = true;
|
|
96
|
+
onTerminal();
|
|
97
|
+
};
|
|
67
98
|
const closePort = () => {
|
|
68
99
|
try {
|
|
69
100
|
port.close();
|
|
@@ -72,12 +103,16 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
72
103
|
// Best-effort.
|
|
73
104
|
}
|
|
74
105
|
};
|
|
75
|
-
const failBridge = (error) => {
|
|
106
|
+
const failBridge = (error, notifyPeer = true) => {
|
|
76
107
|
if (terminal)
|
|
77
108
|
return;
|
|
78
109
|
terminal = true;
|
|
79
110
|
acceptingWrites = false;
|
|
80
111
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
terminalError = err;
|
|
113
|
+
pendingInboundWriteId = null;
|
|
114
|
+
pendingOutboundAcknowledgement?.reject(err);
|
|
115
|
+
pendingOutboundAcknowledgement = null;
|
|
81
116
|
if (stream != null) {
|
|
82
117
|
void Promise.resolve(stream.reset(err)).catch(() => {
|
|
83
118
|
// The bridge error is already delivered through the terminal response.
|
|
@@ -89,17 +124,146 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
89
124
|
catch {
|
|
90
125
|
// Best-effort.
|
|
91
126
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
127
|
+
if (notifyPeer) {
|
|
128
|
+
try {
|
|
129
|
+
port.postMessage({
|
|
130
|
+
type: PROXY_WINDOW_STREAM_RESET_MSG_TYPE,
|
|
131
|
+
message: err.message,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Best-effort.
|
|
136
|
+
}
|
|
100
137
|
}
|
|
101
138
|
closePort();
|
|
139
|
+
notifyTerminal();
|
|
140
|
+
};
|
|
141
|
+
const failProtocol = (message) => {
|
|
142
|
+
failBridge(new Error(`proxy Window stream protocol error: ${message}`));
|
|
143
|
+
};
|
|
144
|
+
const postChunkAndWait = async (chunk) => {
|
|
145
|
+
if (chunk.byteLength > maxBufferedBytes) {
|
|
146
|
+
throw new Error("proxy WebSocket inbound chunk exceeded the buffer limit");
|
|
147
|
+
}
|
|
148
|
+
if (!Number.isSafeInteger(nextWriteId)) {
|
|
149
|
+
throw new Error("proxy Window stream write identifier space exhausted");
|
|
150
|
+
}
|
|
151
|
+
if (pendingOutboundAcknowledgement != null) {
|
|
152
|
+
throw new Error("proxy Window stream already has an unacknowledged outbound chunk");
|
|
153
|
+
}
|
|
154
|
+
const writeId = nextWriteId++;
|
|
155
|
+
const ab = cloneChunk(chunk);
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
pendingOutboundAcknowledgement = { writeId, resolve, reject };
|
|
158
|
+
try {
|
|
159
|
+
port.postMessage({
|
|
160
|
+
type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE,
|
|
161
|
+
data: ab,
|
|
162
|
+
writeId,
|
|
163
|
+
}, [ab]);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
pendingOutboundAcknowledgement = null;
|
|
167
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
port.onmessage = (ev) => {
|
|
172
|
+
const data = ev.data;
|
|
173
|
+
if (data == null || typeof data !== "object")
|
|
174
|
+
return;
|
|
175
|
+
const type = typeof data.type === "string" ? data.type : "";
|
|
176
|
+
if (stream == null) {
|
|
177
|
+
if (type === PROXY_WINDOW_STREAM_RESET_MSG_TYPE) {
|
|
178
|
+
const message = String(data.message ?? "stream reset");
|
|
179
|
+
failBridge(new Error(message), false);
|
|
180
|
+
}
|
|
181
|
+
else if (type === PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE
|
|
182
|
+
|| type === PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE
|
|
183
|
+
|| type === PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE) {
|
|
184
|
+
failProtocol("received stream traffic before the websocket opened");
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
switch (type) {
|
|
189
|
+
case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
|
|
190
|
+
if (!acceptingWrites)
|
|
191
|
+
return;
|
|
192
|
+
const chunkMessage = data;
|
|
193
|
+
const raw = chunkMessage.data;
|
|
194
|
+
if (!(raw instanceof ArrayBuffer) || !validWriteId(chunkMessage.writeId)) {
|
|
195
|
+
failProtocol("invalid stream chunk");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (pendingInboundWriteId != null) {
|
|
199
|
+
failProtocol("received more than one unacknowledged chunk");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (raw.byteLength > maxBufferedBytes) {
|
|
203
|
+
failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const writeId = chunkMessage.writeId;
|
|
207
|
+
const chunk = new Uint8Array(raw);
|
|
208
|
+
pendingInboundWriteId = writeId;
|
|
209
|
+
void stream.write(chunk)
|
|
210
|
+
.then(() => {
|
|
211
|
+
if (terminal)
|
|
212
|
+
return;
|
|
213
|
+
if (pendingInboundWriteId !== writeId) {
|
|
214
|
+
failProtocol("completed write does not match the pending chunk");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
pendingInboundWriteId = null;
|
|
218
|
+
port.postMessage({
|
|
219
|
+
type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
|
|
220
|
+
writeId,
|
|
221
|
+
});
|
|
222
|
+
})
|
|
223
|
+
.catch((error) => failBridge(error));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE: {
|
|
227
|
+
if (!acceptingWrites)
|
|
228
|
+
return;
|
|
229
|
+
if (pendingInboundWriteId != null) {
|
|
230
|
+
failProtocol("stream closed before the pending chunk was acknowledged");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
acceptingWrites = false;
|
|
234
|
+
terminal = true;
|
|
235
|
+
const closeError = new Error("stream is closed");
|
|
236
|
+
pendingInboundWriteId = null;
|
|
237
|
+
pendingOutboundAcknowledgement?.reject(closeError);
|
|
238
|
+
pendingOutboundAcknowledgement = null;
|
|
239
|
+
void Promise.resolve(stream.close()).catch(() => {
|
|
240
|
+
// The peer already closed its bridge endpoint.
|
|
241
|
+
});
|
|
242
|
+
closePort();
|
|
243
|
+
notifyTerminal();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
case PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE: {
|
|
247
|
+
const writeId = data.writeId;
|
|
248
|
+
const pending = pendingOutboundAcknowledgement;
|
|
249
|
+
if (!validWriteId(writeId) || pending == null || pending.writeId !== writeId) {
|
|
250
|
+
failProtocol("unexpected stream write acknowledgement");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
pendingOutboundAcknowledgement = null;
|
|
254
|
+
pending.resolve();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
|
|
258
|
+
const message = String(data.message ?? "stream reset");
|
|
259
|
+
failBridge(new Error(message), false);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
102
265
|
};
|
|
266
|
+
port.start?.();
|
|
103
267
|
void (async () => {
|
|
104
268
|
try {
|
|
105
269
|
const wsOpts = {
|
|
@@ -107,69 +271,15 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
107
271
|
...(msg.protocols === undefined ? {} : { protocols: msg.protocols }),
|
|
108
272
|
};
|
|
109
273
|
const opened = await runtime.openWebSocketStream(msg.path, wsOpts);
|
|
274
|
+
if (terminal) {
|
|
275
|
+
await Promise.resolve(opened.stream.reset(terminalError ?? new Error("proxy controller Window bridge is closed")));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
110
278
|
stream = opened.stream;
|
|
111
|
-
port.onmessage = (ev) => {
|
|
112
|
-
const data = ev.data;
|
|
113
|
-
if (data == null || typeof data !== "object")
|
|
114
|
-
return;
|
|
115
|
-
const type = typeof data.type === "string" ? data.type : "";
|
|
116
|
-
switch (type) {
|
|
117
|
-
case PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE: {
|
|
118
|
-
if (!acceptingWrites || stream == null)
|
|
119
|
-
return;
|
|
120
|
-
const raw = data.data;
|
|
121
|
-
if (!(raw instanceof ArrayBuffer))
|
|
122
|
-
return;
|
|
123
|
-
if (pendingWriteBytes + raw.byteLength > maxBufferedBytes) {
|
|
124
|
-
failBridge(new Error("proxy WebSocket outbound buffer exceeded"));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const rawWriteId = data.writeId;
|
|
128
|
-
const writeId = Number.isSafeInteger(rawWriteId) && Number(rawWriteId) > 0
|
|
129
|
-
? Number(rawWriteId)
|
|
130
|
-
: undefined;
|
|
131
|
-
const chunk = new Uint8Array(raw);
|
|
132
|
-
pendingWriteBytes += chunk.byteLength;
|
|
133
|
-
writeChain = writeChain
|
|
134
|
-
.then(async () => {
|
|
135
|
-
if (terminal || stream == null)
|
|
136
|
-
throw new Error("stream is closed");
|
|
137
|
-
await stream.write(chunk);
|
|
138
|
-
if (terminal || writeId === undefined)
|
|
139
|
-
return;
|
|
140
|
-
port.postMessage({
|
|
141
|
-
type: PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE,
|
|
142
|
-
writeId,
|
|
143
|
-
});
|
|
144
|
-
})
|
|
145
|
-
.catch((error) => failBridge(error))
|
|
146
|
-
.finally(() => {
|
|
147
|
-
pendingWriteBytes = Math.max(0, pendingWriteBytes - chunk.byteLength);
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE:
|
|
152
|
-
if (!acceptingWrites || stream == null)
|
|
153
|
-
return;
|
|
154
|
-
acceptingWrites = false;
|
|
155
|
-
writeChain = writeChain
|
|
156
|
-
.then(() => stream?.close())
|
|
157
|
-
.catch((error) => failBridge(error));
|
|
158
|
-
return;
|
|
159
|
-
case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
|
|
160
|
-
const message = String(data.message ?? "stream reset");
|
|
161
|
-
failBridge(new Error(message));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
default:
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
port.start?.();
|
|
169
279
|
port.postMessage({
|
|
170
280
|
type: PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE,
|
|
171
281
|
protocol: opened.protocol,
|
|
172
|
-
capabilities: [
|
|
282
|
+
capabilities: [PROXY_WINDOW_WS_BIDIRECTIONAL_ACK_CAPABILITY],
|
|
173
283
|
});
|
|
174
284
|
while (!terminal) {
|
|
175
285
|
const chunk = await stream.read();
|
|
@@ -178,22 +288,36 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
178
288
|
acceptingWrites = false;
|
|
179
289
|
port.postMessage({ type: PROXY_WINDOW_STREAM_END_MSG_TYPE });
|
|
180
290
|
closePort();
|
|
291
|
+
notifyTerminal();
|
|
181
292
|
return;
|
|
182
293
|
}
|
|
183
|
-
|
|
184
|
-
port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab }, [ab]);
|
|
294
|
+
await postChunkAndWait(chunk);
|
|
185
295
|
}
|
|
186
296
|
}
|
|
187
297
|
catch (error) {
|
|
298
|
+
if (terminal)
|
|
299
|
+
return;
|
|
188
300
|
const message = error instanceof Error ? error.message : String(error);
|
|
189
301
|
if (stream == null) {
|
|
190
|
-
|
|
191
|
-
|
|
302
|
+
terminal = true;
|
|
303
|
+
acceptingWrites = false;
|
|
304
|
+
terminalError = error instanceof Error ? error : new Error(message);
|
|
305
|
+
try {
|
|
306
|
+
port.postMessage({ type: PROXY_WINDOW_WS_ERROR_MSG_TYPE, message });
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
closePort();
|
|
310
|
+
notifyTerminal();
|
|
311
|
+
}
|
|
192
312
|
return;
|
|
193
313
|
}
|
|
194
314
|
failBridge(error);
|
|
195
315
|
}
|
|
196
316
|
})();
|
|
317
|
+
return {
|
|
318
|
+
dispose: (error) => failBridge(error),
|
|
319
|
+
isTerminal: () => terminal,
|
|
320
|
+
};
|
|
197
321
|
}
|
|
198
322
|
export function registerProxyControllerWindow(opts) {
|
|
199
323
|
const allowedOrigins = normalizeOrigins(opts.allowedOrigins);
|
|
@@ -202,6 +326,8 @@ export function registerProxyControllerWindow(opts) {
|
|
|
202
326
|
}
|
|
203
327
|
const capabilityNonce = normalizeCapabilityNonce(opts.capabilityNonce);
|
|
204
328
|
requireBridgeCapability(opts.expectedSource, capabilityNonce);
|
|
329
|
+
const activeWebSocketBridges = new Set();
|
|
330
|
+
let disposed = false;
|
|
205
331
|
const targetWindow = opts.targetWindow ?? globalThis.window;
|
|
206
332
|
if (targetWindow == null) {
|
|
207
333
|
throw new Error("targetWindow is not available");
|
|
@@ -227,9 +353,18 @@ export function registerProxyControllerWindow(opts) {
|
|
|
227
353
|
opts.runtime.dispatchFetch(withTrustedExternalOrigin(msg.req, eventOrigin), port);
|
|
228
354
|
return;
|
|
229
355
|
}
|
|
230
|
-
case PROXY_WINDOW_WS_OPEN_MSG_TYPE:
|
|
231
|
-
|
|
356
|
+
case PROXY_WINDOW_WS_OPEN_MSG_TYPE: {
|
|
357
|
+
if (disposed)
|
|
358
|
+
return;
|
|
359
|
+
let bridge = null;
|
|
360
|
+
bridge = bridgeWebSocket(opts.runtime, data, port, () => {
|
|
361
|
+
if (bridge != null)
|
|
362
|
+
activeWebSocketBridges.delete(bridge);
|
|
363
|
+
});
|
|
364
|
+
if (!bridge.isTerminal())
|
|
365
|
+
activeWebSocketBridges.add(bridge);
|
|
232
366
|
return;
|
|
367
|
+
}
|
|
233
368
|
default:
|
|
234
369
|
return;
|
|
235
370
|
}
|
|
@@ -237,7 +372,13 @@ export function registerProxyControllerWindow(opts) {
|
|
|
237
372
|
targetWindow.addEventListener("message", onMessage);
|
|
238
373
|
return {
|
|
239
374
|
dispose: () => {
|
|
375
|
+
if (disposed)
|
|
376
|
+
return;
|
|
377
|
+
disposed = true;
|
|
240
378
|
targetWindow.removeEventListener("message", onMessage);
|
|
379
|
+
const error = new Error("proxy controller Window bridge is disposed");
|
|
380
|
+
for (const bridge of [...activeWebSocketBridges])
|
|
381
|
+
bridge.dispose(error);
|
|
241
382
|
},
|
|
242
383
|
};
|
|
243
384
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { profileToPresetManifest } from "./profiles.js";
|
|
2
1
|
import { resolveProxyPreset } from "./preset.js";
|
|
3
2
|
import { registerServiceWorkerAndEnsureControl } from "./registerServiceWorker.js";
|
|
4
3
|
import { createProxyRuntime, ensureServiceWorkerRuntimeRegistered } from "./runtime.js";
|
|
@@ -165,13 +164,8 @@ function shouldIgnoreMismatch(plugins, ctx) {
|
|
|
165
164
|
return false;
|
|
166
165
|
}
|
|
167
166
|
function resolveIntegrationPreset(opts) {
|
|
168
|
-
if (opts.preset !== undefined && opts.profile !== undefined) {
|
|
169
|
-
throw new Error("preset and deprecated profile cannot be used together");
|
|
170
|
-
}
|
|
171
167
|
if (opts.preset !== undefined)
|
|
172
168
|
return resolveProxyPreset(opts.preset);
|
|
173
|
-
if (opts.profile !== undefined)
|
|
174
|
-
return resolveProxyPreset(profileToPresetManifest(opts.profile));
|
|
175
169
|
return resolveProxyPreset();
|
|
176
170
|
}
|
|
177
171
|
function buildRuntimeOptions(preset, runtime) {
|