feat: finish the web-endpoint plugin, add ctx.desktop (B8)

This commit is contained in:
LukeGus committed 2026-09-23 18:36:41 -05:00
1 parent 1682861853
commit 535126fd1b
50 files changed
+2018 -898

No files matched your search

@@ -1,6 +1,6 @@
const { randomUUID } = require("node:crypto");
function webUrl(value) {
function isolatedUrl(value) {
const url = new URL(value);
if (
!["http:", "https:"].includes(url.protocol) ||
@@ -8,27 +8,35 @@ function webUrl(value) {
url.password
) {
throw new Error(
"Web endpoints require an HTTP(S) URL without embedded credentials",
"Isolated windows require an HTTP(S) URL without embedded credentials",
);
}
return url;
}
function createWebEndpointWindows({ BrowserWindow, session, getMainWindow }) {
/**
* Opens a BrowserWindow in its own non-persistent session, isolated from the
* main Termix window's cookies and storage. Used for any URL a plugin wants
* shown without sharing Termix's own session (a tunnelled or direct host web
* UI today; core has no other caller yet).
*
* Two ways in, both trusted: the renderer's own IPC call (checked against the
* main window's webContents before reaching here) and the embedded backend's
* fork IPC request (trusted because it is this app's own forked process, not
* arbitrary web content -- see the "backend-request" handler below).
*/
function createIsolatedWindows({ BrowserWindow, session, getMainWindow }) {
const certificates = new WeakMap();
async function open(event, options = {}) {
async function open(options = {}) {
const main = getMainWindow();
if (
!main ||
event.sender !== main.webContents ||
event.senderFrame !== main.webContents.mainFrame
) {
throw new Error("Only the main Termix window can open web endpoints");
if (!main) {
throw new Error("No window to attach an isolated window to");
}
const url = webUrl(options.url);
const isolated = session.fromPartition(`web-endpoint-${randomUUID()}`, {
cache: false,
});
const url = isolatedUrl(options.url);
const isolated = session.fromPartition(
options.partition || `isolated-window-${randomUUID()}`,
{ cache: false },
);
isolated.setPermissionRequestHandler((_contents, _permission, callback) =>
callback(false),
);
@@ -57,7 +65,7 @@ function createWebEndpointWindows({ BrowserWindow, session, getMainWindow }) {
win.setMenu(null);
const canNavigate = (target) => {
try {
return target === "about:blank" || !!webUrl(target);
return target === "about:blank" || !!isolatedUrl(target);
} catch {
return false;
}
@@ -90,7 +98,7 @@ function createWebEndpointWindows({ BrowserWindow, session, getMainWindow }) {
const win = new BrowserWindow({
width: 1100,
height: 800,
title: `Web endpoint — ${url.hostname}`,
title: options.title || url.hostname,
webPreferences: preferences,
});
configure(win);
@@ -125,4 +133,4 @@ function createWebEndpointWindows({ BrowserWindow, session, getMainWindow }) {
}
return { open, handleCertificateError };
}
module.exports = { createWebEndpointWindows, webUrl };
module.exports = { createIsolatedWindows, isolatedUrl };
+40 -11
View File
@@ -633,15 +633,45 @@ if (isInsecureModeEnabled()) {
app.commandLine.appendSwitch("--enable-features=NetworkService");
let mainWindow = null;
const { createWebEndpointWindows } = require("./web-endpoint-window.cjs");
const webEndpointWindows = createWebEndpointWindows({
const { createIsolatedWindows } = require("./isolated-window.cjs");
const isolatedWindows = createIsolatedWindows({
BrowserWindow,
session,
getMainWindow: () => mainWindow,
});
ipcMain.handle("open-isolated-web-endpoint", (event, options) =>
webEndpointWindows.open(event, options),
);
// Requests from the embedded backend over the fork IPC channel (the other
// direction from the "shutdown" message main already sends it). A plugin's
// ctx.desktop.openIsolatedWindow is the one caller today.
const BACKEND_REQUEST_HANDLERS = {
"open-isolated-window": (payload) => isolatedWindows.open(payload),
};
async function handleBackendRequest(msg) {
if (!msg || msg.type !== "backend-request") return;
const { id, channel, payload } = msg;
const handler = BACKEND_REQUEST_HANDLERS[channel];
const reply = (response) => {
if (backendProcess && !backendProcess.killed) {
try {
backendProcess.send({ type: "backend-response", id, ...response });
} catch (error) {
logToFile("Failed to reply to backend request:", error.message);
}
}
};
if (!handler) {
reply({ ok: false, error: `Unknown backend request channel: ${channel}` });
return;
}
try {
const result = await handler(payload);
reply({ ok: true, result });
} catch (error) {
reply({ ok: false, error: error.message || String(error) });
}
}
let backendProcess = null;
let backendStartFailed = false;
// Why the embedded backend died, once it has. Null while it is healthy
@@ -826,12 +856,7 @@ app.on(
"certificate-error",
(event, _webContents, url, error, certificate, callback) => {
if (
webEndpointWindows.handleCertificateError(
event,
_webContents,
url,
callback,
)
isolatedWindows.handleCertificateError(event, _webContents, url, callback)
)
return;
if (isWebEndpointCertificateAllowed(url)) {
@@ -1155,6 +1180,10 @@ function startBackendServer() {
logToFile("[backend:stderr]", chunk.trim());
});
backendProcess.on("message", (msg) => {
void handleBackendRequest(msg);
});
backendProcess.on("exit", (code, signal) => {
logToFile(`Backend process exited with code ${code}, signal ${signal}`);
if (!resolved && code !== 0) {
-1
View File
@@ -16,7 +16,6 @@ const ALLOWED_INVOKE_CHANNELS = new Set([
"save-remote-sync-jwt",
"test-server-connection",
"allow-invalid-certificate-for-origin",
"open-isolated-web-endpoint",
]);
function invokeAllowed(channel, ...args) {