diff --git a/electron/app-version.json b/electron/app-version.json new file mode 100644 index 00000000..8a0e30bf --- /dev/null +++ b/electron/app-version.json @@ -0,0 +1,3 @@ +{ + "version": "4.8.3" +} diff --git a/electron/appVersion.js b/electron/appVersion.js new file mode 100644 index 00000000..5e01edaa --- /dev/null +++ b/electron/appVersion.js @@ -0,0 +1,29 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +/** + * Packaged Electron builds only ship electron/ in asar, so app.getVersion() can + * read 0.0.0. Prefer app-version.json (synced from package.json) then dev package.json. + */ +function readPackagedAppVersion(fallback = "0.0.0") { + const candidates = [ + path.join(__dirname, "app-version.json"), + path.join(__dirname, "..", "package.json"), + ]; + for (const candidate of candidates) { + try { + const raw = JSON.parse(fs.readFileSync(candidate, "utf8")); + const version = raw && raw.version; + if (version) { + return String(version); + } + } catch { + // try next candidate + } + } + return fallback; +} + +module.exports = { readPackagedAppVersion }; diff --git a/electron/loading.html b/electron/loading.html index ff9ba902..bba5ca6e 100644 --- a/electron/loading.html +++ b/electron/loading.html @@ -104,9 +104,7 @@ -

- v0.0.0 -

+

@@ -137,6 +135,15 @@ const MAX_FAILURE_HISTORY = 8; let protocolOrder = ["https", "http"]; + let detectedProtocol = "http"; + let attemptCount = 0; + let runtimeProbeAttempt = 0; + let cachedRuntimeState = null; + let cachedDiagnostics = null; + let cachedCrash = null; + let startupFailed = false; + let pollTimer = null; + const recentFailures = []; applyTheme(detectPreferredTheme()); showAppVersion(); @@ -279,7 +286,9 @@ async function showAppVersion() { try { const appVersion = await window.electron.appVersion(); - document.getElementById("app-version").innerText = "v" + appVersion; + if (appVersion) { + document.getElementById("app-version").innerText = "v" + appVersion; + } } catch (e) {} } @@ -312,16 +321,6 @@ }); } - let detectedProtocol = "http"; - let attemptCount = 0; - let runtimeProbeAttempt = 0; - let cachedRuntimeState = null; - let cachedDiagnostics = null; - let cachedCrash = null; - let startupFailed = false; - let pollTimer = null; - const recentFailures = []; - function parseStatusJson(text) { const helper = window.MeshchatLoadingStatusProbe; if (helper && typeof helper.parseStatusJson === "function") { diff --git a/electron/main.js b/electron/main.js index bb0bef9b..23b7a7c7 100644 --- a/electron/main.js +++ b/electron/main.js @@ -18,6 +18,7 @@ const fs = require("fs"); const path = require("node:path"); const { createBackendProcessManager } = require("./backendProcess"); +const { readPackagedAppVersion } = require("./appVersion"); const { getCrashRecoveryInfo } = require("./offlineRecovery"); const { getUserProvidedArguments, @@ -48,6 +49,14 @@ const { const { getLogsDir } = require("./backendCrashReport"); const { installBrokenPipeGuards, createMainProcessLogger } = require("./safeConsole"); +function resolvePreloadScriptPath() { + const bundled = path.join(__dirname, "preload.bundle.js"); + if (fs.existsSync(bundled)) { + return bundled; + } + return path.join(__dirname, "preload.js"); +} + installBrokenPipeGuards(process); const mainProcessLogger = createMainProcessLogger({ @@ -168,7 +177,7 @@ function trustedIpcHandle(channel, listener) { // allow fetching app version via ipc trustedIpcHandle("app-version", () => { - return app.getVersion(); + return readPackagedAppVersion(app.getVersion()); }); // allow fetching hardware acceleration status via ipc @@ -525,7 +534,7 @@ function getChildBrowserWindowOptions() { return { autoHideMenuBar: true, webPreferences: { - preload: path.join(__dirname, "preload.js"), + preload: resolvePreloadScriptPath(), nodeIntegration: false, contextIsolation: true, sandbox: true, @@ -1048,7 +1057,7 @@ app.whenReady().then(async () => { autoHideMenuBar: true, webPreferences: { // used to inject logging over ipc - preload: path.join(__dirname, "preload.js"), + preload: resolvePreloadScriptPath(), // Security: disable node integration in renderer nodeIntegration: false, // Security: enable context isolation (default in Electron 12+) diff --git a/electron/preload.bundle.js b/electron/preload.bundle.js new file mode 100644 index 00000000..7e32cc05 --- /dev/null +++ b/electron/preload.bundle.js @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: 0BSD +// Generated by scripts/bundle-electron-preload.cjs. Do not edit by hand. + +"use strict"; + +const LOCAL_BACKEND_HOSTS = new Set(["127.0.0.1", "localhost"]); +const LOCAL_BACKEND_PORT = "9337"; + +/** + * Parse a URL string. Returns null when the input is not a valid absolute URL. + * @param {unknown} url + * @returns {URL | null} + */ +function parseAbsoluteUrl(url) { + if (!url || typeof url !== "string") { + return null; + } + try { + return new URL(url); + } catch { + return null; + } +} + +/** + * Inner http(s) URL of a blob URL, or null. + * blob:https://host/uuid has an origin of https://host, not the blob scheme itself. + * @param {unknown} url + * @returns {string | null} + */ +function blobInnerHttpUrl(url) { + if (!url || typeof url !== "string" || !url.startsWith("blob:")) { + return null; + } + const inner = url.slice("blob:".length); + const parsed = parseAbsoluteUrl(inner); + if (!parsed) { + return null; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return null; + } + return inner; +} + +/** + * Whether the URL is the MeshChatX local backend origin (loading / API checks). + * Parses so userinfo like http://127.0.0.1:9337@example.com is not local. + * @param {unknown} url + * @returns {boolean} + */ +function isLocalBackendUrl(url) { + const parsed = parseAbsoluteUrl(url); + if (!parsed) { + return false; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return false; + } + if (parsed.username !== "" || parsed.password !== "") { + return false; + } + const host = String(parsed.hostname || "").toLowerCase(); + if (!LOCAL_BACKEND_HOSTS.has(host)) { + return false; + } + return parsed.port === LOCAL_BACKEND_PORT; +} + +/** + * blob: URLs whose inner origin is the local backend (print preview). + * @param {unknown} url + * @returns {boolean} + */ +function isTrustedBlobUrl(url) { + const inner = blobInnerHttpUrl(url); + return inner != null && isLocalBackendUrl(inner); +} + +/** + * file: loading.html and crash.html in the Electron shell. Not arbitrary files. + * @param {unknown} url + * @returns {boolean} + */ +function isTrustedShellFileUrl(url) { + const parsed = parseAbsoluteUrl(url); + if (!parsed || parsed.protocol !== "file:") { + return false; + } + let pathname = parsed.pathname || ""; + try { + pathname = decodeURIComponent(pathname); + } catch { + return false; + } + const normalized = pathname.replace(/\\/g, "/").toLowerCase(); + return normalized.endsWith("/loading.html") || normalized.endsWith("/crash.html"); +} + +/** + * Origins allowed to call preload window.electron IPC. + * file: loading/crash pages, the local backend, and trusted print blobs. + * @param {unknown} url + * @returns {boolean} + */ +function isTrustedShellOrigin(url) { + if (isTrustedShellFileUrl(url)) { + return true; + } + if (isTrustedBlobUrl(url)) { + return true; + } + return isLocalBackendUrl(url); +} + +/** + * Whether window.open should create a child Electron window instead of the OS browser. + * Local backend popouts and call.html must stay in Electron so they keep the app session. + * @param {unknown} url + * @returns {boolean} + */ +function shouldOpenInElectronWindow(url) { + if (!url || typeof url !== "string") { + return false; + } + if (url.startsWith("blob:")) { + return isTrustedBlobUrl(url); + } + if (!isLocalBackendUrl(url)) { + return false; + } + const parsed = parseAbsoluteUrl(url); + if (!parsed) { + return false; + } + const pathname = parsed.pathname || ""; + if (pathname === "/call.html" || pathname.endsWith("/call.html")) { + return true; + } + return parsed.hash.startsWith("#/popout/"); +} + +/** + * Whether the main frame may navigate to this URL inside Electron (local app shell). + * External http(s) links must open in the system browser instead. + * data: and file: are denied. blob: is allowed only when the inner origin is local. + * @param {unknown} url + * @returns {boolean} + */ +function shouldAllowInWindowNavigation(url) { + if (!url || typeof url !== "string") { + return false; + } + if (url.startsWith("blob:")) { + return isTrustedBlobUrl(url); + } + return isLocalBackendUrl(url); +} + +/** + * URL of the renderer frame that invoked an ipcMain handler. + * Prefers senderFrame.url, then sender.getURL(). + * @param {unknown} event + * @returns {string} + */ +function senderUrlFromIpcEvent(event) { + if (!event || typeof event !== "object") { + return ""; + } + const frame = event.senderFrame; + if (frame && typeof frame.url === "string" && frame.url) { + return frame.url; + } + const sender = event.sender; + if (sender && typeof sender.getURL === "function") { + try { + const url = sender.getURL(); + return typeof url === "string" ? url : ""; + } catch { + return ""; + } + } + return ""; +} + +/** + * Whether ipcMain may run for this invoke. Same allowlist as preload. + * @param {unknown} event + * @returns {boolean} + */ +function isTrustedIpcEvent(event) { + return isTrustedShellOrigin(senderUrlFromIpcEvent(event)); +} + +const { ipcRenderer, contextBridge } = require("electron"); +function originAllowed() { + if (typeof location === "undefined") { + return false; + } + return isTrustedShellOrigin(location.href); +} + +function invokeTrusted(channel, ...args) { + if (!originAllowed()) { + return Promise.reject(new Error("MeshChatX IPC blocked for this origin")); + } + return ipcRenderer.invoke(channel, ...args); +} + +function onTrusted(channel, listener) { + ipcRenderer.on(channel, (event, ...payload) => { + if (!originAllowed()) { + return; + } + listener(event, ...payload); + }); +} + +onTrusted("log", (event, message) => console.log(message)); + +contextBridge.exposeInMainWorld("electron", { + appVersion: async function () { + return await invokeTrusted("app-version"); + }, + + electronVersion: function () { + if (!originAllowed()) { + return ""; + } + return process.versions.electron; + }, + + chromeVersion: function () { + if (!originAllowed()) { + return ""; + } + return process.versions.chrome; + }, + + nodeVersion: function () { + if (!originAllowed()) { + return ""; + } + return process.versions.node; + }, + + alert: async function (message) { + return await invokeTrusted("alert", message); + }, + + confirm: async function (message) { + return await invokeTrusted("confirm", message); + }, + + prompt: async function (message, defaultValue = "") { + return await invokeTrusted("prompt", message, defaultValue); + }, + + relaunch: async function () { + return await invokeTrusted("relaunch"); + }, + + relaunchEmergency: async function () { + return await invokeTrusted("relaunch-emergency"); + }, + + relaunchAutoRecover: async function () { + return await invokeTrusted("relaunch-auto-recover"); + }, + + getCrashRecoveryInfo: async function () { + return await invokeTrusted("crash-recovery-info"); + }, + + restoreDatabaseBackup: async function (backupPath) { + return await invokeTrusted("restore-database-backup", backupPath); + }, + + pickDatabaseBackup: async function () { + return await invokeTrusted("pick-database-backup"); + }, + + shutdown: async function () { + return await invokeTrusted("shutdown"); + }, + + getCloseSettings: async function () { + return await invokeTrusted("get-close-settings"); + }, + + setCloseSettings: async function (partial) { + return await invokeTrusted("set-close-settings", partial); + }, + + getPlatform: function () { + if (!originAllowed()) { + return ""; + } + return process.platform; + }, + + getScreenSecuritySettings: async function () { + return await invokeTrusted("get-screen-security-settings"); + }, + + setScreenSecurityEnabled: async function (enabled) { + return await invokeTrusted("set-screen-security-enabled", enabled === true); + }, + + getMemoryUsage: async function () { + return await invokeTrusted("get-memory-usage"); + }, + + getBatteryStatus: async function () { + return await invokeTrusted("get-battery-status"); + }, + + showPathInFolder: async function (path) { + return await invokeTrusted("showPathInFolder", path); + }, + openPath: async function (path) { + return await invokeTrusted("open-path", path); + }, + pickFile: async function () { + return await invokeTrusted("pick-file"); + }, + pickDirectory: async function () { + return await invokeTrusted("pick-directory"); + }, + isHardwareAccelerationEnabled: async function () { + return await invokeTrusted("is-hardware-acceleration-enabled"); + }, + getIntegrityStatus: async function () { + return await invokeTrusted("get-integrity-status"); + }, + showNotification: function (title, body, silent = false, destinationHash = null) { + invokeTrusted("show-notification", { title, body, silent, destinationHash }); + }, + closeMessageNotifications: function (destinationHash = null) { + return invokeTrusted("close-message-notifications", destinationHash); + }, + setPowerSaveBlocker: async function (enabled) { + return await invokeTrusted("set-power-save-blocker", enabled); + }, + onProtocolLink: function (callback) { + onTrusted("open-protocol-link", (event, url) => callback(url)); + }, + backendHttpOnly: async function () { + return await invokeTrusted("backend-http-only"); + }, + backendRuntimeState: async function () { + return await invokeTrusted("backend-runtime-state"); + }, + backendStartupDiagnostics: async function () { + return await invokeTrusted("backend-startup-diagnostics"); + }, + markBackendHealthy: async function () { + return await invokeTrusted("mark-backend-healthy"); + }, + restartBackend: async function () { + return await invokeTrusted("restart-backend"); + }, + openBackendCrashReport: async function () { + return await invokeTrusted("open-backend-crash-report"); + }, + onBackendProcessExited: function (callback) { + if (typeof callback !== "function") { + return; + } + onTrusted("backend-process-exited", (_event, payload) => callback(payload)); + }, + onBackendStartupFailed: function (callback) { + if (typeof callback !== "function") { + return; + } + onTrusted("backend-startup-failed", (_event, payload) => callback(payload)); + }, +}); diff --git a/meshchatx.rsm b/meshchatx.rsm index 3a18b35e..7cdff4d5 100644 Binary files a/meshchatx.rsm and b/meshchatx.rsm differ diff --git a/meshchatx/src/backend/database/__init__.py b/meshchatx/src/backend/database/__init__.py index 3650405a..6c138df7 100644 --- a/meshchatx/src/backend/database/__init__.py +++ b/meshchatx/src/backend/database/__init__.py @@ -47,8 +47,30 @@ MIN_SIZE_RATIO = 0.2 MIN_WIPE_MESSAGE_COUNT = 5 MESSAGE_DROP_RATIO = 0.5 MIN_SIZE_COMPARE_BYTES = 100_000 +_MIN_ZIP_DATE_TIME = (1980, 1, 1, 0, 0, 0) +_EPOCH_1980 = 315532800.0 _CONTENT_ANOMALY_PREFIX = "Database content anomaly:" + +def _zip_file_date_time(file_path: str) -> tuple: + try: + mtime = os.path.getmtime(file_path) + except OSError: + return _MIN_ZIP_DATE_TIME + if mtime < _EPOCH_1980: + return _MIN_ZIP_DATE_TIME + return time.localtime(mtime)[:6] + + +def _zip_write_file(zf: zipfile.ZipFile, file_path: str, arcname: str | None = None) -> None: + name = arcname if arcname is not None else os.path.basename(file_path) + with open(file_path, "rb") as handle: + data = handle.read() + zinfo = zipfile.ZipInfo(filename=name) + zinfo.date_time = _zip_file_date_time(file_path) + zinfo.compress_type = zipfile.ZIP_DEFLATED + zf.writestr(zinfo, data) + _log = logging.getLogger("meshchatx.database") @@ -696,7 +718,7 @@ class Database: rel_path = os.path.relpath(full_path, identity_dir).replace("\\", "/") if rel_path.startswith(".."): continue - zf.write(full_path, arcname=rel_path) + _zip_write_file(zf, full_path, arcname=rel_path) included.append(rel_path) return included @@ -733,11 +755,11 @@ class Database: db_basenames = {os.path.basename(p) for p in paths.values()} backup_abs = os.path.abspath(backup_path) with zipfile.ZipFile(backup_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: - zf.write(paths["main"], arcname=main_filename) + _zip_write_file(zf, paths["main"], arcname=main_filename) if os.path.exists(paths["wal"]): - zf.write(paths["wal"], arcname=f"{main_filename}-wal") + _zip_write_file(zf, paths["wal"], arcname=f"{main_filename}-wal") if os.path.exists(paths["shm"]): - zf.write(paths["shm"], arcname=f"{main_filename}-shm") + _zip_write_file(zf, paths["shm"], arcname=f"{main_filename}-shm") included = self._add_identity_storage_to_zip( zf, db_basenames, diff --git a/package.json b/package.json index 00ba1657..cfda0c5a 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "test:e2e:install": "playwright install chromium", "test:mutation:frontend": "node scripts/mutation/run.mjs", "test:mutation:frontend:sample": "node scripts/mutation/run.mjs --source meshchatx/src/frontend/js/rnode/Capabilities.js --max-per-file 20", - "electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js && node scripts/patch-electron-builder-fs.cjs && node scripts/patch-electron-installer-common.cjs", + "electron-postinstall": "electron-builder install-app-deps && node scripts/ensure-micron-parser-package.js && node scripts/patch-electron-builder-fs.cjs && node scripts/patch-electron-installer-common.cjs && node scripts/bundle-electron-preload.cjs", "electron": "pnpm run electron-postinstall && pnpm run build && electron .", "dist": "pnpm run electron-postinstall && pnpm run build && electron-builder --publish=never", "dist:linux": "pnpm run electron-postinstall && cross-env PLATFORM=linux pnpm run build && electron-builder --linux AppImage deb --publish=never", @@ -124,6 +124,7 @@ "appimage": "1.0.2" }, "files": [ + "package.json", "electron/", "electron/**" ], diff --git a/scripts/bundle-electron-preload.cjs b/scripts/bundle-electron-preload.cjs new file mode 100644 index 00000000..d789b74e --- /dev/null +++ b/scripts/bundle-electron-preload.cjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +"use strict"; + +/** + * Electron sandbox preloads cannot require() sibling modules. Bundle shellOrigin + * into a single preload script for packaged AppImage / desktop builds. + */ + +const fs = require("fs"); +const path = require("path"); + +const root = path.join(__dirname, ".."); +const shellPath = path.join(root, "electron", "shellOrigin.js"); +const preloadPath = path.join(root, "electron", "preload.js"); +const outPath = path.join(root, "electron", "preload.bundle.js"); + +const shellSource = fs.readFileSync(shellPath, "utf8"); +const preloadSource = fs.readFileSync(preloadPath, "utf8"); + +const shellBody = shellSource + .replace(/^"use strict";\s*/m, "") + .replace(/\nmodule\.exports\s*=\s*\{[\s\S]*$/m, "") + .trim(); + +const preloadBody = preloadSource + .replace(/const \{ isTrustedShellOrigin \} = require\("\.\/shellOrigin"\);\s*/m, "") + .trim(); + +const bundled = `// SPDX-License-Identifier: 0BSD +// Generated by scripts/bundle-electron-preload.cjs. Do not edit by hand. + +"use strict"; + +${shellBody} + +${preloadBody} +`; + +fs.writeFileSync(outPath, bundled, "utf8"); +console.log(`Bundled electron preload -> ${path.relative(root, outPath)}`); diff --git a/scripts/sync_version.js b/scripts/sync_version.js index 5cdff9d8..87ba2796 100644 --- a/scripts/sync_version.js +++ b/scripts/sync_version.js @@ -8,6 +8,7 @@ * meshchatx/src/frontend/public/meshchatx-docs/en/platform-guides/raspberry-pi.md, * meshchatx/src/backend/data/licenses_backend.json (reticulum-meshchatx entry), * android/app/build.gradle, + * electron/app-version.json, * pipx example, packaging/arch/PKGBUILD pkgver / printf fallback, * then runs scripts/bake_build_meta.js (git commit / channel overlay). * @@ -137,6 +138,9 @@ patchFile("packaging/arch/.SRCINFO", (c) => console.log(`Synced version ${version} from package.json`); +const appVersionPath = path.join(root, "electron", "app-version.json"); +writeIfChanged(appVersionPath, `${JSON.stringify({ version }, null, 4)}\n`); + try { require("./bake_build_meta.js"); } catch (err) { diff --git a/tests/backend/test_database_snapshots.py b/tests/backend/test_database_snapshots.py index 64dd82a7..5514895b 100644 --- a/tests/backend/test_database_snapshots.py +++ b/tests/backend/test_database_snapshots.py @@ -537,6 +537,70 @@ def test_pre_migration_backup_written_before_schema_upgrade(temp_dir): assert any("backup-pre-migrate" in row["name"] for row in backups) +def test_pre_migration_backup_handles_pre_1980_identity_file_mtime(temp_dir): + from meshchatx.src.backend.database.schema import DatabaseSchema + + legacy = os.path.join(temp_dir, "legacy.dat") + with open(legacy, "wb") as handle: + handle.write(b"legacy") + os.utime(legacy, (0, 0)) + + db_path = os.path.join(temp_dir, "test.db") + db = Database(db_path) + db.initialize() + db.close_all() + + prior = DatabaseSchema.LATEST_VERSION - 1 + if prior < 1: + pytest.skip("No prior schema version to simulate") + + provider = DatabaseProvider(db_path) + provider.execute( + "UPDATE config SET value = ? WHERE key = ?", + (str(prior), "database_version"), + ) + provider.close_all() + + upgraded = Database(db_path) + upgraded.initialize() + backups = upgraded.list_auto_backups(temp_dir) + upgraded.close_all() + + assert any("backup-pre-migrate" in row["name"] for row in backups) + + +def test_pre_migration_backup_handles_pre_1980_identity_file_mtime(temp_dir): + from meshchatx.src.backend.database.schema import DatabaseSchema + + legacy = os.path.join(temp_dir, "legacy.dat") + with open(legacy, "wb") as handle: + handle.write(b"legacy") + os.utime(legacy, (0, 0)) + + db_path = os.path.join(temp_dir, "test.db") + db = Database(db_path) + db.initialize() + db.close_all() + + prior = DatabaseSchema.LATEST_VERSION - 1 + if prior < 1: + pytest.skip("No prior schema version to simulate") + + provider = DatabaseProvider(db_path) + provider.execute( + "UPDATE config SET value = ? WHERE key = ?", + (str(prior), "database_version"), + ) + provider.close_all() + + upgraded = Database(db_path) + upgraded.initialize() + backups = upgraded.list_auto_backups(temp_dir) + upgraded.close_all() + + assert any("backup-pre-migrate" in row["name"] for row in backups) + + def test_pre_migration_backup_skipped_with_env(temp_dir, monkeypatch): from meshchatx.src.backend.database.schema import DatabaseSchema diff --git a/tests/backend/test_database_zip_timestamps.py b/tests/backend/test_database_zip_timestamps.py new file mode 100644 index 00000000..8444b45a --- /dev/null +++ b/tests/backend/test_database_zip_timestamps.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: 0BSD + +import os +import zipfile + +from meshchatx.src.backend.database import _zip_write_file + + +def test_zip_write_file_clamps_pre_1980_mtime(tmp_path): + old_file = tmp_path / "legacy.dat" + old_file.write_bytes(b"legacy") + os.utime(old_file, (0, 0)) + + zip_path = tmp_path / "backup.zip" + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + _zip_write_file(zf, str(old_file), arcname="legacy.dat") + + with zipfile.ZipFile(zip_path, "r") as zf: + info = zf.getinfo("legacy.dat") + assert info.date_time[0] >= 1980 + assert zf.read("legacy.dat") == b"legacy" diff --git a/tests/electron/appVersion.test.js b/tests/electron/appVersion.test.js new file mode 100644 index 00000000..c62303a1 --- /dev/null +++ b/tests/electron/appVersion.test.js @@ -0,0 +1,8 @@ +import { describe, expect, it } from "vitest"; +import { readPackagedAppVersion } from "../../electron/appVersion.js"; + +describe("electron/appVersion", () => { + it("reads version from electron/app-version.json", () => { + expect(readPackagedAppVersion("0.0.0")).toBe("4.8.3"); + }); +}); diff --git a/tests/electron/preloadBundle.test.js b/tests/electron/preloadBundle.test.js new file mode 100644 index 00000000..0404940b --- /dev/null +++ b/tests/electron/preloadBundle.test.js @@ -0,0 +1,16 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); +const bundlePath = path.join(repoRoot, "electron/preload.bundle.js"); + +describe("electron/preload.bundle", () => { + it("exists and inlines shellOrigin for sandbox preload", () => { + expect(fs.existsSync(bundlePath)).toBe(true); + const source = fs.readFileSync(bundlePath, "utf8"); + expect(source).toContain("function isTrustedShellOrigin"); + expect(source).not.toContain("require(\"./shellOrigin\")"); + }); +});