Add Electron desktop shell workspace

This commit is contained in:
dirtydishes 2026-05-13 09:21:06 -04:00
parent b803d10836
commit 5d8e5ea44a
16 changed files with 1652 additions and 21 deletions

117
apps/desktop/src/main.ts Normal file
View file

@ -0,0 +1,117 @@
import { app, BrowserWindow, shell } from "electron";
import type { Event as ElectronEvent } from "electron";
import {
DESKTOP_PRODUCTION_URL,
isSafeExternalUrl,
isTrustedAppUrl,
resolveDesktopStartUrl
} from "./security.js";
const WINDOW_BACKGROUND_COLOR = "#06080b";
const WINDOW_TITLE = "Islandflow";
let mainWindow: BrowserWindow | null = null;
const canOpenExternalUrl = (sourceUrl: string, targetUrl: string): boolean => {
return isTrustedAppUrl(sourceUrl) && isSafeExternalUrl(targetUrl);
};
const openExternalUrl = async (sourceUrl: string, targetUrl: string): Promise<void> => {
if (!canOpenExternalUrl(sourceUrl, targetUrl)) {
return;
}
await shell.openExternal(targetUrl);
};
const installNavigationGuards = (window: BrowserWindow): void => {
const { webContents } = window;
const { session } = webContents;
session.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false);
});
const handleNavigationAttempt = (event: ElectronEvent, targetUrl: string) => {
if (isTrustedAppUrl(targetUrl)) {
return;
}
event.preventDefault();
void openExternalUrl(webContents.getURL(), targetUrl);
};
webContents.on("will-navigate", handleNavigationAttempt);
webContents.on("will-redirect", handleNavigationAttempt);
webContents.setWindowOpenHandler(({ url }) => {
void openExternalUrl(webContents.getURL(), url);
return { action: "deny" };
});
};
const createMainWindow = (): BrowserWindow => {
const window = new BrowserWindow({
width: 1440,
height: 960,
minWidth: 1200,
minHeight: 800,
show: false,
title: WINDOW_TITLE,
backgroundColor: WINDOW_BACKGROUND_COLOR,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
webviewTag: false
}
});
installNavigationGuards(window);
window.once("ready-to-show", () => {
window.show();
});
window.on("closed", () => {
if (mainWindow === window) {
mainWindow = null;
}
});
const startUrl = resolveDesktopStartUrl(process.env.ISLANDFLOW_DESKTOP_START_URL);
if (process.env.ISLANDFLOW_DESKTOP_START_URL && startUrl === DESKTOP_PRODUCTION_URL) {
console.warn(
`[desktop] Refused untrusted ISLANDFLOW_DESKTOP_START_URL; falling back to ${DESKTOP_PRODUCTION_URL}`
);
}
void window.loadURL(startUrl);
return window;
};
const ensureMainWindow = (): void => {
if (mainWindow) {
return;
}
mainWindow = createMainWindow();
};
app.whenReady().then(() => {
ensureMainWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
ensureMainWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});

View file

@ -0,0 +1,41 @@
import { describe, expect, it } from "bun:test";
import {
DESKTOP_PRODUCTION_URL,
isSafeExternalUrl,
isTrustedAppUrl,
resolveDesktopStartUrl
} from "./security.js";
describe("desktop URL policy", () => {
it("allows the hosted production origin", () => {
expect(isTrustedAppUrl("https://flow.deltaisland.io/tape?symbol=SPY")).toBe(true);
});
it("allows local dev origins", () => {
expect(isTrustedAppUrl("http://127.0.0.1:3000/signals")).toBe(true);
expect(isTrustedAppUrl("http://localhost:3000/charts")).toBe(true);
});
it("rejects untrusted origins", () => {
expect(isTrustedAppUrl("https://example.com")).toBe(false);
expect(isTrustedAppUrl("http://127.0.0.1:4000")).toBe(false);
});
it("rejects malformed URLs", () => {
expect(isTrustedAppUrl("not a url")).toBe(false);
expect(isTrustedAppUrl("javascript:alert('xss')")).toBe(false);
});
it("treats third-party http targets as external-only", () => {
expect(isSafeExternalUrl("https://deltaisland.io/about")).toBe(true);
expect(isSafeExternalUrl("mailto:support@deltaisland.io")).toBe(false);
expect(isSafeExternalUrl("https://flow.deltaisland.io/help")).toBe(false);
});
it("falls back to production when the desktop start URL is invalid", () => {
expect(resolveDesktopStartUrl(undefined)).toBe(DESKTOP_PRODUCTION_URL);
expect(resolveDesktopStartUrl("https://example.com")).toBe(DESKTOP_PRODUCTION_URL);
expect(resolveDesktopStartUrl("http://127.0.0.1:3000")).toBe("http://127.0.0.1:3000");
});
});

View file

@ -0,0 +1,44 @@
export const DESKTOP_PRODUCTION_URL = "https://flow.deltaisland.io";
export const DESKTOP_LOCAL_DEV_URL = "http://127.0.0.1:3000";
const TRUSTED_ORIGINS = new Set([
new URL(DESKTOP_PRODUCTION_URL).origin,
new URL(DESKTOP_LOCAL_DEV_URL).origin,
"http://localhost:3000"
]);
const HTTP_PROTOCOLS = new Set(["http:", "https:"]);
const parseUrl = (candidate: string): URL | null => {
try {
return new URL(candidate);
} catch {
return null;
}
};
export const isTrustedAppUrl = (candidate: string): boolean => {
const url = parseUrl(candidate);
if (!url || !HTTP_PROTOCOLS.has(url.protocol)) {
return false;
}
return TRUSTED_ORIGINS.has(url.origin);
};
export const isSafeExternalUrl = (candidate: string): boolean => {
const url = parseUrl(candidate);
if (!url || !HTTP_PROTOCOLS.has(url.protocol)) {
return false;
}
return !TRUSTED_ORIGINS.has(url.origin);
};
export const resolveDesktopStartUrl = (candidate: string | undefined): string => {
if (candidate && isTrustedAppUrl(candidate)) {
return candidate;
}
return DESKTOP_PRODUCTION_URL;
};