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

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import type { Request } from "express";
import type { UserRecord } from "../../../database/repositories/user-repository.js";
import {
allowsDesktopAutoSession,
isLoopbackRequest,
extractBearerOrCookieToken,
isNativeTokenExportRequest,
@@ -184,3 +185,20 @@ describe("resolveDesktopAutoSessionUser", () => {
expect(result).toBe(earliest);
});
});
describe("allowsDesktopAutoSession", () => {
it("allows the embedded desktop backend and a development backend", () => {
expect(
allowsDesktopAutoSession({
ELECTRON_EMBEDDED: "true",
NODE_ENV: "production",
}),
).toBe(true);
expect(allowsDesktopAutoSession({ NODE_ENV: "development" })).toBe(true);
expect(allowsDesktopAutoSession({})).toBe(true);
});
it("never allows it on a production server", () => {
expect(allowsDesktopAutoSession({ NODE_ENV: "production" })).toBe(false);
});
});
@@ -1,101 +0,0 @@
/**
* Host plugin settings over remote sync: non-secret values travel with the
* host, and a plugin can translate a local row id to its syncId and back.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
const h = vi.hoisted(() => ({
stored: new Map<string, unknown>(),
writes: [] as Array<[string, number, string, unknown]>,
hooks: new Map<string, unknown>(),
manifests: [] as PluginManifest[],
}));
vi.mock("../../../database/routes/host-plugin-settings.js", () => ({
hostSettingsPlugins: () => h.manifests,
}));
vi.mock("../../../plugins/registry.js", () => ({
consume: (key: string) => h.hooks.get(key),
}));
vi.mock("../../../utils/logger.js", () => ({
databaseLogger: { warn: vi.fn(), info: vi.fn(), error: vi.fn() },
}));
vi.mock("../../../plugins/settings.js", () => ({
declaredFields: (manifest: PluginManifest) =>
manifest.contributes!.settings!.host!.fields,
getAllSettings: async (
manifest: PluginManifest,
_scope: string,
id: number,
) =>
Object.fromEntries(
manifest.contributes!.settings!.host!.fields.map((field) => [
field.key,
h.stored.get(`${manifest.id}:${id}:${field.key}`),
]),
),
setSetting: async (
manifest: PluginManifest,
_scope: string,
id: number,
key: string,
value: unknown,
) => {
h.writes.push([manifest.id, id, key, value]);
return null;
},
}));
const { exportHostPluginSettings, importHostPluginSettings } =
await import("../../../database/routes/sync-host-plugin-settings.js");
const VAULT = {
id: "vault",
contributes: {
settings: {
host: {
fields: [
{ key: "profileId", type: "number", labelKey: "k" },
{ key: "token", type: "secret", labelKey: "k" },
],
},
},
},
} as unknown as PluginManifest;
beforeEach(() => {
h.stored.clear();
h.writes.length = 0;
h.hooks.clear();
h.manifests = [VAULT];
h.hooks.set("vault.hostSettingsSync", {
exportValue: (key: string, value: unknown) =>
key === "profileId" && value === 4 ? "profile-sync-4" : value,
importValue: (key: string, value: unknown) =>
key === "profileId" && value === "profile-sync-4" ? 11 : value,
});
});
describe("host plugin settings over sync", () => {
it("exports non-secret values through the plugin's translation", async () => {
h.stored.set("vault:1:profileId", 4);
h.stored.set("vault:1:token", "secret");
expect(await exportHostPluginSettings(1)).toEqual({
vault: { profileId: "profile-sync-4" },
});
});
it("imports them back to local ids", async () => {
await importHostPluginSettings(9, {
vault: { profileId: "profile-sync-4", token: "ignored" },
});
expect(h.writes).toEqual([["vault", 9, "profileId", 11]]);
});
it("ignores a payload without plugin settings", async () => {
await importHostPluginSettings(9, undefined);
expect(h.writes).toEqual([]);
});
});
@@ -1,83 +0,0 @@
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { and, eq } from "drizzle-orm";
import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core";
import { describe, expect, it } from "vitest";
import { locateSyncRow } from "../../../database/routes/sync.js";
import { hosts, userPreferences } from "../../../database/db/schema.js";
/**
* A sync push locates the stored row twice: once to see whether it exists,
* once to write it. Those lookups used to be spelled out separately, and only
* the read knew about singleton entities — the write always keyed on
* `table.id`.
*
* `user_preferences` is the only singleton, and the one synced table whose
* primary key is `user_id` with no `id` column at all. `table.id` was
* therefore `undefined` and drizzle emitted a comparison with nothing on its
* left: `( = ? and "user_preferences"."user_id" = ?)`. The insert branch was
* fine, so the first push of preferences succeeded and every push after it —
* the steady state — failed with `SqliteError: near "=": syntax error`.
*/
describe("locateSyncRow", () => {
const dialect = new SQLiteSyncDialect();
const toSql = (condition: Parameters<typeof dialect.sqlToQuery>[0]): string =>
dialect.sqlToQuery(condition).sql;
/** `= ?` with no operand to its left — what used to reach SQLite. */
const EMPTY_LEFT_OPERAND = /(^|\(|\band\b|\bor\b)\s*=\s*\?/;
it("keys a singleton entity on its owner", () => {
const sql = toSql(locateSyncRow("userPreferences", "user-1", "ignored"));
expect(sql).toContain('"user_id"');
expect(sql).not.toContain('"sync_id"');
expect(sql).not.toMatch(EMPTY_LEFT_OPERAND);
});
it("keys a regular entity on its sync id and owner", () => {
const sql = toSql(locateSyncRow("hosts", "user-1", "sync-abc"));
expect(sql).toContain('"sync_id"');
expect(sql).toContain('"user_id"');
expect(sql).not.toMatch(EMPTY_LEFT_OPERAND);
});
it("is the shape the id-keyed lookup could not produce", () => {
// Guards the assertions above: the pattern really does catch the old SQL.
const previous = and(
eq((userPreferences as unknown as typeof hosts).id, 1),
eq(userPreferences.userId, "user-1"),
)!;
expect(toSql(previous)).toMatch(EMPTY_LEFT_OPERAND);
});
it("updates a preferences row that already exists", () => {
const sqlite = new Database(":memory:");
sqlite.exec(`
CREATE TABLE user_preferences (
user_id TEXT PRIMARY KEY,
theme TEXT
);
`);
const db = drizzle(sqlite, { schema: { userPreferences } });
// The steady state: a row exists, so the push takes the update path.
sqlite
.prepare("INSERT INTO user_preferences (user_id, theme) VALUES (?, ?)")
.run("user-1", "dark");
const updated = db
.update(userPreferences)
.set({ theme: "light" })
.where(locateSyncRow("userPreferences", "user-1", "ignored"))
.returning({ theme: userPreferences.theme })
.all();
expect(updated).toEqual([{ theme: "light" }]);
sqlite.close();
});
});
@@ -1,116 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import syncRouter, {
isSyncedRow,
isValidEntityType,
stripWritePayload,
} from "../../../database/routes/sync.js";
import { registerEntity } from "../../../plugins/sync-registry.js";
describe("sync route order", () => {
it("registers POST /tombstones before the POST /:entityType wildcard", () => {
const postPaths = (
syncRouter as unknown as {
stack: Array<{ route?: { path: string; methods: { post?: boolean } } }>;
}
).stack
.filter((layer) => layer.route?.methods?.post)
.map((layer) => layer.route!.path);
// "/tombstones" is a valid value for :entityType as far as Express is
// concerned, so registering the wildcard first makes the tombstone
// endpoint unreachable -- every deletion push answers 400 "Unknown entity
// type" instead of applying the deletion.
expect(postPaths).toContain("/tombstones");
expect(postPaths.indexOf("/tombstones")).toBeLessThan(
postPaths.indexOf("/:entityType"),
);
});
});
describe("isValidEntityType", () => {
it("accepts every whitelisted sync entity type", () => {
for (const type of [
"hosts",
"sshCredentials",
"sshFolders",
"userPreferences",
]) {
expect(isValidEntityType(type)).toBe(true);
}
});
it("rejects unknown or non-string entity types", () => {
expect(isValidEntityType("hostAccess")).toBe(false);
expect(isValidEntityType("")).toBe(false);
expect(isValidEntityType(undefined)).toBe(false);
expect(isValidEntityType(42)).toBe(false);
});
});
describe("stripWritePayload", () => {
it("strips id, userId, and syncId from every entity type", () => {
const payload = {
id: 1,
userId: "user-1",
syncId: "abc",
name: "prod-db",
};
expect(stripWritePayload("sshFolders", payload)).toEqual({
name: "prod-db",
});
});
it("also strips desktop-only fields flagged read-only for hosts", () => {
const payload = {
id: 1,
userId: "user-1",
syncId: "abc",
name: "web",
connectionOrigin: "remote",
};
expect(stripWritePayload("hosts", payload)).toEqual({ name: "web" });
});
it("keeps preference storage mode local to each device", () => {
expect(
stripWritePayload("userPreferences", {
syncId: "userPreferences:singleton",
theme: "dark",
storageMode: "cloud",
}),
).toEqual({ theme: "dark" });
});
it("does not mutate the original payload object", () => {
const payload = { id: 1, userId: "user-1", syncId: "abc", name: "x" };
stripWritePayload("sshFolders", payload);
expect(payload).toEqual({
id: 1,
userId: "user-1",
syncId: "abc",
name: "x",
});
});
});
describe("isSyncedRow", () => {
let dispose: (() => void) | null = null;
afterEach(() => {
dispose?.();
dispose = null;
});
it("syncs every row of an entity that does not filter", () => {
expect(isSyncedRow("hosts", { id: 1 })).toBe(true);
});
it("leaves out the rows an entity's shouldSync refuses", () => {
dispose = registerEntity("demo", {
type: "demoLayouts",
table: {},
shouldSync: (row) => row.kind !== "local",
});
expect(isSyncedRow("demoLayouts", { kind: "shared" })).toBe(true);
expect(isSyncedRow("demoLayouts", { kind: "local" })).toBe(false);
});
});