From f803bfc140802bf337f89041e59a23808b9113bc Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 9 Oct 2026 07:37:46 -0500 Subject: [PATCH] fix: bundled plugins path for source runs and backend cleanup --- electron/main.cjs | 4 +-- src/backend/database/database.ts | 4 +-- src/backend/database/db/connect.ts | 6 ++--- src/backend/database/db/dialect.ts | 2 +- src/backend/database/db/index.ts | 4 +-- src/backend/database/db/migrate.ts | 2 +- src/backend/database/db/schema.mysql.ts | 10 +++---- src/backend/database/db/schema.pg.ts | 10 +++---- src/backend/database/db/schema.ts | 6 ++--- .../repositories/audit-log-repository.ts | 6 ++--- .../database/repositories/database-context.ts | 6 ++--- src/backend/database/repositories/factory.ts | 10 +++---- .../repositories/host-folder-repository.ts | 2 +- .../database/repositories/mutation-result.ts | 16 +++++------ .../database/repositories/returning.ts | 24 ++++++++--------- .../database/repositories/settings-cache.ts | 4 +-- .../database/repositories/user-repository.ts | 4 +-- .../routes/credential-sidebar-preferences.ts | 2 +- src/backend/database/routes/open-tabs.ts | 16 +++++------ src/backend/database/routes/plugins.ts | 7 +++-- src/backend/hosts/host-identity.ts | 2 +- src/backend/hosts/host-resolver.ts | 2 +- .../hosts/protocol-auth/protocol-auth.ts | 16 +---------- src/backend/hosts/ssh-connection-pool.ts | 2 +- src/backend/plugins/auto-update.ts | 5 ---- src/backend/plugins/paths.ts | 27 +++++++------------ src/backend/plugins/ws.ts | 4 --- src/backend/sync/server/feed.ts | 5 ---- src/backend/sync/store.ts | 6 ----- .../db/audit-log-user-id-nullable.test.ts | 4 +-- .../db/bootstrap-covers-all-tables.test.ts | 4 +-- .../tests/database/db/multi-dialect.test.ts | 4 +-- .../db/share-ssh-auth-backfill.test.ts | 2 +- .../ssh-credentials-username-rebuild.test.ts | 2 +- .../repositories/mutation-result.test.ts | 4 +-- .../database/repositories/returning.test.ts | 4 +-- .../settings-cache-refresh.test.ts | 6 ++--- .../database/repositories/test-support.ts | 14 +++++----- src/backend/tests/hosts/host-identity.test.ts | 2 +- .../tests/hosts/host-resolver-sync-id.test.ts | 2 +- src/backend/tests/plugins/paths.test.ts | 24 +++++++++++++++++ .../tests/utils/audit-forwarder.test.ts | 4 +-- src/backend/utils/audit-forwarder.ts | 2 +- src/backend/utils/compression-config.ts | 4 +-- .../crypto-migration/legacy-share-cleanup.ts | 2 +- src/backend/utils/data-crypto.ts | 2 +- src/backend/utils/data-dir-guard.ts | 2 +- src/backend/utils/error-message.ts | 2 +- src/backend/utils/permission-manager.ts | 2 +- src/backend/utils/ssh-algorithms.ts | 2 +- src/backend/utils/system-secret-crypto.ts | 2 +- 51 files changed, 147 insertions(+), 163 deletions(-) create mode 100644 src/backend/tests/plugins/paths.test.ts diff --git a/electron/main.cjs b/electron/main.cjs index ef89d4f5c..05ebbdd1f 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -1041,7 +1041,7 @@ function getBackendPaths() { backendCwd: backendDir, }; } - // fork() does not go through Electron's asar redirector — use the unpacked path. + // fork() does not go through Electron's asar redirector, use the unpacked path. // On macOS multi-arch builds (mergeASARs: false), electron-builder names the ASAR // app-arm64.asar / app-x64.asar instead of app.asar, so match all variants. const unpackedRoot = getUnpackedAppRoot(appRoot); @@ -1303,7 +1303,7 @@ function createTray() { try { const { nativeImage } = require("electron"); - // Native APIs (Tray, nativeImage) can't load files from inside app.asar — + // Native APIs (Tray, nativeImage) can't load files from inside app.asar, // use the unpacked path so the OS sees a real file. const publicRoot = isDev ? path.join(appRoot, "public") diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index c684918ff..6f6a1c087 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -280,12 +280,12 @@ app.get("/.well-known/acme-challenge/:token", acmeChallengeHandler); * remoteVersion: * type: string * description: Newest release version on the channel. Present only when the update check succeeds. - * example: 2.8.0 + * example: 26.10.0 * version: * type: string * deprecated: true * description: Legacy alias of remoteVersion, not the running instance's version. Present only when the update check succeeds. Use localVersion for the instance or remoteVersion for the latest release. - * example: 2.8.0 + * example: 26.10.0 * latest_release: * type: object * description: GitHub release metadata. Present only when the update check succeeds. diff --git a/src/backend/database/db/connect.ts b/src/backend/database/db/connect.ts index 450704081..91213dd47 100644 --- a/src/backend/database/db/connect.ts +++ b/src/backend/database/db/connect.ts @@ -50,7 +50,7 @@ export function sslOption( /** * Opens a connection to a client-server engine. * - * SQLite is not handled here — it has its own lifecycle in db/index.ts, where + * SQLite is not handled here, it has its own lifecycle in db/index.ts, where * the database is decrypted into memory and serialised back to a file. This * covers the engines that connect to something already running. * @@ -109,7 +109,7 @@ export async function connectRemoteDatabase( // No `schema` option: it only feeds drizzle's relational query API // (`db.query.*`), which nothing here uses. The query builder takes its table - // names and value encoders from the table objects the repositories import — + // names and value encoders from the table objects the repositories import, // see the note in schema.pg.ts on why the generated schemas are DDL-only. if (dialect === "postgres") { const { drizzle } = await import("drizzle-orm/node-postgres"); @@ -119,7 +119,7 @@ export async function connectRemoteDatabase( } // mysql2 names the pool limit differently and wants no `ssl` key at all when - // TLS is off — passing false is not the same as omitting it. + // TLS is off, passing false is not the same as omitting it. const { drizzle } = await import("drizzle-orm/mysql2"); return drizzle({ connection: { diff --git a/src/backend/database/db/dialect.ts b/src/backend/database/db/dialect.ts index 171017b56..c8b0b149f 100644 --- a/src/backend/database/db/dialect.ts +++ b/src/backend/database/db/dialect.ts @@ -42,7 +42,7 @@ export function resolveDatabaseDialect( * * SQLite here is an in-memory database serialised back to an encrypted file, so * every write needs a trigger to flush it. Client-server engines have already - * durably committed by the time the query returns — there is no file to write + * durably committed by the time the query returns, there is no file to write * and nothing to schedule. */ export function needsExplicitPersist(dialect: DatabaseDialect): boolean { diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 681baf700..6dd9d9b06 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1103,7 +1103,7 @@ const migrateSchema = () => { // Derive the replacement table from the live definition instead of // restating it here. The table keeps gaining columns (cert_public_key, // pin, sort_order, sync_id, ...), and a second copy of the column list - // falls behind every time one is added — leaving the copy narrower than + // falls behind every time one is added, leaving the copy narrower than // the table, so the INSERT below fails and the constraint stays put. const createSql = sqlite .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'ssh_credentials'") @@ -2108,7 +2108,7 @@ export function getSqlite(): Database.Database { if (dialect !== "sqlite") { throw new Error( `No SQLite handle: DATABASE_DIALECT is "${dialect}". This caller needs a ` + - `synchronous query, which only SQLite offers — give it an async path instead.`, + `synchronous query, which only SQLite offers. Give it an async path instead.`, ); } throw new Error( diff --git a/src/backend/database/db/migrate.ts b/src/backend/database/db/migrate.ts index 57d999124..86f32f393 100644 --- a/src/backend/database/db/migrate.ts +++ b/src/backend/database/db/migrate.ts @@ -25,7 +25,7 @@ export function migrationsFolder( * Brings a remote database up to the current schema. * * drizzle's migrator records what it has applied in its own table, so this is - * safe to run on every start — including against a database another instance + * safe to run on every start, including against a database another instance * already migrated. */ export async function runRemoteMigrations( diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index 6800acd95..2fd548d39 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -1,10 +1,10 @@ -// GENERATED FILE — do not edit. +// GENERATED FILE, do not edit. // // Produced from schema.ts by scripts/generate-dialect-schema.cjs. // Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`. // Target dialect: mysql. // -// DDL source for drizzle-kit. NOT imported to run queries — repositories use +// DDL source for drizzle-kit. NOT imported to run queries. Repositories use // schema.ts on every dialect. See the generator header for why that is correct. import { @@ -542,7 +542,7 @@ export const sharedHostSecrets = mysqlTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. (table) => [ uniqueIndex("idx_shared_host_secrets_scope").on( table.hostAccessId, @@ -592,7 +592,7 @@ export const userRoles = mysqlTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. // // The unique pair already serves lookups by user, since user_id leads it. // Listing a role's members starts from role_id, which it cannot serve. @@ -1060,7 +1060,7 @@ export const pluginRegistries = mysqlTable("plugin_registries", { /** * Install counts populated by a background job (GitHub release download * counts, aggregated telemetry, or a manual override) rather than by the - * install/uninstall actions themselves — kept separate from `plugins` so + * install/uninstall actions themselves, kept separate from `plugins` so * that job can overwrite counts without touching install state. */ export const pluginInstallCounts = mysqlTable( diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index bb5faa0a0..bf1d61060 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -1,10 +1,10 @@ -// GENERATED FILE — do not edit. +// GENERATED FILE, do not edit. // // Produced from schema.ts by scripts/generate-dialect-schema.cjs. // Edit the sqlite schema and re-run `node scripts/generate-dialect-schema.cjs`. // Target dialect: postgres. // -// DDL source for drizzle-kit. NOT imported to run queries — repositories use +// DDL source for drizzle-kit. NOT imported to run queries. Repositories use // schema.ts on every dialect. See the generator header for why that is correct. import { @@ -542,7 +542,7 @@ export const sharedHostSecrets = pgTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. (table) => [ uniqueIndex("idx_shared_host_secrets_scope").on( table.hostAccessId, @@ -592,7 +592,7 @@ export const userRoles = pgTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. // // The unique pair already serves lookups by user, since user_id leads it. // Listing a role's members starts from role_id, which it cannot serve. @@ -1060,7 +1060,7 @@ export const pluginRegistries = pgTable("plugin_registries", { /** * Install counts populated by a background job (GitHub release download * counts, aggregated telemetry, or a manual override) rather than by the - * install/uninstall actions themselves — kept separate from `plugins` so + * install/uninstall actions themselves, kept separate from `plugins` so * that job can overwrite counts without touching install state. */ export const pluginInstallCounts = pgTable( diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index cbada5b30..8b5a50a7f 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -534,7 +534,7 @@ export const sharedHostSecrets = sqliteTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. (table) => [ uniqueIndex("idx_shared_host_secrets_scope").on( table.hostAccessId, @@ -584,7 +584,7 @@ export const userRoles = sqliteTable( }, // Declared inline in the production DDL as UNIQUE(...), but never here, // so the generated Postgres and MySQL schemas allowed duplicates the - // SQLite deployment forbids — and the upsert had nothing to conflict on. + // SQLite deployment forbids, and the upsert had nothing to conflict on. // // The unique pair already serves lookups by user, since user_id leads it. // Listing a role's members starts from role_id, which it cannot serve. @@ -1058,7 +1058,7 @@ export const pluginRegistries = sqliteTable("plugin_registries", { /** * Install counts populated by a background job (GitHub release download * counts, aggregated telemetry, or a manual override) rather than by the - * install/uninstall actions themselves — kept separate from `plugins` so + * install/uninstall actions themselves, kept separate from `plugins` so * that job can overwrite counts without touching install state. */ export const pluginInstallCounts = sqliteTable( diff --git a/src/backend/database/repositories/audit-log-repository.ts b/src/backend/database/repositories/audit-log-repository.ts index 2da752768..21e898b38 100644 --- a/src/backend/database/repositories/audit-log-repository.ts +++ b/src/backend/database/repositories/audit-log-repository.ts @@ -74,7 +74,7 @@ export class AuditLogRepository { /** * Cached row count backing the cap check. Static because the factory builds * a repository per call, so a per-instance count would never survive to be - * reused. Null means "unknown, re-read" — which is also how any path that + * reused. Null means "unknown, re-read", which is also how any path that * deletes rows invalidates it. */ private static cachedCount: number | null = null; @@ -233,13 +233,13 @@ export class AuditLogRepository { * * Both passes used to run inline on every insert: a retention DELETE plus a * COUNT over the whole table, thousands of times an hour on a busy install, - * almost always to find nothing to do — and audit writes sit in the request + * almost always to find nothing to do, and audit writes sit in the request * path of the actions they record. * * They are split by what they cost and what they guarantee. Retention is * time-based, so nothing is lost by checking it on an interval. The row cap * is a disk-space guard that has to react to inserts, so it is still checked - * on the write that crosses it — but against a cached count, so the common + * on the write that crosses it, but against a cached count, so the common * case is an integer compare rather than a COUNT. */ private async pruneIfDue(): Promise { diff --git a/src/backend/database/repositories/database-context.ts b/src/backend/database/repositories/database-context.ts index 4f0bb476a..03eb07a3b 100644 --- a/src/backend/database/repositories/database-context.ts +++ b/src/backend/database/repositories/database-context.ts @@ -2,7 +2,7 @@ import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3"; import type * as schema from "../db/schema.js"; // Re-exported so repositories can keep importing it from here, but defined in -// db/dialect.ts — a local copy that said "sqlite" survived here for a while and +// db/dialect.ts, a local copy that said "sqlite" survived here for a while and // typed every context as SQLite-only while the runtime already carried all // three, which silently made the dialect branches unreachable to the checker. export type { DatabaseDialect } from "../db/dialect.js"; @@ -18,7 +18,7 @@ import type { DatabaseDialect } from "../db/dialect.js"; * * This is a deliberate approximation, not an accident. The query-builder * surface the repositories actually use is the same on all three engines, and - * that equivalence is asserted in multi-dialect.test.ts rather than assumed — + * that equivalence is asserted in multi-dialect.test.ts rather than assumed, * identifier quoting, placeholder style and value coercion are all covered * there. At runtime this may hold a Postgres or MySQL instance. * @@ -32,7 +32,7 @@ export type PortableDatabase = BetterSQLite3Database; * * Deliberately drizzle-only: with no raw driver handle here, no repository can * reach for engine-specific SQL. Retention queries that previously needed - * `datetime('now', ?)` compute their cutoff in JS instead — see + * `datetime('now', ?)` compute their cutoff in JS instead, see * ./sql-timestamp.ts. */ export interface DatabaseContext { diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index c38931820..2c5fdb800 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -49,7 +49,7 @@ import { UserRepository } from "./user-repository.js"; * write. * * Both cross-dialect harnesses build a DatabaseContext themselves, so neither - * exercises this function — see tests/database/repositories/factory-context. + * exercises this function, see tests/database/repositories/factory-context. */ export function createCurrentRepositoryContext(): DatabaseContext { return { @@ -64,7 +64,7 @@ export function createCurrentRepositoryContext(): DatabaseContext { * Only meaningful for SQLite, where the database lives in memory and has to be * serialised back to its encrypted file. On Postgres and MySQL the write is * already durable, so no hook is installed at all rather than one that does - * nothing — repositories call it as `this.onWrite?.()`. + * nothing, repositories call it as `this.onWrite?.()`. */ export function createCurrentRepositoryWriteHook( reason: string, @@ -91,7 +91,7 @@ export function createCurrentRepositoryLazyWriteHook( } /** - * Raw driver handle for the few synchronous call sites that cannot await — + * Raw driver handle for the few synchronous call sites that cannot await, * getCurrentSettingValue below, and settings reads during startup. Repositories * must not use this: they take a DatabaseContext, which is drizzle-only. * Porting to another engine means giving these callers an async path first. @@ -403,8 +403,8 @@ let refreshTimer: NodeJS.Timeout | null = null; * * The cache is per-process and updated in the process that writes. That is * enough for SQLite, where there is only ever one process. On Postgres and - * MySQL — which exist here precisely so more than one instance can share the - * data — a setting changed on one replica would otherwise never reach the + * MySQL, which exist here precisely so more than one instance can share the + * data, a setting changed on one replica would otherwise never reach the * others, because the synchronous read has no way to go back to the database. * * Periodic re-priming does not make the value immediately consistent. It bounds diff --git a/src/backend/database/repositories/host-folder-repository.ts b/src/backend/database/repositories/host-folder-repository.ts index 2330084fe..9f561bad7 100644 --- a/src/backend/database/repositories/host-folder-repository.ts +++ b/src/backend/database/repositories/host-folder-repository.ts @@ -31,7 +31,7 @@ export class HostFolderRepository { now = new Date().toISOString(), ): Promise { // CAST target: every engine spells the text type differently enough to - // matter here — MySQL has no `text` cast and wants `char`. + // matter here, MySQL has no `text` cast and wants `char`. const textType = this.context.dialect === "mysql" ? "char" : "text"; const oldPrefix = `${oldName} / `; const newPrefix = `${newName} / `; diff --git a/src/backend/database/repositories/mutation-result.ts b/src/backend/database/repositories/mutation-result.ts index d849c7746..2ea47aa2f 100644 --- a/src/backend/database/repositories/mutation-result.ts +++ b/src/backend/database/repositories/mutation-result.ts @@ -4,18 +4,18 @@ import type { DatabaseDialect } from "../db/dialect.js"; * Reading the outcome of a write without depending on RETURNING. * * SQLite and Postgres can attach `.returning()` to a delete or update and get - * the affected rows back. **MySQL cannot** — it has no RETURNING clause, and + * the affected rows back. **MySQL cannot**: it has no RETURNING clause, and * drizzle's mysql-core does not expose the method at all, so the call is a * TypeError rather than a bad query. 175 call sites here read a write's result, * so the difference has to be absorbed somewhere. * * The split that matters is what the caller actually needs: * - * - **How many rows changed** — the majority, and none of them need the rows. + * - **How many rows changed**: the majority, and none of them need the rows. * They used to ask for them anyway, via `.returning().length`. Dropping the * `.returning()` and reading the driver's own count is both portable and one * less thing for the database to send back. - * - **The rows themselves** — cannot be emulated on MySQL without reading + * - **The rows themselves**: cannot be emulated on MySQL without reading * first, which needs a transaction to stay correct under concurrency. Those * call sites are handled individually rather than behind a helper that hides * an extra round trip. @@ -33,7 +33,7 @@ import type { DatabaseDialect } from "../db/dialect.js"; * | node-postgres | `{ rowCount, rows, command }` | * | mysql2 | `[{ affectedRows, insertId }, fields]` | * - * These are the shapes returned when NO `.returning()` is attached — which is + * These are the shapes returned when NO `.returning()` is attached, which is * the portable way to write, since MySQL has no RETURNING clause at all. */ interface WriteHeader { @@ -47,7 +47,7 @@ interface WriteHeader { const COUNT_FIELDS = ["changes", "rowCount", "affectedRows"] as const; /** - * mysql2 hands back `[ResultSetHeader, fields]`, which is itself an array — so + * mysql2 hands back `[ResultSetHeader, fields]`, which is itself an array, so * "is it an array" cannot distinguish a write header from a returning() result. * The header is identified by carrying one of the fields above instead. */ @@ -70,7 +70,7 @@ function asWriteHeader(result: unknown): WriteHeader | null { /** * Number of rows a write touched. * - * Pass the result of the write itself — every driver's header is understood, so + * Pass the result of the write itself, every driver's header is understood, so * the caller neither branches on the dialect nor attaches `.returning()` just to * count what came back. * @@ -97,7 +97,7 @@ export function rowsAffected(result: unknown): number { * * **Only meaningful on the result of an insert.** SQLite's `lastInsertRowid` and * MySQL's `insertId` are connection-level values that survive the statement that - * set them — after a delete, SQLite still reports whatever the last insert + * set them, after a delete, SQLite still reports whatever the last insert * produced. Passing an update or delete result here gets a stale id, not null. * * Returns null when the table has no autoincrement key. @@ -143,7 +143,7 @@ export function supportsReturning(dialect: DatabaseDialect): boolean { * * `sql` is a type assertion, not a conversion. Postgres returns COUNT() * as bigint, which node-postgres hands back as a **string** so that values past - * 2^53 survive — so the annotation is a lie there and comparisons like + * 2^53 survive, so the annotation is a lie there and comparisons like * `count < max` compare a string to a number. */ export function countValue(value: unknown): number { diff --git a/src/backend/database/repositories/returning.ts b/src/backend/database/repositories/returning.ts index fe2e05b14..e97ae4add 100644 --- a/src/backend/database/repositories/returning.ts +++ b/src/backend/database/repositories/returning.ts @@ -11,15 +11,15 @@ import { * Writes that need the affected rows back. * * `mutation-result.ts` covers the call sites that only wanted a count. These are - * the ones that genuinely read the rows — an updated record to return to the + * the ones that genuinely read the rows, an updated record to return to the * caller, a deleted row's fields to clean up alongside it. * * SQLite and Postgres do this in one statement with RETURNING. MySQL has no * such clause, so the read is a second statement, and the pair has to be atomic: * - * - **insert** — write, then read the row back by its key. - * - **update** — write, then read. Reading first would return the old values. - * - **delete** — read, then write. Reading after would return nothing. + * - **insert**: write, then read the row back by its key. + * - **update**: write, then read. Reading first would return the old values. + * - **delete**: read, then write. Reading after would return nothing. * * Both run in a transaction. Without one, a concurrent write between the two * statements makes the returned rows describe a state that never existed, and @@ -29,7 +29,7 @@ import { * ## The trap, and why it cannot bite silently * * On MySQL the update path re-reads using the same `where`. If the update - * changes a column that `where` tests, the read finds nothing — SQLite would + * changes a column that `where` tests, the read finds nothing, SQLite would * have returned the row. Every current caller filters on an id it does not * modify, but that is a convention, not a guarantee, so the mismatch is * detected and thrown rather than returned as an empty array. Same for an @@ -40,7 +40,7 @@ import { */ /** - * What `.set()` accepts: a column's own type, or a SQL expression in its place — + * What `.set()` accepts: a column's own type, or a SQL expression in its place, * `updatedAt: sql`CURRENT_TIMESTAMP`` is the common one here. */ type UpdateValues = { @@ -68,7 +68,7 @@ export async function updateReturning( const rows = await tx.select().from(table).where(where); // The trap this catches: if the update changed a column that `where` tests, - // the read finds nothing and the caller gets [] — on MySQL only, with no + // the read finds nothing and the caller gets [], on MySQL only, with no // error, where SQLite would have returned the row. Rows changed but none // readable back is exactly that case, so make it loud instead. if (rows.length === 0 && rowsAffected(written) > 0) { @@ -108,10 +108,10 @@ type Keyed = SQLiteTable & { id: SQLiteColumn }; /** * Inserts one row and returns it as stored, including whatever the database - * filled in — defaults, an autoincrement id, a CURRENT_TIMESTAMP. + * filled in, defaults, an autoincrement id, a CURRENT_TIMESTAMP. * * This is the one case Postgres cannot shortcut either: without RETURNING there - * is no id to read back by. Hence the split is genuinely three-way — except + * is no id to read back by. Hence the split is genuinely three-way, except * that sqlite and pg both have RETURNING, so it collapses to two again. * * On MySQL the key comes from one of two places: @@ -161,7 +161,7 @@ export async function insertReturning( * back by an explicit condition. * * `user_preferences` is keyed by `userId` and has no `id` column at all, so - * there is no insertId to read back by — the caller has to say what identifies + * there is no insertId to read back by, the caller has to say what identifies * the row it just wrote. */ export async function insertReturningWhere( @@ -195,13 +195,13 @@ export async function insertReturningWhere( * * The clause has three spellings. SQLite and Postgres take * `ON CONFLICT (cols) DO UPDATE`; **MySQL takes `ON DUPLICATE KEY UPDATE` and - * names no columns** — it uses whichever unique key was violated. drizzle + * names no columns**: it uses whichever unique key was violated. drizzle * follows suit, so `onConflictDoUpdate` does not exist on mysql-core at all and * calling it is a TypeError rather than a rejected query. * * The conflict target still has to be passed: it is what SQLite and Postgres * need, and stating it keeps the caller honest about which unique constraint it - * is relying on — four of those were missing from the schema entirely until the + * is relying on, four of those were missing from the schema entirely until the * cross-dialect tests went looking. */ export async function upsert( diff --git a/src/backend/database/repositories/settings-cache.ts b/src/backend/database/repositories/settings-cache.ts index a081c38f4..19c9a6273 100644 --- a/src/backend/database/repositories/settings-cache.ts +++ b/src/backend/database/repositories/settings-cache.ts @@ -1,7 +1,7 @@ /** * Synchronous read-through cache for the settings table. * - * 27 call sites read settings synchronously — during startup, inside request + * 27 call sites read settings synchronously, during startup, inside request * handlers, and from the guacd server bootstrap. On SQLite that works because * better-sqlite3 is synchronous; on Postgres or MySQL there is no synchronous * query at all, and making all 27 async would push `await` through code paths @@ -35,7 +35,7 @@ export function primeSettingsCache( /** * Reads a cached setting. * - * Returns null both for "not set" and "cache not primed yet" — every caller + * Returns null both for "not set" and "cache not primed yet", every caller * already treats a missing setting as "use the default", and startup ordering * means a read before priming should behave the same way rather than throw. */ diff --git a/src/backend/database/repositories/user-repository.ts b/src/backend/database/repositories/user-repository.ts index a04942637..f5c148a66 100644 --- a/src/backend/database/repositories/user-repository.ts +++ b/src/backend/database/repositories/user-repository.ts @@ -141,8 +141,8 @@ export class UserRepository { * at once would otherwise both see an empty table and both become admin. * * The two branches are not a style choice. better-sqlite3 is synchronous and - * rejects an async transaction callback outright — "Transaction function - * cannot return a promise" — so a single body cannot serve both. It fails + * rejects an async transaction callback outright, "Transaction function + * cannot return a promise", so a single body cannot serve both. It fails * loudly rather than silently skipping the write, which is the one mercy here. */ private async createCheckingIfFirst( diff --git a/src/backend/database/routes/credential-sidebar-preferences.ts b/src/backend/database/routes/credential-sidebar-preferences.ts index cca086f41..2a5864a04 100644 --- a/src/backend/database/routes/credential-sidebar-preferences.ts +++ b/src/backend/database/routes/credential-sidebar-preferences.ts @@ -17,7 +17,7 @@ const authenticateJWT = authManager.createAuthMiddleware(); * /credential-sidebar/preferences: * get: * summary: Get the credential sidebar preferences for the current user - * description: Returns the current user's saved credential sidebar preferences (sort, filters, open folders, display settings). Unlike /host-sidebar/preferences, there is no legacy-column migration to perform here — credentials never had exploded preference columns on userPreferences — so a first-time GET simply returns the defaults without writing a row; a row is only created once the user actually changes something via PUT. + * description: Returns the current user's saved credential sidebar preferences (sort, filters, open folders, display settings). Unlike /host-sidebar/preferences, there is no legacy-column migration to perform here, credentials never had exploded preference columns on userPreferences, so a first-time GET simply returns the defaults without writing a row; a row is only created once the user actually changes something via PUT. * tags: * - Credential Sidebar * responses: diff --git a/src/backend/database/routes/open-tabs.ts b/src/backend/database/routes/open-tabs.ts index a58ff228f..a42f28aea 100644 --- a/src/backend/database/routes/open-tabs.ts +++ b/src/backend/database/routes/open-tabs.ts @@ -9,6 +9,14 @@ const router = express.Router(); const authManager = AuthManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); +const DEFAULT_TAB_TTL_MINUTES = 30; + +// Tabs live as long as the terminal keeps a detached session. +function getTabTtlMs(): number { + const minutes = liveTerminalSessions.idleTimeoutMinutes(); + return (minutes && minutes > 0 ? minutes : DEFAULT_TAB_TTL_MINUTES) * 60_000; +} + /** * @openapi * /open-tabs: @@ -20,14 +28,6 @@ const authenticateJWT = authManager.createAuthMiddleware(); * 200: * description: List of open tabs ordered by tab_order. */ -const DEFAULT_TAB_TTL_MINUTES = 30; - -// Tabs live as long as the terminal keeps a detached session. -function getTabTtlMs(): number { - const minutes = liveTerminalSessions.idleTimeoutMinutes(); - return (minutes && minutes > 0 ? minutes : DEFAULT_TAB_TTL_MINUTES) * 60_000; -} - router.get("/", authenticateJWT, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; try { diff --git a/src/backend/database/routes/plugins.ts b/src/backend/database/routes/plugins.ts index 61a7e5d92..70eff9cbb 100644 --- a/src/backend/database/routes/plugins.ts +++ b/src/backend/database/routes/plugins.ts @@ -17,7 +17,7 @@ import { describePluginFrontend } from "../../plugins/assets.js"; import { invalidatePluginPermissionCache } from "../../plugins/permissions.js"; import { getPluginPublicHttpRoutes } from "../../plugins/http.js"; import { getPluginPublicWsRoutes } from "../../plugins/ws.js"; -import { PluginManageError } from "../../plugins/manage.js"; +import { MAX_UPLOAD_BYTES, PluginManageError } from "../../plugins/manage.js"; import { parsePluginChannel } from "../../plugins/registry-index.js"; import { pluginDocsUrl, @@ -2262,7 +2262,10 @@ router.post( "/upload", authenticateJWT, requireManagePlugins, - express.raw({ type: "application/octet-stream", limit: "50mb" }), + express.raw({ + type: "application/octet-stream", + limit: MAX_UPLOAD_BYTES, + }), async (req: Request, res: Response) => { if (await refuseOnLinkedDesktop(res)) return; if (!Buffer.isBuffer(req.body) || req.body.length === 0) { diff --git a/src/backend/hosts/host-identity.ts b/src/backend/hosts/host-identity.ts index b4314a687..436d78549 100644 --- a/src/backend/hosts/host-identity.ts +++ b/src/backend/hosts/host-identity.ts @@ -16,7 +16,7 @@ export function normalizeHostAddress(value: unknown): string { * A client addresses a host by the numeric row id of the database it is * displaying. When the desktop app delegates a connection to a remote sync * server, the id is resolved against *that* server's table instead, and - * autoincrement ids need not line up between the two — they diverge as soon as + * autoincrement ids need not line up between the two, they diverge as soon as * the sides accumulate inserts and deletes in a different order. * * The resolved row then supplies the address, the credentials, the jump hosts diff --git a/src/backend/hosts/host-resolver.ts b/src/backend/hosts/host-resolver.ts index 27b871956..4ab9f037e 100644 --- a/src/backend/hosts/host-resolver.ts +++ b/src/backend/hosts/host-resolver.ts @@ -25,7 +25,7 @@ const sshLogger = logger; * `id` is an autoincrement belonging to whichever database produced the row. * When the desktop app delegates a connection to a sync server, the two * sequences have no reason to agree, and resolving the client's id here lands - * on whatever host happens to own that number — a different machine, with its + * on whatever host happens to own that number, a different machine, with its * own address, credentials and host key. `syncId` is the same string on both * sides, so it names the host the user actually picked. * diff --git a/src/backend/hosts/protocol-auth/protocol-auth.ts b/src/backend/hosts/protocol-auth/protocol-auth.ts index cd8963bf2..50ccb8c09 100644 --- a/src/backend/hosts/protocol-auth/protocol-auth.ts +++ b/src/backend/hosts/protocol-auth/protocol-auth.ts @@ -8,10 +8,7 @@ * needs no core change. */ -import { - createCurrentHostProtocolAuthRepository, - createCurrentSharedHostAuthOverrideRepository, -} from "../../database/repositories/factory.js"; +import { createCurrentHostProtocolAuthRepository } from "../../database/repositories/factory.js"; import { decryptProtocolLogin, type HostProtocolAuthRecord, @@ -605,17 +602,6 @@ export async function resolveRecipientProtocolLogin( }; } -/** Credential ids a recipient chose for this host, per protocol. */ -export async function listRecipientOverrideIds( - hostId: number, - userId: string, -): Promise> { - return (await createCurrentSharedHostAuthOverrideRepository().listCredentialIds( - hostId, - userId, - )) as Record; -} - /** A login in an export or on the sync wire: plaintext, keyed by protocol. */ export interface PortableProtocolLogin { authType: string; diff --git a/src/backend/hosts/ssh-connection-pool.ts b/src/backend/hosts/ssh-connection-pool.ts index b4cf0b915..d6816b0a9 100644 --- a/src/backend/hosts/ssh-connection-pool.ts +++ b/src/backend/hosts/ssh-connection-pool.ts @@ -169,7 +169,7 @@ export class SSHConnectionPool { if (available) { if (!this.isConnectionHealthy(available.client)) { connections = this.removeUnhealthy(key, connections, available); - // Fall through — may create or wait again. + // Fall through, may create or wait again. } else { const waiter = queue.shift()!; if (queue.length === 0) this.waiters.delete(key); diff --git a/src/backend/plugins/auto-update.ts b/src/backend/plugins/auto-update.ts index 37bf55967..65cacf421 100644 --- a/src/backend/plugins/auto-update.ts +++ b/src/backend/plugins/auto-update.ts @@ -61,8 +61,3 @@ export function startPluginAutoUpdate(): void { timer = setInterval(run, CHECK_EVERY_MS); timer.unref?.(); } - -export function stopPluginAutoUpdate(): void { - if (timer) clearInterval(timer); - timer = null; -} diff --git a/src/backend/plugins/paths.ts b/src/backend/plugins/paths.ts index eb89b51ca..2ebc038cd 100644 --- a/src/backend/plugins/paths.ts +++ b/src/backend/plugins/paths.ts @@ -1,4 +1,3 @@ -import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -43,29 +42,21 @@ export function getUnpackedPluginsDir(): string { * plugin is part of the install and must not be deletable or shadowable by * whatever happens to be in the data directory. * - * Resolved from this module's location rather than cwd, because the build - * emits to dist/backend/backend/plugins/ and the bundled plugins sit beside - * dist/backend. + * Resolved from this module's location rather than cwd. The build emits to + * dist/backend/backend/plugins/ with the bundled plugins beside dist/backend, + * and a run from source reads the same dist/plugins. */ export function getBundledPluginsDir(): string { const override = process.env.TERMIX_BUNDLED_PLUGINS_DIR; if (override) return override; const here = path.dirname(fileURLToPath(import.meta.url)); - - const candidates = [ - // Built server: dist/backend/backend/plugins -> dist/plugins - // Source tree: src/backend/plugins -> plugins - // Both are three levels up, which is why there is one entry, not two. - path.resolve(here, "../../../plugins"), - // Built server invoked from a nested layout: .../dist/backend/backend/plugins - path.resolve(here, "../../../../plugins"), - ]; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) return candidate; - } - return candidates[0]; + // Built: dist/backend/backend/plugins -> dist/plugins + // Source (vitest, tsx): src/backend/plugins -> dist/plugins + const fromSource = path.basename(path.resolve(here, "../..")) === "src"; + return fromSource + ? path.resolve(here, "../../../dist/plugins") + : path.resolve(here, "../../../plugins"); } /** The backend entry, from the manifest's own `backend` field. */ diff --git a/src/backend/plugins/ws.ts b/src/backend/plugins/ws.ts index 5a17dc964..579301c8e 100644 --- a/src/backend/plugins/ws.ts +++ b/src/backend/plugins/ws.ts @@ -371,10 +371,6 @@ function disposeRoute(routeKey: string, pluginId: string, path: string): void { }); } -export function getRegisteredWsRoutes(): string[] { - return [...routes.keys()]; -} - /** The socket paths a plugin serves without core's login check. */ export function getPluginPublicWsRoutes(pluginId: string): string[] { return [...routes.values()] diff --git a/src/backend/sync/server/feed.ts b/src/backend/sync/server/feed.ts index 3d3877726..d1cf22780 100644 --- a/src/backend/sync/server/feed.ts +++ b/src/backend/sync/server/feed.ts @@ -126,11 +126,6 @@ export async function reconcileUser( return { changed, wire }; } -/** Forgets a user's snapshot so the next reconcile reads everything. */ -export function invalidateSnapshot(userId: string): void { - snapshots.delete(userId); -} - const listeners = new Map>(); let debounce: NodeJS.Timeout | null = null; let heartbeat: NodeJS.Timeout | null = null; diff --git a/src/backend/sync/store.ts b/src/backend/sync/store.ts index 1a82773ae..430a3258e 100644 --- a/src/backend/sync/store.ts +++ b/src/backend/sync/store.ts @@ -64,12 +64,6 @@ export function locate( return and(eq(table.syncId, syncId), eq(ownerColumn(entity), userId))!; } -export function rowSyncId(entity: RegisteredSyncEntity, row: SyncRow): string { - return entity.singleton - ? singletonSyncId(entity.type) - : (row.syncId as string); -} - let hashKey: Buffer | null = null; export async function getHashKey(): Promise { diff --git a/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts b/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts index 73fdc86a6..b070dc2d8 100644 --- a/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts +++ b/src/backend/tests/database/db/audit-log-user-id-nullable.test.ts @@ -10,8 +10,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; * * The Drizzle schema said so, the repository was written against it, but the * runtime bootstrap still created `user_id TEXT NOT NULL`. A second, corrected - * `CREATE TABLE IF NOT EXISTS` further down was a no-op — the table already - * existed — so every fresh install got the old constraint and every user + * `CREATE TABLE IF NOT EXISTS` further down was a no-op, the table already + * existed, so every fresh install got the old constraint and every user * deletion (including the OIDC account-link cleanup) failed once the account * had logged in at least once. */ diff --git a/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts index b591da098..7088bac84 100644 --- a/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts +++ b/src/backend/tests/database/db/bootstrap-covers-all-tables.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; /** * `migrateSchema()` carried a `SELECT id FROM LIMIT 1` probe for a * number of tables that the primary bootstrap already creates. The probe never - * threw, so the `CREATE TABLE IF NOT EXISTS` in its catch never ran — and one + * threw, so the `CREATE TABLE IF NOT EXISTS` in its catch never ran, and one * of those unreachable copies had drifted away from the real definition * (`sessions` had lost `ON DELETE CASCADE`). * @@ -61,7 +61,7 @@ describe("bootstrap creates the tables the removed probes covered", () => { }); it("creates them on a database old enough to predate them", async () => { - // A database with users and hosts but none of the tables above — the + // A database with users and hosts but none of the tables above, the // upgrade path the deleted probes appeared to be protecting. const seed = new Database(":memory:"); seed.exec(` diff --git a/src/backend/tests/database/db/multi-dialect.test.ts b/src/backend/tests/database/db/multi-dialect.test.ts index 5fda37253..41100b04a 100644 --- a/src/backend/tests/database/db/multi-dialect.test.ts +++ b/src/backend/tests/database/db/multi-dialect.test.ts @@ -59,7 +59,7 @@ describe("needsExplicitPersist", () => { /** * schema.pg.ts and schema.mysql.ts are generated from schema.ts. These check the - * generated output is usable rather than merely syntactically valid — the + * generated output is usable rather than merely syntactically valid, the * repository layer's correctness rests on all three behaving the same way. */ describe("generated schemas", () => { @@ -133,7 +133,7 @@ describe("generated schemas", () => { * SQLite builds its indexes at runtime from PERFORMANCE_INDEXES; Postgres and * MySQL only ever get what the migrations declare, which comes from schema.ts. * Anything listed in one and missing from the other is an index the engines - * chosen for scale silently do without — which is how 31 of them went missing. + * chosen for scale silently do without, which is how 31 of them went missing. */ it("declares every performance index in schema.ts", () => { // Both are the leading column of an existing composite unique index, which diff --git a/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts b/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts index d713831d3..da0282d31 100644 --- a/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts +++ b/src/backend/tests/database/db/share-ssh-auth-backfill.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; /** * Sharing a host used to hand the owner's SSH authentication to the recipient * unconditionally. 2.6.1 put that behind `ssh_data.share_ssh_auth`, added as - * `NOT NULL DEFAULT 0` — so every host shared before the upgrade silently + * `NOT NULL DEFAULT 0`, so every host shared before the upgrade silently * stopped supplying credentials, and recipients hit "No valid authentication * method provided" on hosts that had worked the day before. * diff --git a/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts b/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts index fe5a8afd9..230e9cab0 100644 --- a/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts +++ b/src/backend/tests/database/db/ssh-credentials-username-rebuild.test.ts @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; /** * `ssh_credentials.username` became nullable when key-only credentials landed, * and databases created before that are rebuilt on startup to drop the - * constraint — SQLite cannot ALTER a column. + * constraint, SQLite cannot ALTER a column. * * The rebuild restated the table's columns as a literal, then copied rows with * `INSERT INTO temp SELECT `. The table has gained columns diff --git a/src/backend/tests/database/repositories/mutation-result.test.ts b/src/backend/tests/database/repositories/mutation-result.test.ts index b905de24f..a4ca6f216 100644 --- a/src/backend/tests/database/repositories/mutation-result.test.ts +++ b/src/backend/tests/database/repositories/mutation-result.test.ts @@ -17,7 +17,7 @@ describe("rowsAffected", () => { }); it("reads changes from a better-sqlite3 write result", () => { - // The shape of a write with no .returning() attached — verified against + // The shape of a write with no .returning() attached, verified against // the driver, not assumed. expect(rowsAffected({ changes: 1, lastInsertRowid: 7 })).toBe(1); expect(rowsAffected({ changes: 0, lastInsertRowid: 7 })).toBe(0); @@ -81,7 +81,7 @@ describe("supportsReturning", () => { expect(supportsReturning("sqlite")).toBe(true); expect(supportsReturning("postgres")).toBe(true); // No RETURNING clause in MySQL, and drizzle's mysql-core does not expose - // the method — call sites that need rows back must read first. + // the method, call sites that need rows back must read first. expect(supportsReturning("mysql")).toBe(false); }); }); diff --git a/src/backend/tests/database/repositories/returning.test.ts b/src/backend/tests/database/repositories/returning.test.ts index 42c1111c7..725523105 100644 --- a/src/backend/tests/database/repositories/returning.test.ts +++ b/src/backend/tests/database/repositories/returning.test.ts @@ -11,7 +11,7 @@ import type { DatabaseDialect } from "../../../database/db/dialect.js"; * The MySQL path cannot be exercised against a real engine here, and its whole * correctness is an ordering property: an update must be read AFTER the write, * a delete BEFORE it. Get either backwards and the rows describe the wrong - * state — silently, with no error anywhere. + * state, silently, with no error anywhere. * * So the drizzle handle is stubbed and the order of calls is recorded. */ @@ -169,7 +169,7 @@ describe("deleteReturning", () => { await deleteReturning(context, {} as never, where); // Without this, a concurrent write between them makes the returned rows - // describe a state that never existed — and with a pool the second + // describe a state that never existed, and with a pool the second // statement need not even reach the same connection. expect(calls[0]).toBe("begin"); expect(calls[calls.length - 1]).toBe("commit"); diff --git a/src/backend/tests/database/repositories/settings-cache-refresh.test.ts b/src/backend/tests/database/repositories/settings-cache-refresh.test.ts index 1f644d4c4..fae85bc59 100644 --- a/src/backend/tests/database/repositories/settings-cache-refresh.test.ts +++ b/src/backend/tests/database/repositories/settings-cache-refresh.test.ts @@ -8,8 +8,8 @@ import { /** * The settings cache lives in one process and is updated by whichever process * wrote the setting. On SQLite that is the only process there is. On Postgres - * and MySQL — the reason those exist here is to let several instances share one - * database — a setting changed on one replica would otherwise never reach the + * and MySQL, the reason those exist here is to let several instances share one + * database, a setting changed on one replica would otherwise never reach the * others, because the synchronous read cannot go back to the database. * * Re-priming on a timer does not make settings immediately consistent. It @@ -57,7 +57,7 @@ describe("settings cache refresh", () => { it("keeps running after a refresh throws", async () => { // A transient database blip must not stop the loop, or the replica is stuck - // on stale settings until it restarts — the exact failure this prevents. + // on stale settings until it restarts, the exact failure this prevents. const refresh = vi .fn() .mockRejectedValueOnce(new Error("connection reset")) diff --git a/src/backend/tests/database/repositories/test-support.ts b/src/backend/tests/database/repositories/test-support.ts index dc8891d87..977b69590 100644 --- a/src/backend/tests/database/repositories/test-support.ts +++ b/src/backend/tests/database/repositories/test-support.ts @@ -20,7 +20,7 @@ import type { DatabaseDialect } from "../../../database/db/dialect.js"; * * Defaults to SQLite, so `npm test` behaves as it always has and needs no * server. Set TEST_DIALECT=postgres or mysql, plus TEST_DATABASE_URL, to run - * the same tests against a real one — see the database-dialects CI job. + * the same tests against a real one, see the database-dialects CI job. */ export function testDialect(env = process.env): DatabaseDialect { const value = env.TEST_DIALECT?.trim().toLowerCase(); @@ -125,7 +125,7 @@ export class TestSqliteDatabase { * Portable write for test setup. * * Separate from query() because better-sqlite3 refuses `.all()` on a - * statement that returns no rows — "This statement does not return data". + * statement that returns no rows, "This statement does not return data". */ async run(statement: SQL): Promise { const context = this.context; @@ -207,7 +207,7 @@ async function truncateAll(context: DatabaseContext): Promise { } /** - * Splits seed SQL into statements, ignoring semicolons inside string literals — + * Splits seed SQL into statements, ignoring semicolons inside string literals, * JSON payloads in the fixtures contain them. */ function splitStatements(sql: string): string[] { @@ -274,7 +274,7 @@ function portableSql(statement: string, dialect: DatabaseDialect): string { if (dialect !== "mysql") return out; // Only the column list, before VALUES. A blanket replace also mangles the - // double quotes inside JSON payloads in the values — '{"slots":[]}' became + // double quotes inside JSON payloads in the values, '{"slots":[]}' became // '{`slots`:[]}', which is valid SQL and silently wrong data. const split = /^(.*?\bVALUES\b)(.*)$/is.exec(out); if (!split) return out.replace(/"([a-z_]+)"/g, "`$1`"); @@ -354,7 +354,7 @@ async function resyncAutoIncrement( for (const table of tables) { // Only tables whose id is generated. A text primary key, like users.id, // has no sequence and no counter to move, and a table keyed on something - // else entirely — host_sidebar_preferences.user_id — has no id at all. + // else entirely, host_sidebar_preferences.user_id, has no id at all. if (context.dialect === "postgres") { // pg_get_serial_sequence() raises 42703 rather than returning null when // the column is missing, so let information_schema decide whether there @@ -408,7 +408,7 @@ async function resyncAutoIncrement( * * Every test builds a fixture, and each would otherwise re-run the migrator * against the same shared database. drizzle's journal makes that a no-op only - * when the first run finished — several fixtures racing inside one file hit + * when the first run finished, several fixtures racing inside one file hit * "table already exists" instead. */ const migrations = new Map>(); @@ -436,7 +436,7 @@ let cachedSqliteSchema: string | null = null; * The full schema, from the generated SQLite migrations rather than * hand-written DDL in each test file. * - * Tests used to declare a cut-down version of every table they touched — a + * Tests used to declare a cut-down version of every table they touched, a * `users` with five columns where the real one has thirty. That drifts from the * schema silently, and it is the reason the same tests could not be pointed at * another engine. diff --git a/src/backend/tests/hosts/host-identity.test.ts b/src/backend/tests/hosts/host-identity.test.ts index 45ee0f58f..92b4eb273 100644 --- a/src/backend/tests/hosts/host-identity.test.ts +++ b/src/backend/tests/hosts/host-identity.test.ts @@ -19,7 +19,7 @@ import { * * The resolved row supplies the address, the credentials, the jump hosts and * the stored host key, so the session opened on whichever machine owned that - * id on the server — the host list stayed correct the whole time, and nothing + * id on the server, the host list stayed correct the whole time, and nothing * announced the substitution. */ describe("hostAddressMismatch", () => { diff --git a/src/backend/tests/hosts/host-resolver-sync-id.test.ts b/src/backend/tests/hosts/host-resolver-sync-id.test.ts index 4d7d43aa2..3b227a655 100644 --- a/src/backend/tests/hosts/host-resolver-sync-id.test.ts +++ b/src/backend/tests/hosts/host-resolver-sync-id.test.ts @@ -26,7 +26,7 @@ vi.mock("../../utils/shared-host-auth-resolver.js", () => ({ /** * A numeric host id belongs to whichever database produced it. Resolving the * desktop app's id against a sync server's table lands on whatever host owns - * that number there — a different machine, with its own address, credentials + * that number there, a different machine, with its own address, credentials * and host key. `sync_id` is the same string on both sides. */ describe("resolveHostBySyncId", () => { diff --git a/src/backend/tests/plugins/paths.test.ts b/src/backend/tests/plugins/paths.test.ts new file mode 100644 index 000000000..4f5931f42 --- /dev/null +++ b/src/backend/tests/plugins/paths.test.ts @@ -0,0 +1,24 @@ +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { getBundledPluginsDir } from "../../plugins/paths.js"; + +describe("getBundledPluginsDir", () => { + const saved = process.env.TERMIX_BUNDLED_PLUGINS_DIR; + + afterEach(() => { + if (saved === undefined) delete process.env.TERMIX_BUNDLED_PLUGINS_DIR; + else process.env.TERMIX_BUNDLED_PLUGINS_DIR = saved; + }); + + it("reads dist/plugins when run from source", () => { + delete process.env.TERMIX_BUNDLED_PLUGINS_DIR; + expect(getBundledPluginsDir()).toBe( + path.resolve(process.cwd(), "dist", "plugins"), + ); + }); + + it("honors the override", () => { + process.env.TERMIX_BUNDLED_PLUGINS_DIR = "/somewhere/else"; + expect(getBundledPluginsDir()).toBe("/somewhere/else"); + }); +}); diff --git a/src/backend/tests/utils/audit-forwarder.test.ts b/src/backend/tests/utils/audit-forwarder.test.ts index eb1c928c9..bd15e1d11 100644 --- a/src/backend/tests/utils/audit-forwarder.test.ts +++ b/src/backend/tests/utils/audit-forwarder.test.ts @@ -123,7 +123,7 @@ describe("forwardAuditEntry", () => { ); }); - it("swallows transport errors — a dead SIEM must not break auditing", async () => { + it("swallows transport errors, a dead SIEM must not break auditing", async () => { safeFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); await expect(forwardAuditEntry(ENTRY, NOW, env)).resolves.toBe(false); @@ -140,7 +140,7 @@ describe("forwardAuditEntry", () => { await forwardAuditEntry(ENTRY, NOW, env); } - // 5 per-entry warnings, then one suppression notice — not 8. + // 5 per-entry warnings, then one suppression notice, not 8. const perEntry = logs.warn.mock.calls.filter( (call) => call[0] === "Failed to forward audit entry", ); diff --git a/src/backend/utils/audit-forwarder.ts b/src/backend/utils/audit-forwarder.ts index e757193cd..a48da2a4b 100644 --- a/src/backend/utils/audit-forwarder.ts +++ b/src/backend/utils/audit-forwarder.ts @@ -22,7 +22,7 @@ function readForwardSetting(key: string): string | null { /** * How many consecutive failures before the forwarder stops complaining on every - * entry. It keeps trying — this only throttles the log noise, and it reports + * entry. It keeps trying, this only throttles the log noise, and it reports * again once delivery recovers. */ const QUIET_AFTER_FAILURES = 5; diff --git a/src/backend/utils/compression-config.ts b/src/backend/utils/compression-config.ts index 0a003bc67..6489541d7 100644 --- a/src/backend/utils/compression-config.ts +++ b/src/backend/utils/compression-config.ts @@ -6,8 +6,8 @@ import { type Request, type RequestHandler, type Response } from "express"; * * The default is 1KB; this is slightly higher because almost every response * under a few KB here is a small status or preference object where the CPU and - * the extra headers are not worth it. The payloads that matter — host lists, - * audit pages, fleet inventories — are orders of magnitude above this. + * the extra headers are not worth it. The payloads that matter, host lists, + * audit pages, fleet inventories, are orders of magnitude above this. */ const MIN_RESPONSE_BYTES = 2048; diff --git a/src/backend/utils/crypto-migration/legacy-share-cleanup.ts b/src/backend/utils/crypto-migration/legacy-share-cleanup.ts index 4fa020ba6..89f1c0624 100644 --- a/src/backend/utils/crypto-migration/legacy-share-cleanup.ts +++ b/src/backend/utils/crypto-migration/legacy-share-cleanup.ts @@ -47,7 +47,7 @@ export async function runLegacySharedCredentialCleanup(): Promise<{ // Nothing to clean up on an engine this application has never run on. A // Postgres or MySQL database is created by the drizzle migrations, which have - // never emitted these legacy columns, so there is nothing to drop — and the + // never emitted these legacy columns, so there is nothing to drop, and the // check itself needs a synchronous PRAGMA that only SQLite offers. if (!needsExplicitPersist(resolveDatabaseDialect())) return result; diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index 8115c50f4..bec129bd4 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -201,7 +201,7 @@ class DataCrypto { migratedFieldsCount: number; }> { // Only a database that predates field encryption has plaintext to migrate, - // and only SQLite deployments can predate it — Postgres and MySQL support + // and only SQLite deployments can predate it, Postgres and MySQL support // arrived after. The store also needs synchronous queries no other driver // has, so this would throw rather than find nothing to do. if (!needsExplicitPersist(resolveDatabaseDialect())) { diff --git a/src/backend/utils/data-dir-guard.ts b/src/backend/utils/data-dir-guard.ts index 867cbaaa0..670664cf1 100644 --- a/src/backend/utils/data-dir-guard.ts +++ b/src/backend/utils/data-dir-guard.ts @@ -14,7 +14,7 @@ export class DataDirMisconfiguredError extends Error { /** * Directories Termix has shipped or documented as a data location. A deployment - * that loses DATA_DIR — an unloaded .env file, an unmounted volume — falls back + * that loses DATA_DIR, an unloaded .env file, an unmounted volume, falls back * to the default and finds an empty directory, which is indistinguishable from a * first run. Checking these tells the two apart. */ diff --git a/src/backend/utils/error-message.ts b/src/backend/utils/error-message.ts index ad7d83bfa..8230cf89b 100644 --- a/src/backend/utils/error-message.ts +++ b/src/backend/utils/error-message.ts @@ -1,7 +1,7 @@ /** * Extract a stable, human-readable message from an unknown thrown value. * - * A thrown value is not guaranteed to be an `Error` — libraries may throw + * A thrown value is not guaranteed to be an `Error`, libraries may throw * strings or plain objects, which carry no useful `.message`. When that * happens the caller gets `fallback` instead, so error paths never surface a * raw `[object Object]` in logs or responses. diff --git a/src/backend/utils/permission-manager.ts b/src/backend/utils/permission-manager.ts index 2d42cbcbc..799237194 100644 --- a/src/backend/utils/permission-manager.ts +++ b/src/backend/utils/permission-manager.ts @@ -295,7 +295,7 @@ class PermissionManager { * queries instead of one call per host. * * canAccessHost costs between one and four queries, so filtering a list with - * it is linear in host count — and the status poll does exactly that every + * it is linear in host count, and the status poll does exactly that every * few seconds for the whole fleet. This answers the same question for many * hosts at once using the same three rules, in the same order: owner, then * an unexpired grant, then admin bypass. diff --git a/src/backend/utils/ssh-algorithms.ts b/src/backend/utils/ssh-algorithms.ts index 68b812ffd..2e5d6a8f2 100644 --- a/src/backend/utils/ssh-algorithms.ts +++ b/src/backend/utils/ssh-algorithms.ts @@ -34,7 +34,7 @@ try { // The pure-JS fallback in ssh2 for chacha20-poly1305@openssh.com is broken and // corrupts the transport: the target sshd aborts the KEX with // "ssh_dispatch_run_fatal: ... incomplete message [preauth]" and the client times out. - // A working OpenSSL "chacha20" cipher is NOT sufficient here — only the native + // A working OpenSSL "chacha20" cipher is NOT sufficient here, only the native // binding (sshcrypto.node) makes chacha20-poly1305 usable. Keep it disabled otherwise // so filterCiphers() drops it and the connection negotiates AES-GCM instead. ssh2BindingAvailable = false; diff --git a/src/backend/utils/system-secret-crypto.ts b/src/backend/utils/system-secret-crypto.ts index 2eef21a12..5358527f7 100644 --- a/src/backend/utils/system-secret-crypto.ts +++ b/src/backend/utils/system-secret-crypto.ts @@ -7,7 +7,7 @@ import { SystemCrypto } from "./system-crypto.js"; * Per-user field encryption (DataCrypto/FieldCrypto) derives its key from the * user's DEK, which works for host passwords and SSH keys. It does not work for * SSO provider configuration: `sso_providers` has no `userId`, and the OIDC - * client secret and LDAP bind password must be readable during login — before + * client secret and LDAP bind password must be readable during login, before * any user is authenticated, let alone unlocked. * * Those secrets were previously stored base64-encoded behind an `encoded:`