feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling

This commit is contained in:
LukeGus committed 2026-09-26 00:41:25 -05:00
1 parent 7c02daccf4
commit 6cd9a2846d
242 files changed
+39127 -8558

No files matched your search

@@ -0,0 +1,85 @@
import { createRequire } from "node:module";
import { beforeEach, describe, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const linked = require("../../../../electron/linked-server.cjs") as {
setLinkedServer: (config: unknown) => void;
getLinkedServer: () => Record<string, unknown> | null;
isLinkedOrigin: (url: string) => boolean;
applyLinkHeaders: (
url: string,
headers: Record<string, string>,
) => Record<string, string>;
linkedRequestHeaders: (
extra?: Record<string, string>,
) => Record<string, string>;
answerLogin: (
authInfo: { isProxy?: boolean } | null,
url: string,
) => { username: string; password: string } | null;
};
const ORIGIN = "https://termix.example";
beforeEach(() => {
linked.setLinkedServer(null);
});
describe("linked server in Electron main", () => {
it("adds proxy headers only for the linked origin", () => {
linked.setLinkedServer({
origin: ORIGIN,
headers: [{ name: "CF-Access-Client-Id", value: "id" }],
});
expect(
linked.applyLinkHeaders(`${ORIGIN}/sync/v2/info`, { Accept: "x" }),
).toEqual({ Accept: "x", "CF-Access-Client-Id": "id" });
expect(
linked.applyLinkHeaders("https://elsewhere.example/", { Accept: "x" }),
).toEqual({ Accept: "x" });
});
it("moves a Termix bearer into the jwt cookie when basic auth is in front", () => {
linked.setLinkedServer({
origin: ORIGIN,
basicAuth: { username: "gate", password: "pw" },
});
const headers = linked.applyLinkHeaders(`${ORIGIN}/host/status`, {
authorization: "Bearer tok",
Cookie: "a=b",
});
expect(headers.Authorization).toBe(
`Basic ${Buffer.from("gate:pw").toString("base64")}`,
);
expect(headers.authorization).toBeUndefined();
expect(headers.Cookie).toBe("a=b; jwt=tok");
});
it("builds main's own request headers with the session", () => {
linked.setLinkedServer({ origin: ORIGIN, token: "tok" });
expect(linked.linkedRequestHeaders()).toEqual({
"X-Electron-App": "true",
Authorization: "Bearer tok",
});
});
it("answers basic auth challenges from the linked origin only", () => {
linked.setLinkedServer({
origin: ORIGIN,
basicAuth: { username: "gate", password: "pw" },
});
expect(linked.answerLogin({}, `${ORIGIN}/`)).toEqual({
username: "gate",
password: "pw",
});
expect(linked.answerLogin({ isProxy: true }, `${ORIGIN}/`)).toBeNull();
expect(linked.answerLogin({}, "https://other.example/")).toBeNull();
});
it("forgets everything on unlink", () => {
linked.setLinkedServer({ origin: ORIGIN, token: "tok" });
linked.setLinkedServer(null);
expect(linked.getLinkedServer()).toBeNull();
expect(linked.isLinkedOrigin(`${ORIGIN}/`)).toBe(false);
});
});
+18 -13
View File
@@ -44,8 +44,10 @@ const localFiles = require("../../../../electron/local-files.cjs") as {
TRANSFER_ROUTES: Record<string, string>;
createTargetResolver: (deps: {
localBaseUrl?: string;
getRemoteSyncConfig?: () => { serverUrl?: string } | null;
getRemoteSyncJwt?: () => string | null;
getLinkedServer?: () => {
serverUrl?: string;
token?: string | null;
} | null;
}) => (req: { origin?: unknown; route?: unknown; deviceId?: unknown }) => {
url: string;
headers: Record<string, string>;
@@ -53,8 +55,10 @@ const localFiles = require("../../../../electron/local-files.cjs") as {
createLocalFileHandlers: (deps: {
net: unknown;
shell: unknown;
getRemoteSyncConfig?: () => { serverUrl?: string } | null;
getRemoteSyncJwt?: () => string | null;
getLinkedServer?: () => {
serverUrl?: string;
token?: string | null;
} | null;
localBaseUrl?: string;
publishFs?: PublishFs;
}) => Record<
@@ -161,8 +165,10 @@ function startBackend(
describe("local-files transfer target resolution", () => {
const resolve = localFiles.createTargetResolver({
localBaseUrl: "http://127.0.0.1:30001/plugin-api/file-manager",
getRemoteSyncConfig: () => ({ serverUrl: "https://termix.example.com/" }),
getRemoteSyncJwt: () => "remote-jwt",
getLinkedServer: () => ({
serverUrl: "https://termix.example.com/",
token: "remote-jwt",
}),
});
it("only reaches the two file-manager streaming routes on the local backend", () => {
@@ -175,7 +181,7 @@ describe("local-files transfer target resolution", () => {
);
});
it("derives the remote target from the configured sync server and attaches its JWT itself", () => {
it("derives the remote target from the linked server and attaches its session itself", () => {
const target = resolve({ origin: "remote", route: "downloadFileStream" });
expect(target.url).toBe(
"https://termix.example.com/plugin-api/file-manager/downloadFileStream",
@@ -196,15 +202,15 @@ describe("local-files transfer target resolution", () => {
expect(() => resolve({})).toThrow(/Unknown transfer route/);
});
it("refuses a remote origin when no sync server is configured or it is not http(s)", () => {
it("refuses a remote origin when the device is not linked or the server is not http(s)", () => {
const unconfigured = localFiles.createTargetResolver({
getRemoteSyncConfig: () => null,
getLinkedServer: () => null,
});
expect(() =>
unconfigured({ origin: "remote", route: "uploadFileStream" }),
).toThrow(/not configured/);
).toThrow(/not linked/);
const bogus = localFiles.createTargetResolver({
getRemoteSyncConfig: () => ({ serverUrl: "file:///etc/passwd" }),
getLinkedServer: () => ({ serverUrl: "file:///etc/passwd" }),
});
expect(() =>
bogus({ origin: "remote", route: "uploadFileStream" }),
@@ -290,8 +296,7 @@ describe("local-files download boundary", () => {
net: fakeNet,
shell: {},
localBaseUrl: backend.url,
getRemoteSyncConfig: () => null,
getRemoteSyncJwt: () => null,
getLinkedServer: () => null,
});
});
afterAll(() => backend.close());
@@ -1,101 +0,0 @@
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import https from "node:https";
import type { AddressInfo } from "node:net";
import { afterAll, beforeAll, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const {
createRemoteSyncFetch,
} = require("../../../../electron/remote-sync-fetch.cjs");
let server: https.Server;
let origin: string;
let directory: string;
let redirectedRequests = 0;
const config = { serverUrl: "", allowInvalidCertificate: false };
const request = createRemoteSyncFetch(() => config);
beforeAll(async () => {
directory = mkdtempSync(join(tmpdir(), "termix-sync-tls-"));
const key = join(directory, "key.pem");
const cert = join(directory, "cert.pem");
execFileSync(
"openssl",
[
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
key,
"-out",
cert,
"-days",
"1",
"-subj",
"/CN=localhost",
],
{ stdio: "ignore" },
);
server = https.createServer(
{ key: readFileSync(key), cert: readFileSync(cert) },
(req, res) => {
if (req.url === "/redirect") {
res.writeHead(302, { Location: `${origin}/redirect-target` });
} else {
if (req.url === "/redirect-target") redirectedRequests++;
res.setHeader("Content-Type", "application/json");
res.write(
JSON.stringify({
authorization: req.headers.authorization,
method: req.method,
}),
);
}
res.end();
},
);
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
origin = `https://127.0.0.1:${(server.address() as AddressInfo).port}`;
config.serverUrl = `${origin}/termix`;
});
afterAll(async () => {
server?.closeAllConnections();
if (server)
await new Promise<void>((resolve) => server.close(() => resolve()));
if (directory) rmSync(directory, { recursive: true, force: true });
});
it("rejects self-signed certificates until enabled, then preserves authenticated requests", async () => {
await expect(request(`${origin}/users/me`)).rejects.toThrow();
config.allowInvalidCertificate = true;
const response = await request(`${origin}/users/me`, {
headers: { Authorization: "Bearer dummy-token" },
method: "POST",
body: "{}",
});
expect(await response.json()).toEqual({
authorization: "Bearer dummy-token",
method: "POST",
});
config.allowInvalidCertificate = false;
await expect(request(`${origin}/users/me`)).rejects.toThrow();
});
it("does not apply an exception to another configured origin", async () => {
config.allowInvalidCertificate = true;
config.serverUrl = "https://another.example";
await expect(request(`${origin}/sync/hosts`)).rejects.toThrow();
config.serverUrl = origin;
});
it("refuses redirects rather than carrying the TLS exception to another request", async () => {
config.allowInvalidCertificate = true;
await expect(request(`${origin}/redirect`)).rejects.toThrow();
expect(redirectedRequests).toBe(0);
});
@@ -1,112 +0,0 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { runInNewContext } from "node:vm";
import { describe, expect, it } from "vitest";
import { deserializeSyncReferences } from "../../database/routes/sync-references.js";
const filename = resolve("electron/remote-sync.cjs");
const localRequire = createRequire(filename);
const { orderSyncWrites } = localRequire("./sync-write-order.cjs");
const moduleStub = { exports: {} };
const Engine = runInNewContext(
readFileSync(filename, "utf8") + "\nRemoteSyncEngine;",
{
module: moduleStub,
require: (name: string) => (name === "electron" ? {} : localRequire(name)),
},
);
type Row = {
syncId: string;
updatedAt: string;
parentHostSyncId: string | null;
};
const local = "http://127.0.0.1:30001";
const remote = "https://remote.example";
describe("nested host sync writes", () => {
it.each([local, remote])(
"copies parents before reparented children into %s",
async (destination) => {
const oldChild: Row = {
syncId: "child",
updatedAt: "2026-01-01",
parentHostSyncId: null,
};
const parent: Row = {
syncId: "parent",
updatedAt: "2026-01-02",
parentHostSyncId: null,
};
const newChild: Row = {
...oldChild,
updatedAt: "2026-01-02",
parentHostSyncId: "parent",
};
const source = destination === local ? remote : local;
const stores = new Map<string, Map<string, Row>>([
[destination, new Map([["child", oldChild]])],
[
source,
new Map([
["parent", parent],
["child", newChild],
]),
],
]);
const engine = new Engine(() => null);
engine.localJwt = "local-token";
engine.pullSide = async (url: string) => [...stores.get(url)!.values()];
engine.pullTombstones = async () => [];
const written: string[] = [];
engine.pushRow = async (
url: string,
_token: string,
entity: "hosts",
row: Row,
) => {
await deserializeSyncReferences(entity, row, async (_type, id) =>
stores.get(url)!.has(id) ? 1 : null,
);
stores.get(url)!.set(row.syncId, row);
written.push(row.syncId);
};
await engine.syncEntity({
entityType: "hosts",
remoteBaseUrl: remote,
remoteJwt: "remote-token",
});
expect(written).toEqual(["parent", "child"]);
expect(stores.get(destination)!.get("child")!.parentHostSyncId).toBe(
"parent",
);
written.length = 0;
await engine.syncEntity({
entityType: "hosts",
remoteBaseUrl: remote,
remoteJwt: "remote-token",
});
expect(written).toEqual([]);
},
);
it("rejects cyclic write dependencies before any writes are sent", () => {
const writes = [
{ baseUrl: local, row: { syncId: "a", parentHostSyncId: "b" } },
{ baseUrl: local, row: { syncId: "b", parentHostSyncId: "a" } },
];
expect(() => orderSyncWrites("hosts", writes)).toThrow(
"Cyclic parent host",
);
});
it("does not confuse writes to different destinations", () => {
const writes = [
{ baseUrl: local, row: { syncId: "a", parentHostSyncId: "b" } },
{ baseUrl: remote, row: { syncId: "b", parentHostSyncId: "a" } },
];
expect(orderSyncWrites("hosts", writes)).toEqual(writes);
expect(orderSyncWrites("snippets", writes)).toBe(writes);
});
});