@tutti-os/browser-node 0.0.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/LICENSE +202 -0
- package/README.md +21 -0
- package/dist/assets/workspace-dock-website.d.ts +3 -0
- package/dist/assets/workspace-dock-website.png +0 -0
- package/dist/bridge/index.d.ts +15 -0
- package/dist/bridge/index.js +9 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/chunk-2GEY55PS.js +1179 -0
- package/dist/chunk-2GEY55PS.js.map +1 -0
- package/dist/chunk-IO5AJ2R5.js +111 -0
- package/dist/chunk-IO5AJ2R5.js.map +1 -0
- package/dist/chunk-IS2USG4D.js +93 -0
- package/dist/chunk-IS2USG4D.js.map +1 -0
- package/dist/chunk-LVVPDNEF.js +93 -0
- package/dist/chunk-LVVPDNEF.js.map +1 -0
- package/dist/chunk-UTXZLRPE.js +34 -0
- package/dist/chunk-UTXZLRPE.js.map +1 -0
- package/dist/electron-main/index.d.ts +158 -0
- package/dist/electron-main/index.js +1492 -0
- package/dist/electron-main/index.js.map +1 -0
- package/dist/electron-preload/index.d.ts +33 -0
- package/dist/electron-preload/index.js +140 -0
- package/dist/electron-preload/index.js.map +1 -0
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +13 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +165 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +41 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-Bmzz1Q84.d.ts +32 -0
- package/dist/workbench/index.d.ts +55 -0
- package/dist/workbench/index.js +194 -0
- package/dist/workbench/index.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isBrowserSessionPartitionAllowed,
|
|
3
|
+
resolveBrowserSessionPartition
|
|
4
|
+
} from "../chunk-UTXZLRPE.js";
|
|
5
|
+
import {
|
|
6
|
+
normalizeHostBrowserComparableUrl,
|
|
7
|
+
resolveBrowserNavigationUrl,
|
|
8
|
+
resolveHostBrowserNavigationUrl
|
|
9
|
+
} from "../chunk-LVVPDNEF.js";
|
|
10
|
+
|
|
11
|
+
// src/electron-main/loopbackPreviewProxy.ts
|
|
12
|
+
import {
|
|
13
|
+
createServer,
|
|
14
|
+
request as httpRequest
|
|
15
|
+
} from "http";
|
|
16
|
+
import { request as httpsRequest } from "https";
|
|
17
|
+
import { pipeline } from "stream";
|
|
18
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
19
|
+
var defaultLoopbackPreviewCacheTtlMs = 3e4;
|
|
20
|
+
var loopbackHostPattern = /^(localhost|127(?:\.\d{1,3}){0,3})$/i;
|
|
21
|
+
var loopbackPreviewProxyBypassRules = "*;<-loopback>;https://*;wss://*";
|
|
22
|
+
var hopByHopHeaders = /* @__PURE__ */ new Set([
|
|
23
|
+
"connection",
|
|
24
|
+
"keep-alive",
|
|
25
|
+
"proxy-authenticate",
|
|
26
|
+
"proxy-authorization",
|
|
27
|
+
"proxy-connection",
|
|
28
|
+
"te",
|
|
29
|
+
"trailer",
|
|
30
|
+
"transfer-encoding",
|
|
31
|
+
"upgrade"
|
|
32
|
+
]);
|
|
33
|
+
function createBrowserNodeLoopbackPreviewProxy({
|
|
34
|
+
logger,
|
|
35
|
+
resolveSession,
|
|
36
|
+
routing
|
|
37
|
+
}) {
|
|
38
|
+
const configuredSessions = /* @__PURE__ */ new WeakSet();
|
|
39
|
+
const targetCache = /* @__PURE__ */ new Map();
|
|
40
|
+
const downstreamWebSocketServer = new WebSocketServer({ noServer: true });
|
|
41
|
+
let server = null;
|
|
42
|
+
let serverStartPromise = null;
|
|
43
|
+
const cacheTtlMs = routing.cacheTtlMs ?? defaultLoopbackPreviewCacheTtlMs;
|
|
44
|
+
const fallback = routing.fallback ?? "direct";
|
|
45
|
+
const pruneExpiredTargets = (now) => {
|
|
46
|
+
for (const [cacheKey, cachedTarget] of targetCache.entries()) {
|
|
47
|
+
if (cachedTarget.expiresAt <= now) {
|
|
48
|
+
targetCache.delete(cacheKey);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const start = async () => {
|
|
53
|
+
if (server?.listening) {
|
|
54
|
+
return addressForTesting();
|
|
55
|
+
}
|
|
56
|
+
if (serverStartPromise) {
|
|
57
|
+
return serverStartPromise;
|
|
58
|
+
}
|
|
59
|
+
serverStartPromise = new Promise((resolve, reject) => {
|
|
60
|
+
const nextServer = createServer((request, response) => {
|
|
61
|
+
void handleRequest(request, response);
|
|
62
|
+
});
|
|
63
|
+
nextServer.on("upgrade", (request, socket, head) => {
|
|
64
|
+
void handleUpgrade(request, socket, head);
|
|
65
|
+
});
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
nextServer.removeListener("error", onError);
|
|
68
|
+
nextServer.removeListener("listening", onListening);
|
|
69
|
+
};
|
|
70
|
+
const onError = (error) => {
|
|
71
|
+
cleanup();
|
|
72
|
+
reject(error);
|
|
73
|
+
};
|
|
74
|
+
const onListening = () => {
|
|
75
|
+
cleanup();
|
|
76
|
+
server = nextServer;
|
|
77
|
+
resolve(addressForTesting());
|
|
78
|
+
};
|
|
79
|
+
nextServer.once("error", onError);
|
|
80
|
+
nextServer.once("listening", onListening);
|
|
81
|
+
nextServer.listen(0, "127.0.0.1");
|
|
82
|
+
}).finally(() => {
|
|
83
|
+
serverStartPromise = null;
|
|
84
|
+
});
|
|
85
|
+
return serverStartPromise;
|
|
86
|
+
};
|
|
87
|
+
const resolveLoopbackTarget = async (originalUrl) => {
|
|
88
|
+
const port = Number(originalUrl.port);
|
|
89
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
pruneExpiredTargets(now);
|
|
94
|
+
const cacheKey = originalUrl.toString();
|
|
95
|
+
const cached = targetCache.get(cacheKey);
|
|
96
|
+
if (cached && cached.expiresAt > now) {
|
|
97
|
+
return cached.target;
|
|
98
|
+
}
|
|
99
|
+
const nextTarget = await Promise.resolve(
|
|
100
|
+
routing.resolver.resolveTarget({
|
|
101
|
+
port,
|
|
102
|
+
url: originalUrl.toString()
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
const target = normalizeLoopbackPreviewTarget(nextTarget);
|
|
106
|
+
targetCache.set(cacheKey, {
|
|
107
|
+
expiresAt: now + Math.max(0, cacheTtlMs),
|
|
108
|
+
target
|
|
109
|
+
});
|
|
110
|
+
return target;
|
|
111
|
+
};
|
|
112
|
+
const configureSession = async (input) => {
|
|
113
|
+
const nextSession = await resolveSession(input);
|
|
114
|
+
if (configuredSessions.has(nextSession)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const address = await start();
|
|
118
|
+
await nextSession.setProxy({
|
|
119
|
+
mode: "fixed_servers",
|
|
120
|
+
proxyBypassRules: loopbackPreviewProxyBypassRules,
|
|
121
|
+
proxyRules: `http=${address}`
|
|
122
|
+
});
|
|
123
|
+
configuredSessions.add(nextSession);
|
|
124
|
+
};
|
|
125
|
+
const dispose = async () => {
|
|
126
|
+
targetCache.clear();
|
|
127
|
+
downstreamWebSocketServer.close();
|
|
128
|
+
const activeServer = server;
|
|
129
|
+
server = null;
|
|
130
|
+
if (!activeServer || !activeServer.listening) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await new Promise((resolve) => {
|
|
134
|
+
activeServer.close(() => resolve());
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
const handleRequest = async (request, response) => {
|
|
138
|
+
const originalUrl = resolveProxyRequestUrl(request);
|
|
139
|
+
if (!originalUrl) {
|
|
140
|
+
response.writeHead(400);
|
|
141
|
+
response.end("invalid proxy request");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (originalUrl.protocol !== "http:") {
|
|
145
|
+
response.writeHead(400);
|
|
146
|
+
response.end("unsupported proxy request");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const targetContext = await resolveTargetRequestContext(originalUrl);
|
|
150
|
+
if (!targetContext.targetUrl) {
|
|
151
|
+
response.writeHead(502);
|
|
152
|
+
response.end("unable to resolve loopback preview target");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const headers = filterProxyHeaders(request.headers);
|
|
156
|
+
headers.set("host", targetContext.targetUrl.host);
|
|
157
|
+
const forward = targetContext.targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
158
|
+
const upstream = forward(
|
|
159
|
+
targetContext.targetUrl,
|
|
160
|
+
{
|
|
161
|
+
headers: Object.fromEntries(headers.entries()),
|
|
162
|
+
method: request.method
|
|
163
|
+
},
|
|
164
|
+
(upstreamResponse) => {
|
|
165
|
+
const responseHeaders = filterProxyHeaders(upstreamResponse.headers);
|
|
166
|
+
const location = responseHeaders.get("location");
|
|
167
|
+
if (location && targetContext.loopbackTarget) {
|
|
168
|
+
const rewritten = rewriteLoopbackLocation({
|
|
169
|
+
location,
|
|
170
|
+
originalUrl,
|
|
171
|
+
targetUrl: targetContext.loopbackTarget.targetUrl
|
|
172
|
+
});
|
|
173
|
+
if (rewritten) {
|
|
174
|
+
responseHeaders.set("location", rewritten);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
response.writeHead(
|
|
178
|
+
upstreamResponse.statusCode ?? 502,
|
|
179
|
+
upstreamResponse.statusMessage,
|
|
180
|
+
Object.fromEntries(responseHeaders.entries())
|
|
181
|
+
);
|
|
182
|
+
pipeline(upstreamResponse, response, () => void 0);
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
upstream.on("error", (error) => {
|
|
186
|
+
logger?.warn?.("Browser Node loopback preview request failed", {
|
|
187
|
+
error: normalizeProxyError(error),
|
|
188
|
+
originalUrl: originalUrl.toString(),
|
|
189
|
+
targetUrl: targetContext.targetUrl?.toString() ?? null,
|
|
190
|
+
workspaceId: targetContext.loopbackTarget?.workspaceId ?? null
|
|
191
|
+
});
|
|
192
|
+
if (!response.headersSent) {
|
|
193
|
+
response.writeHead(502);
|
|
194
|
+
}
|
|
195
|
+
response.end(
|
|
196
|
+
`loopback preview upstream request failed: ${normalizeProxyError(error)}`
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
pipeline(request, upstream, () => void 0);
|
|
200
|
+
};
|
|
201
|
+
const handleUpgrade = async (request, socket, head) => {
|
|
202
|
+
const originalUrl = resolveProxyRequestUrl(request, "ws:");
|
|
203
|
+
if (!originalUrl) {
|
|
204
|
+
socket.destroy();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (originalUrl.protocol !== "ws:") {
|
|
208
|
+
socket.destroy();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const targetContext = await resolveTargetRequestContext(originalUrl);
|
|
212
|
+
const targetUrl = targetContext.targetUrl;
|
|
213
|
+
if (!targetUrl) {
|
|
214
|
+
socket.destroy();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
downstreamWebSocketServer.handleUpgrade(
|
|
218
|
+
request,
|
|
219
|
+
socket,
|
|
220
|
+
head,
|
|
221
|
+
(downstream) => {
|
|
222
|
+
const headers = Object.fromEntries(
|
|
223
|
+
filterProxyHeaders(request.headers).entries()
|
|
224
|
+
);
|
|
225
|
+
headers.host = targetUrl.host;
|
|
226
|
+
const upstream = new WebSocket(targetUrl, { headers });
|
|
227
|
+
const pendingMessages = [];
|
|
228
|
+
downstream.on("message", (data, isBinary) => {
|
|
229
|
+
if (upstream.readyState === WebSocket.OPEN) {
|
|
230
|
+
upstream.send(data, { binary: isBinary });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
pendingMessages.push({ data, isBinary });
|
|
234
|
+
});
|
|
235
|
+
upstream.once("open", () => {
|
|
236
|
+
for (const nextMessage of pendingMessages.splice(0)) {
|
|
237
|
+
upstream.send(nextMessage.data, { binary: nextMessage.isBinary });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
upstream.on("message", (data, isBinary) => {
|
|
241
|
+
if (downstream.readyState === WebSocket.OPEN) {
|
|
242
|
+
downstream.send(data, { binary: isBinary });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
upstream.once("close", (code, reason) => {
|
|
246
|
+
if (downstream.readyState === WebSocket.OPEN || downstream.readyState === WebSocket.CONNECTING) {
|
|
247
|
+
downstream.close(normalizeWebSocketCloseCode(code), reason);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
downstream.once("close", (code, reason) => {
|
|
251
|
+
if (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING) {
|
|
252
|
+
upstream.close(normalizeWebSocketCloseCode(code), reason);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
upstream.once("error", (error) => {
|
|
256
|
+
logger?.warn?.("Browser Node loopback preview websocket failed", {
|
|
257
|
+
error: normalizeProxyError(error),
|
|
258
|
+
originalUrl: originalUrl.toString(),
|
|
259
|
+
targetUrl: targetContext.targetUrl?.toString() ?? null,
|
|
260
|
+
workspaceId: targetContext.loopbackTarget?.workspaceId ?? null
|
|
261
|
+
});
|
|
262
|
+
downstream.close(1011, "loopback preview websocket upstream failed");
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
const resolveTargetRequestContext = async (originalUrl) => {
|
|
268
|
+
if (!isLoopbackUrl(originalUrl)) {
|
|
269
|
+
return {
|
|
270
|
+
loopbackTarget: null,
|
|
271
|
+
targetUrl: null
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const loopbackTarget = await resolveLoopbackTarget(originalUrl);
|
|
275
|
+
if (loopbackTarget) {
|
|
276
|
+
return {
|
|
277
|
+
loopbackTarget,
|
|
278
|
+
targetUrl: buildTargetRequestUrl(loopbackTarget.targetUrl, originalUrl)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (fallback === "direct") {
|
|
282
|
+
return {
|
|
283
|
+
loopbackTarget: null,
|
|
284
|
+
targetUrl: cloneUrl(originalUrl)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
loopbackTarget: null,
|
|
289
|
+
targetUrl: null
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const addressForTesting = () => {
|
|
293
|
+
const address = server?.address();
|
|
294
|
+
if (!address || typeof address === "string") {
|
|
295
|
+
throw new Error("Browser Node loopback preview proxy is not listening");
|
|
296
|
+
}
|
|
297
|
+
return `127.0.0.1:${address.port}`;
|
|
298
|
+
};
|
|
299
|
+
const serverForTesting = () => {
|
|
300
|
+
if (!server) {
|
|
301
|
+
throw new Error("Browser Node loopback preview proxy is not started");
|
|
302
|
+
}
|
|
303
|
+
return server;
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
addressForTesting,
|
|
307
|
+
configureSession,
|
|
308
|
+
dispose,
|
|
309
|
+
serverForTesting
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function normalizeLoopbackPreviewTarget(input) {
|
|
313
|
+
if (!input) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const normalizedTargetUrl = input.targetUrl.trim();
|
|
317
|
+
if (normalizedTargetUrl.length === 0) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const parsed = new URL(normalizedTargetUrl);
|
|
322
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
targetUrl: parsed.toString(),
|
|
327
|
+
workspaceId: input.workspaceId
|
|
328
|
+
};
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function resolveProxyRequestUrl(request, defaultProtocol = "http:") {
|
|
334
|
+
const rawUrl = request.url ?? "";
|
|
335
|
+
try {
|
|
336
|
+
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(rawUrl)) {
|
|
337
|
+
return new URL(rawUrl);
|
|
338
|
+
}
|
|
339
|
+
const host = request.headers.host;
|
|
340
|
+
if (typeof host !== "string" || host.trim().length === 0) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const path = rawUrl.startsWith("/") ? rawUrl : `/${rawUrl}`;
|
|
344
|
+
return new URL(`${defaultProtocol}//${host}${path}`);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function filterProxyHeaders(input) {
|
|
350
|
+
const output = new Headers();
|
|
351
|
+
const appendValue = (key, value) => {
|
|
352
|
+
if (hopByHopHeaders.has(key.toLowerCase())) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
output.append(key, value);
|
|
356
|
+
};
|
|
357
|
+
if (input instanceof Headers) {
|
|
358
|
+
input.forEach((value, key) => {
|
|
359
|
+
appendValue(key, value);
|
|
360
|
+
});
|
|
361
|
+
return output;
|
|
362
|
+
}
|
|
363
|
+
for (const [key, value] of Object.entries(input)) {
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
for (const nextValue of value) {
|
|
366
|
+
appendValue(key, nextValue);
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (typeof value === "string") {
|
|
371
|
+
appendValue(key, value);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return output;
|
|
375
|
+
}
|
|
376
|
+
function isLoopbackUrl(url) {
|
|
377
|
+
return (url.protocol === "http:" || url.protocol === "ws:") && loopbackHostPattern.test(url.hostname) && url.port.length > 0;
|
|
378
|
+
}
|
|
379
|
+
function buildTargetRequestUrl(targetUrl, originalUrl) {
|
|
380
|
+
try {
|
|
381
|
+
const base = new URL(targetUrl);
|
|
382
|
+
const normalizedBasePath = normalizeBasePath(base.pathname);
|
|
383
|
+
const nextUrl = new URL(base.toString());
|
|
384
|
+
nextUrl.pathname = joinBasePath(normalizedBasePath, originalUrl.pathname);
|
|
385
|
+
nextUrl.search = originalUrl.search;
|
|
386
|
+
nextUrl.hash = "";
|
|
387
|
+
if (originalUrl.protocol === "ws:") {
|
|
388
|
+
nextUrl.protocol = nextUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
389
|
+
}
|
|
390
|
+
return nextUrl;
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function rewriteLoopbackLocation({
|
|
396
|
+
location,
|
|
397
|
+
originalUrl,
|
|
398
|
+
targetUrl
|
|
399
|
+
}) {
|
|
400
|
+
try {
|
|
401
|
+
const targetBase = new URL(targetUrl);
|
|
402
|
+
const resolvedLocation = new URL(location, targetBase);
|
|
403
|
+
if (resolvedLocation.origin !== targetBase.origin) {
|
|
404
|
+
return location;
|
|
405
|
+
}
|
|
406
|
+
const basePath = normalizeBasePath(targetBase.pathname);
|
|
407
|
+
const strippedPath = stripBasePath(basePath, resolvedLocation.pathname) ?? resolvedLocation.pathname;
|
|
408
|
+
const loopbackUrl = new URL(originalUrl.origin);
|
|
409
|
+
resolvedLocation.protocol = loopbackUrl.protocol;
|
|
410
|
+
resolvedLocation.host = loopbackUrl.host;
|
|
411
|
+
resolvedLocation.pathname = strippedPath;
|
|
412
|
+
resolvedLocation.username = "";
|
|
413
|
+
resolvedLocation.password = "";
|
|
414
|
+
return resolvedLocation.toString();
|
|
415
|
+
} catch {
|
|
416
|
+
return location;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function normalizeBasePath(pathname) {
|
|
420
|
+
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
421
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
422
|
+
}
|
|
423
|
+
function joinBasePath(basePath, requestPath) {
|
|
424
|
+
const normalizedRequestPath = requestPath === "/" ? "" : requestPath.replace(/^\/+/, "");
|
|
425
|
+
return normalizedRequestPath.length > 0 ? `${basePath}${normalizedRequestPath}`.replace(/\/{2,}/g, "/") : basePath;
|
|
426
|
+
}
|
|
427
|
+
function stripBasePath(basePath, pathname) {
|
|
428
|
+
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
429
|
+
if (normalizedPath === basePath.slice(0, -1)) {
|
|
430
|
+
return "/";
|
|
431
|
+
}
|
|
432
|
+
if (!normalizedPath.startsWith(basePath)) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const suffix = normalizedPath.slice(basePath.length);
|
|
436
|
+
return suffix.length > 0 ? `/${suffix}` : "/";
|
|
437
|
+
}
|
|
438
|
+
function cloneUrl(url) {
|
|
439
|
+
try {
|
|
440
|
+
return new URL(url.toString());
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function normalizeProxyError(error) {
|
|
446
|
+
if (error instanceof Error) {
|
|
447
|
+
const code = typeof error.code === "string" ? ` ${error.code}` : "";
|
|
448
|
+
return `${error.message}${code}`;
|
|
449
|
+
}
|
|
450
|
+
return String(error);
|
|
451
|
+
}
|
|
452
|
+
function normalizeWebSocketCloseCode(code) {
|
|
453
|
+
if (code === 1e3 || code >= 3e3 && code <= 4999) {
|
|
454
|
+
return code;
|
|
455
|
+
}
|
|
456
|
+
return 1e3;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/electron-main/guestManager.ts
|
|
460
|
+
var browserPreviewMaxWidth = 260;
|
|
461
|
+
var browserPreviewMaxHeight = 170;
|
|
462
|
+
var abortedNavigationErrorCode = -3;
|
|
463
|
+
function canGuestGoBack(contents) {
|
|
464
|
+
return contents.navigationHistory?.canGoBack() ?? contents.canGoBack();
|
|
465
|
+
}
|
|
466
|
+
function canGuestGoForward(contents) {
|
|
467
|
+
return contents.navigationHistory?.canGoForward() ?? contents.canGoForward();
|
|
468
|
+
}
|
|
469
|
+
function goGuestBack(contents) {
|
|
470
|
+
if (contents.navigationHistory) {
|
|
471
|
+
contents.navigationHistory.goBack();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
contents.goBack();
|
|
475
|
+
}
|
|
476
|
+
function goGuestForward(contents) {
|
|
477
|
+
if (contents.navigationHistory) {
|
|
478
|
+
contents.navigationHistory.goForward();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
contents.goForward();
|
|
482
|
+
}
|
|
483
|
+
function getPopupLogMetadata(url) {
|
|
484
|
+
try {
|
|
485
|
+
const parsed = new URL(url);
|
|
486
|
+
return {
|
|
487
|
+
popupOrigin: parsed.origin,
|
|
488
|
+
popupPath: parsed.pathname,
|
|
489
|
+
popupProtocol: parsed.protocol
|
|
490
|
+
};
|
|
491
|
+
} catch {
|
|
492
|
+
return {
|
|
493
|
+
popupOrigin: null,
|
|
494
|
+
popupPath: null,
|
|
495
|
+
popupProtocol: null
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function allowBrowserNodeGuestPopupWindow({
|
|
500
|
+
logger,
|
|
501
|
+
nodeId,
|
|
502
|
+
url,
|
|
503
|
+
webContentsId
|
|
504
|
+
}) {
|
|
505
|
+
logger?.info?.("Browser Node guest popup allowed", {
|
|
506
|
+
nodeId,
|
|
507
|
+
webContentsId,
|
|
508
|
+
...getPopupLogMetadata(url)
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
action: "allow",
|
|
512
|
+
overrideBrowserWindowOptions: {
|
|
513
|
+
height: 720,
|
|
514
|
+
show: true,
|
|
515
|
+
width: 520
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function resolveBrowserNodeUrlError(resolved) {
|
|
520
|
+
if (resolved.errorCode === "invalid-url") {
|
|
521
|
+
return { code: "invalid-url" };
|
|
522
|
+
}
|
|
523
|
+
if (resolved.errorCode === "unsupported-protocol") {
|
|
524
|
+
return {
|
|
525
|
+
code: "unsupported-protocol",
|
|
526
|
+
params: resolved.errorParams
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
return { code: "unsupported-url" };
|
|
530
|
+
}
|
|
531
|
+
function resizeBrowserPreviewImage(image) {
|
|
532
|
+
if (image.isEmpty?.() === true || !image.resize || !image.getSize) {
|
|
533
|
+
return image;
|
|
534
|
+
}
|
|
535
|
+
const size = image.getSize();
|
|
536
|
+
if (size.width <= 0 || size.height <= 0) {
|
|
537
|
+
return image;
|
|
538
|
+
}
|
|
539
|
+
const scale = Math.min(
|
|
540
|
+
1,
|
|
541
|
+
browserPreviewMaxWidth / size.width,
|
|
542
|
+
browserPreviewMaxHeight / size.height
|
|
543
|
+
);
|
|
544
|
+
if (scale >= 1) {
|
|
545
|
+
return image;
|
|
546
|
+
}
|
|
547
|
+
return image.resize({
|
|
548
|
+
height: Math.max(1, Math.round(size.height * scale)),
|
|
549
|
+
quality: "good",
|
|
550
|
+
width: Math.max(1, Math.round(size.width * scale))
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
function isAbortedNavigationError(input) {
|
|
554
|
+
return input.errorCode === abortedNavigationErrorCode || input.errorDescription === "ERR_ABORTED";
|
|
555
|
+
}
|
|
556
|
+
function isHttpErrorStatusCode(statusCode) {
|
|
557
|
+
return statusCode !== void 0 && statusCode >= 400;
|
|
558
|
+
}
|
|
559
|
+
function emitBrowserNavigationFailed(input) {
|
|
560
|
+
input.emit({
|
|
561
|
+
code: "navigation-failed",
|
|
562
|
+
diagnosticMessage: input.errorDescription,
|
|
563
|
+
nodeId: input.nodeId,
|
|
564
|
+
params: input.errorCode === void 0 ? void 0 : { errorCode: input.errorCode },
|
|
565
|
+
type: "error"
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function resolveBrowserNavigationOrigin(url) {
|
|
569
|
+
const resolved = resolveBrowserNavigationUrl(url);
|
|
570
|
+
if (!resolved.url) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
return new URL(resolved.url).origin;
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function isBrowserNavigationAllowedByPolicy(input) {
|
|
580
|
+
if (!input.policy) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
if (input.policy.mode === "same-origin") {
|
|
584
|
+
const policyOrigin = resolveBrowserNavigationOrigin(input.policy.originUrl);
|
|
585
|
+
const nextOrigin = resolveBrowserNavigationOrigin(input.url);
|
|
586
|
+
return policyOrigin !== null && nextOrigin !== null && policyOrigin === nextOrigin;
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
async function applyPreferredColorSchemeToGuest(session, logger, syncPreferredColorScheme, scheme) {
|
|
591
|
+
const contents = session.contents;
|
|
592
|
+
if (!contents || contents.isDestroyed() || !syncPreferredColorScheme || scheme === null || session.appliedColorScheme === scheme) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
await syncPreferredColorScheme(contents, scheme);
|
|
597
|
+
session.appliedColorScheme = scheme;
|
|
598
|
+
} catch (error) {
|
|
599
|
+
session.appliedColorScheme = null;
|
|
600
|
+
logger?.warn?.("Browser Node failed to sync guest color scheme", {
|
|
601
|
+
error: error instanceof Error ? error.message : String(error),
|
|
602
|
+
nodeId: session.nodeId,
|
|
603
|
+
scheme,
|
|
604
|
+
webContentsId: session.webContentsId
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function createBrowserGuestManager({
|
|
609
|
+
emit,
|
|
610
|
+
getPreferredColorScheme,
|
|
611
|
+
logger,
|
|
612
|
+
openExternal,
|
|
613
|
+
prepareSession,
|
|
614
|
+
resolveWebContents,
|
|
615
|
+
syncPreferredColorScheme,
|
|
616
|
+
subscribePreferredColorScheme
|
|
617
|
+
}) {
|
|
618
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
619
|
+
const nodeIdByWebContentsId = /* @__PURE__ */ new Map();
|
|
620
|
+
let preferredColorScheme = getPreferredColorScheme?.() ?? null;
|
|
621
|
+
const getSession = (nodeId, input) => {
|
|
622
|
+
const existing = sessions.get(nodeId);
|
|
623
|
+
if (existing) {
|
|
624
|
+
if (input?.profileId !== void 0) {
|
|
625
|
+
existing.profileId = input.profileId;
|
|
626
|
+
}
|
|
627
|
+
if (input?.sessionMode !== void 0) {
|
|
628
|
+
existing.sessionMode = input.sessionMode;
|
|
629
|
+
}
|
|
630
|
+
if (input?.sessionPartition !== void 0) {
|
|
631
|
+
existing.sessionPartition = input.sessionPartition;
|
|
632
|
+
}
|
|
633
|
+
if (input?.navigationPolicy !== void 0) {
|
|
634
|
+
existing.navigationPolicy = input.navigationPolicy;
|
|
635
|
+
}
|
|
636
|
+
if (input?.url !== void 0) {
|
|
637
|
+
existing.desiredUrl = input.url;
|
|
638
|
+
}
|
|
639
|
+
return existing;
|
|
640
|
+
}
|
|
641
|
+
const session = {
|
|
642
|
+
appliedColorScheme: null,
|
|
643
|
+
contents: null,
|
|
644
|
+
desiredUrl: input?.url ?? "about:blank",
|
|
645
|
+
lifecycle: "cold",
|
|
646
|
+
listeners: [],
|
|
647
|
+
navigationFailureSequence: 0,
|
|
648
|
+
navigationPolicy: input?.navigationPolicy ?? null,
|
|
649
|
+
nodeId,
|
|
650
|
+
profileId: input?.profileId ?? null,
|
|
651
|
+
sessionMode: input?.sessionMode ?? "shared",
|
|
652
|
+
sessionPartition: input?.sessionPartition ?? null,
|
|
653
|
+
webContentsId: null
|
|
654
|
+
};
|
|
655
|
+
sessions.set(nodeId, session);
|
|
656
|
+
return session;
|
|
657
|
+
};
|
|
658
|
+
const publishState = (session) => {
|
|
659
|
+
const contents = session.contents && !session.contents.isDestroyed() ? session.contents : null;
|
|
660
|
+
emit({
|
|
661
|
+
canGoBack: contents ? canGuestGoBack(contents) : false,
|
|
662
|
+
canGoForward: contents ? canGuestGoForward(contents) : false,
|
|
663
|
+
isAttachedToWindow: Boolean(contents),
|
|
664
|
+
isLoading: contents ? contents.isLoading() : false,
|
|
665
|
+
isOccluded: session.lifecycle === "cold",
|
|
666
|
+
lifecycle: session.lifecycle,
|
|
667
|
+
nodeId: session.nodeId,
|
|
668
|
+
title: contents ? contents.getTitle() || null : null,
|
|
669
|
+
type: "state",
|
|
670
|
+
url: contents ? contents.getURL() || session.desiredUrl : session.desiredUrl
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
const detachGuest = (session) => {
|
|
674
|
+
const contents = session.contents;
|
|
675
|
+
const webContentsId = session.webContentsId;
|
|
676
|
+
if (contents) {
|
|
677
|
+
for (const record of session.listeners) {
|
|
678
|
+
contents.off(record.event, record.listener);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
session.listeners = [];
|
|
682
|
+
session.contents = null;
|
|
683
|
+
session.appliedColorScheme = null;
|
|
684
|
+
session.webContentsId = null;
|
|
685
|
+
if (webContentsId !== null && nodeIdByWebContentsId.get(webContentsId) === session.nodeId) {
|
|
686
|
+
nodeIdByWebContentsId.delete(webContentsId);
|
|
687
|
+
}
|
|
688
|
+
publishState(session);
|
|
689
|
+
};
|
|
690
|
+
const handlePreferredColorSchemeChange = (scheme) => {
|
|
691
|
+
preferredColorScheme = scheme;
|
|
692
|
+
for (const session of sessions.values()) {
|
|
693
|
+
session.appliedColorScheme = null;
|
|
694
|
+
void applyPreferredColorSchemeToGuest(
|
|
695
|
+
session,
|
|
696
|
+
logger,
|
|
697
|
+
syncPreferredColorScheme,
|
|
698
|
+
scheme
|
|
699
|
+
).catch(() => void 0);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const unsubscribePreferredColorScheme = subscribePreferredColorScheme?.(handlePreferredColorSchemeChange) ?? null;
|
|
703
|
+
const attachGuestListeners = (session) => {
|
|
704
|
+
const contents = session.contents;
|
|
705
|
+
if (!contents) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const onStateChange = () => publishState(session);
|
|
709
|
+
const onDidNavigate = (...args) => {
|
|
710
|
+
const url = typeof args[1] === "string" ? args[1] : void 0;
|
|
711
|
+
const statusCode = typeof args[2] === "number" ? args[2] : void 0;
|
|
712
|
+
const statusText = typeof args[3] === "string" ? args[3] : void 0;
|
|
713
|
+
publishState(session);
|
|
714
|
+
if (!isHttpErrorStatusCode(statusCode)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
logger?.warn?.("Browser Node guest navigation returned HTTP error", {
|
|
718
|
+
currentUrl: contents.getURL(),
|
|
719
|
+
desiredUrl: session.desiredUrl,
|
|
720
|
+
nodeId: session.nodeId,
|
|
721
|
+
statusCode,
|
|
722
|
+
statusText,
|
|
723
|
+
url,
|
|
724
|
+
webContentsId: session.webContentsId
|
|
725
|
+
});
|
|
726
|
+
emit({
|
|
727
|
+
code: "navigation-failed",
|
|
728
|
+
diagnosticMessage: statusText,
|
|
729
|
+
nodeId: session.nodeId,
|
|
730
|
+
params: {
|
|
731
|
+
statusCode,
|
|
732
|
+
...statusText ? { statusText } : {}
|
|
733
|
+
},
|
|
734
|
+
type: "error"
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
const onFailLoad = (...args) => {
|
|
738
|
+
const errorCode = typeof args[1] === "number" ? args[1] : void 0;
|
|
739
|
+
const errorDescription = typeof args[2] === "string" ? args[2] : void 0;
|
|
740
|
+
const validatedUrl = typeof args[3] === "string" ? args[3] : void 0;
|
|
741
|
+
const isMainFrame = typeof args[4] === "boolean" ? args[4] : void 0;
|
|
742
|
+
if (isAbortedNavigationError({ errorCode, errorDescription })) {
|
|
743
|
+
publishState(session);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
logger?.warn?.("Browser Node guest navigation failed", {
|
|
747
|
+
currentUrl: contents.getURL(),
|
|
748
|
+
desiredUrl: session.desiredUrl,
|
|
749
|
+
errorCode,
|
|
750
|
+
errorDescription,
|
|
751
|
+
isMainFrame,
|
|
752
|
+
nodeId: session.nodeId,
|
|
753
|
+
validatedUrl,
|
|
754
|
+
webContentsId: session.webContentsId
|
|
755
|
+
});
|
|
756
|
+
session.navigationFailureSequence += 1;
|
|
757
|
+
publishState(session);
|
|
758
|
+
emitBrowserNavigationFailed({
|
|
759
|
+
emit,
|
|
760
|
+
errorCode,
|
|
761
|
+
errorDescription,
|
|
762
|
+
nodeId: session.nodeId
|
|
763
|
+
});
|
|
764
|
+
};
|
|
765
|
+
const onDestroyed = () => detachGuest(session);
|
|
766
|
+
const onWillNavigate = (...args) => {
|
|
767
|
+
const event = args[0] && typeof args[0] === "object" && "preventDefault" in args[0] ? args[0] : null;
|
|
768
|
+
const url = typeof args[1] === "string" ? args[1] : "";
|
|
769
|
+
if (url.length === 0 || isBrowserNavigationAllowedByPolicy({
|
|
770
|
+
policy: session.navigationPolicy,
|
|
771
|
+
url
|
|
772
|
+
})) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
event?.preventDefault?.();
|
|
776
|
+
emitOpenUrlFromGuest(session, url);
|
|
777
|
+
publishState(session);
|
|
778
|
+
};
|
|
779
|
+
const records = [
|
|
780
|
+
{ event: "did-start-loading", listener: onStateChange },
|
|
781
|
+
{ event: "did-stop-loading", listener: onStateChange },
|
|
782
|
+
{ event: "did-navigate", listener: onDidNavigate },
|
|
783
|
+
{ event: "did-navigate-in-page", listener: onStateChange },
|
|
784
|
+
{ event: "page-title-updated", listener: onStateChange },
|
|
785
|
+
{ event: "will-navigate", listener: onWillNavigate },
|
|
786
|
+
{ event: "did-fail-load", listener: onFailLoad },
|
|
787
|
+
{ event: "destroyed", listener: onDestroyed }
|
|
788
|
+
];
|
|
789
|
+
for (const record of records) {
|
|
790
|
+
contents.on(record.event, record.listener);
|
|
791
|
+
}
|
|
792
|
+
session.listeners = records;
|
|
793
|
+
};
|
|
794
|
+
const loadDesiredUrl = async (session) => {
|
|
795
|
+
const contents = session.contents;
|
|
796
|
+
if (!contents || contents.isDestroyed()) {
|
|
797
|
+
publishState(session);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const resolved = resolveHostBrowserNavigationUrl(session.desiredUrl);
|
|
801
|
+
if (!resolved.url) {
|
|
802
|
+
emit({
|
|
803
|
+
...resolveBrowserNodeUrlError(resolved),
|
|
804
|
+
nodeId: session.nodeId,
|
|
805
|
+
type: "error"
|
|
806
|
+
});
|
|
807
|
+
publishState(session);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (!isBrowserNavigationAllowedByPolicy({
|
|
811
|
+
policy: session.navigationPolicy,
|
|
812
|
+
url: resolved.url
|
|
813
|
+
})) {
|
|
814
|
+
emitOpenUrlFromGuest(session, resolved.url);
|
|
815
|
+
publishState(session);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const currentComparable = normalizeHostBrowserComparableUrl(
|
|
819
|
+
contents.getURL()
|
|
820
|
+
);
|
|
821
|
+
const nextComparable = normalizeHostBrowserComparableUrl(resolved.url);
|
|
822
|
+
if (currentComparable && currentComparable === nextComparable) {
|
|
823
|
+
publishState(session);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const failureSequenceBeforeLoad = session.navigationFailureSequence;
|
|
827
|
+
try {
|
|
828
|
+
await contents.loadURL(resolved.url);
|
|
829
|
+
publishState(session);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
832
|
+
logger?.warn?.("Browser Node guest loadURL failed", {
|
|
833
|
+
currentUrl: contents.getURL(),
|
|
834
|
+
desiredUrl: session.desiredUrl,
|
|
835
|
+
error: message,
|
|
836
|
+
nodeId: session.nodeId,
|
|
837
|
+
resolvedUrl: resolved.url,
|
|
838
|
+
webContentsId: session.webContentsId
|
|
839
|
+
});
|
|
840
|
+
if (session.navigationFailureSequence !== failureSequenceBeforeLoad) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
publishState(session);
|
|
844
|
+
emitBrowserNavigationFailed({
|
|
845
|
+
emit,
|
|
846
|
+
errorDescription: message,
|
|
847
|
+
nodeId: session.nodeId
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const emitOpenUrlFromGuest = (session, url) => {
|
|
852
|
+
const resolved = resolveBrowserNavigationUrl(url);
|
|
853
|
+
if (resolved.url) {
|
|
854
|
+
logger?.info?.("Browser Node guest emitted open-url", {
|
|
855
|
+
nodeId: session.nodeId,
|
|
856
|
+
url: resolved.url,
|
|
857
|
+
webContentsId: session.webContentsId
|
|
858
|
+
});
|
|
859
|
+
emit({
|
|
860
|
+
reuseIfOpen: false,
|
|
861
|
+
sourceNodeId: session.nodeId,
|
|
862
|
+
type: "open-url",
|
|
863
|
+
url: resolved.url
|
|
864
|
+
});
|
|
865
|
+
return { action: "deny" };
|
|
866
|
+
}
|
|
867
|
+
void Promise.resolve(openExternal(url)).catch((error) => {
|
|
868
|
+
logger?.warn?.("Browser Node openExternal failed", {
|
|
869
|
+
error: error instanceof Error ? error.message : String(error)
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
return { action: "deny" };
|
|
873
|
+
};
|
|
874
|
+
return {
|
|
875
|
+
async activate(input) {
|
|
876
|
+
const resolved = resolveHostBrowserNavigationUrl(input.url);
|
|
877
|
+
if (!resolved.url) {
|
|
878
|
+
throw new Error("Browser Node rejected navigation URL");
|
|
879
|
+
}
|
|
880
|
+
const session = getSession(input.nodeId, {
|
|
881
|
+
navigationPolicy: input.navigationPolicy,
|
|
882
|
+
profileId: input.profileId,
|
|
883
|
+
sessionMode: input.sessionMode,
|
|
884
|
+
sessionPartition: input.sessionPartition,
|
|
885
|
+
url: resolved.url
|
|
886
|
+
});
|
|
887
|
+
session.lifecycle = "active";
|
|
888
|
+
await loadDesiredUrl(session);
|
|
889
|
+
},
|
|
890
|
+
async capturePreview(input) {
|
|
891
|
+
const session = sessions.get(input.nodeId);
|
|
892
|
+
const contents = session?.contents && !session.contents.isDestroyed() ? session.contents : null;
|
|
893
|
+
if (!contents?.capturePage) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
const image = await contents.capturePage();
|
|
897
|
+
if (image.isEmpty?.() === true) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
return resizeBrowserPreviewImage(image).toDataURL();
|
|
901
|
+
},
|
|
902
|
+
close(input) {
|
|
903
|
+
const session = sessions.get(input.nodeId);
|
|
904
|
+
if (session) {
|
|
905
|
+
detachGuest(session);
|
|
906
|
+
sessions.delete(input.nodeId);
|
|
907
|
+
}
|
|
908
|
+
emit({ nodeId: input.nodeId, type: "closed" });
|
|
909
|
+
return Promise.resolve();
|
|
910
|
+
},
|
|
911
|
+
debugDump(input) {
|
|
912
|
+
const session = sessions.get(input.nodeId);
|
|
913
|
+
if (!session) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
const contents = session.contents && !session.contents.isDestroyed() ? session.contents : null;
|
|
917
|
+
return {
|
|
918
|
+
canGoBack: contents ? canGuestGoBack(contents) : false,
|
|
919
|
+
canGoForward: contents ? canGuestGoForward(contents) : false,
|
|
920
|
+
currentUrl: contents ? contents.getURL() : null,
|
|
921
|
+
desiredUrl: session.desiredUrl,
|
|
922
|
+
isAttachedToWindow: Boolean(contents),
|
|
923
|
+
isLoading: contents ? contents.isLoading() : false,
|
|
924
|
+
lifecycle: session.lifecycle,
|
|
925
|
+
nodeId: session.nodeId,
|
|
926
|
+
profileId: session.profileId,
|
|
927
|
+
sessionMode: session.sessionMode,
|
|
928
|
+
sessionPartition: session.sessionPartition,
|
|
929
|
+
title: contents ? contents.getTitle() : null,
|
|
930
|
+
userAgent: contents?.getUserAgent?.() ?? null,
|
|
931
|
+
webContentsDestroyed: session.contents ? session.contents.isDestroyed() : null,
|
|
932
|
+
webContentsId: session.webContentsId
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
goBack(input) {
|
|
936
|
+
const contents = sessions.get(input.nodeId)?.contents;
|
|
937
|
+
if (contents && !contents.isDestroyed() && canGuestGoBack(contents)) {
|
|
938
|
+
goGuestBack(contents);
|
|
939
|
+
}
|
|
940
|
+
return Promise.resolve();
|
|
941
|
+
},
|
|
942
|
+
goForward(input) {
|
|
943
|
+
const contents = sessions.get(input.nodeId)?.contents;
|
|
944
|
+
if (contents && !contents.isDestroyed() && canGuestGoForward(contents)) {
|
|
945
|
+
goGuestForward(contents);
|
|
946
|
+
}
|
|
947
|
+
return Promise.resolve();
|
|
948
|
+
},
|
|
949
|
+
handleGuestOpenUrl(webContentsId, input) {
|
|
950
|
+
const nodeId = nodeIdByWebContentsId.get(webContentsId);
|
|
951
|
+
const session = nodeId ? sessions.get(nodeId) : null;
|
|
952
|
+
if (!session) {
|
|
953
|
+
logger?.warn?.("Browser Node ignored guest open-url request", {
|
|
954
|
+
url: input.url,
|
|
955
|
+
webContentsId
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
logger?.info?.("Browser Node handling guest open-url request", {
|
|
960
|
+
nodeId: session.nodeId,
|
|
961
|
+
url: input.url,
|
|
962
|
+
webContentsId
|
|
963
|
+
});
|
|
964
|
+
emitOpenUrlFromGuest(session, input.url);
|
|
965
|
+
},
|
|
966
|
+
async navigate(input) {
|
|
967
|
+
const resolved = resolveBrowserNavigationUrl(input.url);
|
|
968
|
+
if (!resolved.url) {
|
|
969
|
+
throw new Error("Browser Node rejected navigation URL");
|
|
970
|
+
}
|
|
971
|
+
const session = getSession(input.nodeId, {
|
|
972
|
+
navigationPolicy: input.navigationPolicy,
|
|
973
|
+
url: resolved.url
|
|
974
|
+
});
|
|
975
|
+
session.lifecycle = "active";
|
|
976
|
+
await loadDesiredUrl(session);
|
|
977
|
+
},
|
|
978
|
+
async openExternal(input) {
|
|
979
|
+
const resolved = resolveBrowserNavigationUrl(input.url);
|
|
980
|
+
if (!resolved.url) {
|
|
981
|
+
throw new Error("Browser Node rejected external URL");
|
|
982
|
+
}
|
|
983
|
+
await Promise.resolve(openExternal(resolved.url));
|
|
984
|
+
},
|
|
985
|
+
openDevTools(input) {
|
|
986
|
+
const session = sessions.get(input.nodeId);
|
|
987
|
+
const contents = session?.contents ?? null;
|
|
988
|
+
if (contents && !contents.isDestroyed()) {
|
|
989
|
+
try {
|
|
990
|
+
contents.openDevTools?.({ activate: true, mode: "detach" });
|
|
991
|
+
} catch (error) {
|
|
992
|
+
logger?.warn?.("Browser Node open devtools failed", {
|
|
993
|
+
error: error instanceof Error ? error.message : String(error),
|
|
994
|
+
nodeId: input.nodeId,
|
|
995
|
+
webContentsId: session?.webContentsId ?? null
|
|
996
|
+
});
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return Promise.resolve();
|
|
1001
|
+
},
|
|
1002
|
+
async prepareSession(input) {
|
|
1003
|
+
await prepareSession?.(input);
|
|
1004
|
+
getSession(input.nodeId, {
|
|
1005
|
+
navigationPolicy: input.navigationPolicy,
|
|
1006
|
+
profileId: input.profileId,
|
|
1007
|
+
sessionMode: input.sessionMode,
|
|
1008
|
+
sessionPartition: input.sessionPartition
|
|
1009
|
+
});
|
|
1010
|
+
},
|
|
1011
|
+
async registerGuest(input) {
|
|
1012
|
+
await prepareSession?.({
|
|
1013
|
+
nodeId: input.nodeId,
|
|
1014
|
+
profileId: input.profileId,
|
|
1015
|
+
sessionMode: input.sessionMode,
|
|
1016
|
+
sessionPartition: input.sessionPartition,
|
|
1017
|
+
navigationPolicy: input.navigationPolicy
|
|
1018
|
+
});
|
|
1019
|
+
const contents = resolveWebContents(input.webContentsId);
|
|
1020
|
+
if (!contents || contents.isDestroyed()) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`Browser Node guest ${input.webContentsId} is not available`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const ownerNodeId = nodeIdByWebContentsId.get(input.webContentsId);
|
|
1026
|
+
if (ownerNodeId && ownerNodeId !== input.nodeId) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`Browser Node guest ${input.webContentsId} is already registered`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const session = getSession(input.nodeId, {
|
|
1032
|
+
navigationPolicy: input.navigationPolicy,
|
|
1033
|
+
profileId: input.profileId,
|
|
1034
|
+
sessionMode: input.sessionMode,
|
|
1035
|
+
sessionPartition: input.sessionPartition
|
|
1036
|
+
});
|
|
1037
|
+
if (session.webContentsId === input.webContentsId && session.contents === contents) {
|
|
1038
|
+
publishState(session);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (session.contents && session.contents !== contents) {
|
|
1042
|
+
detachGuest(session);
|
|
1043
|
+
}
|
|
1044
|
+
session.contents = contents;
|
|
1045
|
+
session.webContentsId = input.webContentsId;
|
|
1046
|
+
nodeIdByWebContentsId.set(input.webContentsId, input.nodeId);
|
|
1047
|
+
session.lifecycle = "active";
|
|
1048
|
+
logger?.info?.("Browser Node registered guest owner", {
|
|
1049
|
+
nodeId: input.nodeId,
|
|
1050
|
+
sessionPartition: session.sessionPartition,
|
|
1051
|
+
webContentsId: input.webContentsId
|
|
1052
|
+
});
|
|
1053
|
+
contents.setWindowOpenHandler?.(
|
|
1054
|
+
({ url }) => allowBrowserNodeGuestPopupWindow({
|
|
1055
|
+
logger,
|
|
1056
|
+
nodeId: input.nodeId,
|
|
1057
|
+
url,
|
|
1058
|
+
webContentsId: input.webContentsId
|
|
1059
|
+
})
|
|
1060
|
+
);
|
|
1061
|
+
attachGuestListeners(session);
|
|
1062
|
+
await applyPreferredColorSchemeToGuest(
|
|
1063
|
+
session,
|
|
1064
|
+
logger,
|
|
1065
|
+
syncPreferredColorScheme,
|
|
1066
|
+
preferredColorScheme
|
|
1067
|
+
);
|
|
1068
|
+
await loadDesiredUrl(session);
|
|
1069
|
+
},
|
|
1070
|
+
reload(input) {
|
|
1071
|
+
const contents = sessions.get(input.nodeId)?.contents;
|
|
1072
|
+
if (contents && !contents.isDestroyed()) {
|
|
1073
|
+
contents.reload();
|
|
1074
|
+
}
|
|
1075
|
+
return Promise.resolve();
|
|
1076
|
+
},
|
|
1077
|
+
unregisterGuest(input) {
|
|
1078
|
+
const session = sessions.get(input.nodeId);
|
|
1079
|
+
if (!session || session.webContentsId !== input.webContentsId) {
|
|
1080
|
+
return Promise.resolve();
|
|
1081
|
+
}
|
|
1082
|
+
session.lifecycle = "cold";
|
|
1083
|
+
detachGuest(session);
|
|
1084
|
+
return Promise.resolve();
|
|
1085
|
+
},
|
|
1086
|
+
dispose() {
|
|
1087
|
+
unsubscribePreferredColorScheme?.();
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/electron-main/registerElectronMain.ts
|
|
1093
|
+
function registerBrowserNodeElectronMain(input) {
|
|
1094
|
+
const managersByWindow = /* @__PURE__ */ new WeakMap();
|
|
1095
|
+
const loopbackPreviewProxy = input.loopbackPreviewRouting !== void 0 ? createBrowserNodeLoopbackPreviewProxy({
|
|
1096
|
+
logger: input.logger,
|
|
1097
|
+
resolveSession: async ({
|
|
1098
|
+
profileId,
|
|
1099
|
+
sessionMode,
|
|
1100
|
+
sessionPartition
|
|
1101
|
+
}) => {
|
|
1102
|
+
const { session } = await import("electron");
|
|
1103
|
+
return session.fromPartition(
|
|
1104
|
+
resolveBrowserSessionPartition({
|
|
1105
|
+
profileId,
|
|
1106
|
+
sessionMode,
|
|
1107
|
+
sessionPartition
|
|
1108
|
+
})
|
|
1109
|
+
);
|
|
1110
|
+
},
|
|
1111
|
+
routing: input.loopbackPreviewRouting
|
|
1112
|
+
}) : null;
|
|
1113
|
+
const resolveManagerForWindow = (event, ownerWindow) => {
|
|
1114
|
+
const existing = managersByWindow.get(ownerWindow);
|
|
1115
|
+
if (existing) {
|
|
1116
|
+
return existing;
|
|
1117
|
+
}
|
|
1118
|
+
const manager = createBrowserGuestManager({
|
|
1119
|
+
emit(browserEvent) {
|
|
1120
|
+
if (!ownerWindow.isDestroyed()) {
|
|
1121
|
+
ownerWindow.webContents.send(input.channels.event, browserEvent);
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
getPreferredColorScheme: input.getPreferredColorScheme,
|
|
1125
|
+
logger: input.logger,
|
|
1126
|
+
openExternal: input.openExternal,
|
|
1127
|
+
prepareSession: loopbackPreviewProxy !== null ? (payload) => loopbackPreviewProxy.configureSession(payload) : void 0,
|
|
1128
|
+
resolveWebContents: (webContentsId) => input.resolveWebContents({
|
|
1129
|
+
event,
|
|
1130
|
+
ownerWindow,
|
|
1131
|
+
webContentsId
|
|
1132
|
+
}),
|
|
1133
|
+
syncPreferredColorScheme: input.syncPreferredColorScheme,
|
|
1134
|
+
subscribePreferredColorScheme: input.subscribePreferredColorScheme
|
|
1135
|
+
});
|
|
1136
|
+
ownerWindow.once("closed", () => {
|
|
1137
|
+
manager.dispose();
|
|
1138
|
+
managersByWindow.delete(ownerWindow);
|
|
1139
|
+
});
|
|
1140
|
+
managersByWindow.set(ownerWindow, manager);
|
|
1141
|
+
return manager;
|
|
1142
|
+
};
|
|
1143
|
+
const resolveOwnedManager = (event) => {
|
|
1144
|
+
const ownerWindow = input.getOwnerWindow(event);
|
|
1145
|
+
if (!ownerWindow) {
|
|
1146
|
+
throw new Error("Browser Node IPC requires an owner window");
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
manager: resolveManagerForWindow(event, ownerWindow),
|
|
1150
|
+
ownerWindow
|
|
1151
|
+
};
|
|
1152
|
+
};
|
|
1153
|
+
const showDevToolsContextMenu = input.showDevToolsContextMenu ?? showElectronDevToolsContextMenu;
|
|
1154
|
+
input.registerHandler(
|
|
1155
|
+
input.channels.prepareSession,
|
|
1156
|
+
(event, payload) => resolveOwnedManager(event).manager.prepareSession(
|
|
1157
|
+
payload
|
|
1158
|
+
)
|
|
1159
|
+
);
|
|
1160
|
+
input.registerHandler(
|
|
1161
|
+
input.channels.activate,
|
|
1162
|
+
(event, payload) => resolveOwnedManager(event).manager.activate(
|
|
1163
|
+
payload
|
|
1164
|
+
)
|
|
1165
|
+
);
|
|
1166
|
+
if (input.channels.capturePreview) {
|
|
1167
|
+
input.registerHandler(
|
|
1168
|
+
input.channels.capturePreview,
|
|
1169
|
+
(event, payload) => resolveOwnedManager(event).manager.capturePreview(
|
|
1170
|
+
payload
|
|
1171
|
+
)
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
input.registerHandler(
|
|
1175
|
+
input.channels.registerGuest,
|
|
1176
|
+
(event, payload) => resolveOwnedManager(event).manager.registerGuest(
|
|
1177
|
+
payload
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
input.registerHandler(
|
|
1181
|
+
input.channels.unregisterGuest,
|
|
1182
|
+
(event, payload) => resolveOwnedManager(event).manager.unregisterGuest(
|
|
1183
|
+
payload
|
|
1184
|
+
)
|
|
1185
|
+
);
|
|
1186
|
+
input.registerHandler(
|
|
1187
|
+
input.channels.navigate,
|
|
1188
|
+
(event, payload) => resolveOwnedManager(event).manager.navigate(
|
|
1189
|
+
payload
|
|
1190
|
+
)
|
|
1191
|
+
);
|
|
1192
|
+
if (input.channels.openExternal) {
|
|
1193
|
+
input.registerHandler(
|
|
1194
|
+
input.channels.openExternal,
|
|
1195
|
+
(event, payload) => resolveOwnedManager(event).manager.openExternal(
|
|
1196
|
+
payload
|
|
1197
|
+
)
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
if (input.channels.openDevTools) {
|
|
1201
|
+
input.registerHandler(input.channels.openDevTools, (event, payload) => {
|
|
1202
|
+
const nodePayload = payload;
|
|
1203
|
+
return resolveOwnedManager(event).manager.openDevTools(nodePayload);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (input.channels.showDevToolsContextMenu) {
|
|
1207
|
+
input.registerHandler(
|
|
1208
|
+
input.channels.showDevToolsContextMenu,
|
|
1209
|
+
(event, payload) => {
|
|
1210
|
+
const contextMenuPayload = payload;
|
|
1211
|
+
const { manager, ownerWindow } = resolveOwnedManager(event);
|
|
1212
|
+
return showDevToolsContextMenu({
|
|
1213
|
+
label: contextMenuPayload.label,
|
|
1214
|
+
openDevTools: () => {
|
|
1215
|
+
return manager.openDevTools({ nodeId: contextMenuPayload.nodeId });
|
|
1216
|
+
},
|
|
1217
|
+
ownerWindow,
|
|
1218
|
+
point: contextMenuPayload.point
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
input.registerHandler(
|
|
1224
|
+
input.channels.goBack,
|
|
1225
|
+
(event, payload) => resolveOwnedManager(event).manager.goBack(payload)
|
|
1226
|
+
);
|
|
1227
|
+
input.registerHandler(
|
|
1228
|
+
input.channels.goForward,
|
|
1229
|
+
(event, payload) => resolveOwnedManager(event).manager.goForward(
|
|
1230
|
+
payload
|
|
1231
|
+
)
|
|
1232
|
+
);
|
|
1233
|
+
input.registerHandler(
|
|
1234
|
+
input.channels.reload,
|
|
1235
|
+
(event, payload) => resolveOwnedManager(event).manager.reload(payload)
|
|
1236
|
+
);
|
|
1237
|
+
input.registerHandler(
|
|
1238
|
+
input.channels.close,
|
|
1239
|
+
(event, payload) => resolveOwnedManager(event).manager.close(payload)
|
|
1240
|
+
);
|
|
1241
|
+
if (input.channels.debugDump) {
|
|
1242
|
+
input.registerHandler(
|
|
1243
|
+
input.channels.debugDump,
|
|
1244
|
+
(event, payload) => resolveOwnedManager(event).manager.debugDump(
|
|
1245
|
+
payload
|
|
1246
|
+
)
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (input.channels.guestOpenUrl && input.registerListener) {
|
|
1250
|
+
input.registerListener(input.channels.guestOpenUrl, (event, payload) => {
|
|
1251
|
+
const senderId = readBrowserNodeIpcSenderId(event);
|
|
1252
|
+
const openUrlPayload = payload;
|
|
1253
|
+
if (typeof senderId !== "number" || !Number.isFinite(senderId) || !openUrlPayload || typeof openUrlPayload.url !== "string") {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
resolveOwnedManager(event).manager.handleGuestOpenUrl(
|
|
1257
|
+
senderId,
|
|
1258
|
+
openUrlPayload
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function showElectronDevToolsContextMenu(input) {
|
|
1264
|
+
const { Menu } = await import("electron");
|
|
1265
|
+
const menu = Menu.buildFromTemplate([
|
|
1266
|
+
{
|
|
1267
|
+
click: () => {
|
|
1268
|
+
void Promise.resolve(input.openDevTools()).catch(() => void 0);
|
|
1269
|
+
},
|
|
1270
|
+
label: input.label
|
|
1271
|
+
}
|
|
1272
|
+
]);
|
|
1273
|
+
menu.popup({
|
|
1274
|
+
window: input.ownerWindow,
|
|
1275
|
+
x: Math.round(input.point.x),
|
|
1276
|
+
y: Math.round(input.point.y)
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
function readBrowserNodeIpcSenderId(event) {
|
|
1280
|
+
const sender = event?.sender;
|
|
1281
|
+
return typeof sender?.id === "number" ? sender.id : null;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/electron-main/userAgent.ts
|
|
1285
|
+
var electronUserAgentTokenPattern = /\sElectron\/[^\s]+/g;
|
|
1286
|
+
function sanitizeBrowserGuestUserAgent(userAgent) {
|
|
1287
|
+
return userAgent.trim().replace(electronUserAgentTokenPattern, "").replace(/\s{2,}/g, " ");
|
|
1288
|
+
}
|
|
1289
|
+
function applyBrowserGuestUserAgent(contents, logger) {
|
|
1290
|
+
const guestContents = contents;
|
|
1291
|
+
if (typeof guestContents.getUserAgent !== "function" || typeof guestContents.setUserAgent !== "function") {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const currentUserAgent = guestContents.getUserAgent().trim();
|
|
1295
|
+
const nextUserAgent = sanitizeBrowserGuestUserAgent(currentUserAgent);
|
|
1296
|
+
if (!nextUserAgent || nextUserAgent === currentUserAgent) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
guestContents.setUserAgent(nextUserAgent);
|
|
1300
|
+
logger?.debug?.("Browser Node sanitized guest user agent", {
|
|
1301
|
+
webContentsId: contents.id ?? null
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/electron-main/webviewSecurity.ts
|
|
1306
|
+
function isBrowserNodeInitialWebviewUrl(url) {
|
|
1307
|
+
return (url ?? "").trim() === "about:blank";
|
|
1308
|
+
}
|
|
1309
|
+
function isBrowserNodeWebviewAttach(params, allowedSessionPartitions) {
|
|
1310
|
+
return params["data-browser-node-webview"] === "true" || isBrowserSessionPartitionAllowed(params.partition, allowedSessionPartitions);
|
|
1311
|
+
}
|
|
1312
|
+
function shouldAllowBrowserNodeNativePopups(params) {
|
|
1313
|
+
return params["data-browser-node-webview"] === "true" || isBrowserSessionPartitionAllowed(params.partition);
|
|
1314
|
+
}
|
|
1315
|
+
function getPopupLogMetadata2(url) {
|
|
1316
|
+
try {
|
|
1317
|
+
const parsed = new URL(url);
|
|
1318
|
+
return {
|
|
1319
|
+
popupOrigin: parsed.origin,
|
|
1320
|
+
popupPath: parsed.pathname,
|
|
1321
|
+
popupProtocol: parsed.protocol
|
|
1322
|
+
};
|
|
1323
|
+
} catch {
|
|
1324
|
+
return {
|
|
1325
|
+
popupOrigin: null,
|
|
1326
|
+
popupPath: null,
|
|
1327
|
+
popupProtocol: null
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function allowBrowserNodeNativePopupWindow({
|
|
1332
|
+
guestWebContentsId,
|
|
1333
|
+
logger,
|
|
1334
|
+
url
|
|
1335
|
+
}) {
|
|
1336
|
+
logger?.info?.("Browser Node native popup allowed", {
|
|
1337
|
+
guestWebContentsId,
|
|
1338
|
+
...getPopupLogMetadata2(url)
|
|
1339
|
+
});
|
|
1340
|
+
return {
|
|
1341
|
+
action: "allow",
|
|
1342
|
+
overrideBrowserWindowOptions: {
|
|
1343
|
+
height: 720,
|
|
1344
|
+
show: true,
|
|
1345
|
+
width: 520
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
function enforceBrowserWebviewSecurity({
|
|
1350
|
+
allowedSessionPartitions,
|
|
1351
|
+
params,
|
|
1352
|
+
resolvePreload,
|
|
1353
|
+
webPreferences
|
|
1354
|
+
}) {
|
|
1355
|
+
webPreferences.allowRunningInsecureContent = false;
|
|
1356
|
+
webPreferences.contextIsolation = true;
|
|
1357
|
+
webPreferences.javascript = true;
|
|
1358
|
+
webPreferences.nodeIntegration = false;
|
|
1359
|
+
webPreferences.plugins = false;
|
|
1360
|
+
webPreferences.sandbox = true;
|
|
1361
|
+
webPreferences.webSecurity = true;
|
|
1362
|
+
delete webPreferences.preload;
|
|
1363
|
+
const partition = params.partition;
|
|
1364
|
+
if (!partition || !isBrowserSessionPartitionAllowed(partition, allowedSessionPartitions)) {
|
|
1365
|
+
return {
|
|
1366
|
+
allowed: false,
|
|
1367
|
+
reason: "Unsupported Browser Node session partition"
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
if (isBrowserNodeInitialWebviewUrl(params.src)) {
|
|
1371
|
+
params.src = "about:blank";
|
|
1372
|
+
} else {
|
|
1373
|
+
const resolved = resolveBrowserNavigationUrl(params.src ?? "about:blank");
|
|
1374
|
+
if (!resolved.url) {
|
|
1375
|
+
return {
|
|
1376
|
+
allowed: false,
|
|
1377
|
+
reason: "Unsupported browser URL"
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
params.src = resolved.url;
|
|
1381
|
+
}
|
|
1382
|
+
const preload = resolvePreload?.({ params: { ...params } });
|
|
1383
|
+
const resolvedPreload = typeof preload === "string" ? preload.trim() : "";
|
|
1384
|
+
if (resolvedPreload.length > 0) {
|
|
1385
|
+
webPreferences.preload = resolvedPreload;
|
|
1386
|
+
}
|
|
1387
|
+
return { allowed: true, reason: null };
|
|
1388
|
+
}
|
|
1389
|
+
function installBrowserWebviewSecurity({
|
|
1390
|
+
allowedSessionPartitions,
|
|
1391
|
+
contents,
|
|
1392
|
+
logger,
|
|
1393
|
+
onGuestAttached,
|
|
1394
|
+
openExternal,
|
|
1395
|
+
resolvePreload,
|
|
1396
|
+
shouldHandleWebview
|
|
1397
|
+
}) {
|
|
1398
|
+
const pendingBrowserAttaches = [];
|
|
1399
|
+
const handleWillAttachWebview = (event, webPreferences, params) => {
|
|
1400
|
+
const shouldHandle = shouldHandleWebview?.(params) ?? isBrowserNodeWebviewAttach(params, allowedSessionPartitions);
|
|
1401
|
+
logger?.debug?.("Browser Node webview will attach", {
|
|
1402
|
+
partition: params.partition ?? null,
|
|
1403
|
+
shouldHandle,
|
|
1404
|
+
src: params.src ?? null
|
|
1405
|
+
});
|
|
1406
|
+
if (!shouldHandle) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const allowNativePopups = shouldAllowBrowserNodeNativePopups(params);
|
|
1410
|
+
if (allowNativePopups) {
|
|
1411
|
+
params.allowpopups = "true";
|
|
1412
|
+
}
|
|
1413
|
+
logger?.info?.("Browser Node webview popup policy applied", {
|
|
1414
|
+
allowNativePopups,
|
|
1415
|
+
allowpopups: params.allowpopups ?? null,
|
|
1416
|
+
partition: params.partition ?? null,
|
|
1417
|
+
src: params.src ?? null
|
|
1418
|
+
});
|
|
1419
|
+
const result = enforceBrowserWebviewSecurity({
|
|
1420
|
+
allowedSessionPartitions,
|
|
1421
|
+
params,
|
|
1422
|
+
resolvePreload,
|
|
1423
|
+
webPreferences
|
|
1424
|
+
});
|
|
1425
|
+
if (!result.allowed) {
|
|
1426
|
+
logger?.warn?.("Browser Node webview blocked", { reason: result.reason });
|
|
1427
|
+
event.preventDefault();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
pendingBrowserAttaches.push({
|
|
1431
|
+
allowNativePopups
|
|
1432
|
+
});
|
|
1433
|
+
logger?.debug?.("Browser Node webview attach allowed", {
|
|
1434
|
+
partition: params.partition ?? null,
|
|
1435
|
+
src: params.src ?? null
|
|
1436
|
+
});
|
|
1437
|
+
};
|
|
1438
|
+
const handleDidAttachWebview = (_event, guestContents) => {
|
|
1439
|
+
const pendingAttach = pendingBrowserAttaches.shift();
|
|
1440
|
+
if (!pendingAttach) {
|
|
1441
|
+
logger?.debug?.("Browser Node webview did attach ignored", {
|
|
1442
|
+
guestWebContentsId: guestContents.id ?? null,
|
|
1443
|
+
pendingBrowserAttachCount: pendingBrowserAttaches.length
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
applyBrowserGuestUserAgent(guestContents, logger);
|
|
1448
|
+
if (pendingAttach.allowNativePopups) {
|
|
1449
|
+
guestContents.setWindowOpenHandler(
|
|
1450
|
+
({ url }) => allowBrowserNodeNativePopupWindow({
|
|
1451
|
+
guestWebContentsId: guestContents.id ?? null,
|
|
1452
|
+
logger,
|
|
1453
|
+
url
|
|
1454
|
+
})
|
|
1455
|
+
);
|
|
1456
|
+
} else {
|
|
1457
|
+
guestContents.setWindowOpenHandler(({ url }) => {
|
|
1458
|
+
logger?.info?.("Browser Node webview popup externalized", {
|
|
1459
|
+
guestWebContentsId: guestContents.id ?? null,
|
|
1460
|
+
...getPopupLogMetadata2(url)
|
|
1461
|
+
});
|
|
1462
|
+
const resolved = resolveBrowserNavigationUrl(url);
|
|
1463
|
+
if (resolved.url) {
|
|
1464
|
+
void Promise.resolve(openExternal(resolved.url)).catch(
|
|
1465
|
+
() => void 0
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
return { action: "deny" };
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
onGuestAttached?.(guestContents);
|
|
1472
|
+
logger?.debug?.("Browser Node webview guest attached", {
|
|
1473
|
+
guestWebContentsId: guestContents.id ?? null,
|
|
1474
|
+
pendingBrowserAttachCount: pendingBrowserAttaches.length
|
|
1475
|
+
});
|
|
1476
|
+
};
|
|
1477
|
+
contents.on("will-attach-webview", handleWillAttachWebview);
|
|
1478
|
+
contents.on("did-attach-webview", handleDidAttachWebview);
|
|
1479
|
+
return () => {
|
|
1480
|
+
contents.off("will-attach-webview", handleWillAttachWebview);
|
|
1481
|
+
contents.off("did-attach-webview", handleDidAttachWebview);
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
export {
|
|
1485
|
+
applyBrowserGuestUserAgent,
|
|
1486
|
+
enforceBrowserWebviewSecurity,
|
|
1487
|
+
installBrowserWebviewSecurity,
|
|
1488
|
+
isBrowserNodeWebviewAttach,
|
|
1489
|
+
registerBrowserNodeElectronMain,
|
|
1490
|
+
sanitizeBrowserGuestUserAgent
|
|
1491
|
+
};
|
|
1492
|
+
//# sourceMappingURL=index.js.map
|