NexusTerm: an SSH session manager for Linux

Connection tree with interleaved folders and hosts, tabbed terminals
grouped by host, a pinned local shell, TOFU host key verification, and
credentials held by the OS keyring.

Terminal bytes travel on a per-session MessagePort with credit-based flow
control, so one session running `cat` on a large file cannot stall the
others. Transports reconnect on their own with jittered backoff, keeping
the scrollback and the tab while replacing only the connection beneath.

156 tests, including integration tests against a real sshd and a
migration exercised against a database built in the old shape.
This commit is contained in:
ylhan
2026-09-17 03:12:24 +08:00
commit 430d78b9f0
88 changed files with 19793 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
import { afterEach, describe, expect, it } from 'vitest'
import { openDatabase, closeDatabase } from '../src/main/db/database'
import { MIGRATIONS } from '../src/main/db/migrations'
import { createFolder, createHost } from '../src/main/db/repositories/connections'
import { createTempDb, type TempDb } from './helpers'
let temp: TempDb | null = null
afterEach(() => {
temp?.cleanup()
temp = null
})
const HIGHEST_VERSION = Math.max(...MIGRATIONS.map((m) => m.version))
describe('database', () => {
it('applies every migration on first open', () => {
temp = createTempDb()
expect(temp.db.pragma('user_version', { simple: true })).toBe(HIGHEST_VERSION)
})
it('creates the expected tables', () => {
temp = createTempDb()
const rows = temp.db
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
.all() as { name: string }[]
const tables = rows.map((r) => r.name)
for (const table of [
'credentials',
'folders',
'history',
'hosts',
'known_hosts',
'master_key_meta',
'nodes',
'port_forwards',
'private_keys',
'settings',
]) {
expect(tables).toContain(table)
}
})
it('is idempotent across reopens', () => {
const dir = createTempDb()
const { path } = dir
closeDatabase(dir.db)
// Reopening must not attempt to re-run migrations; a second CREATE TABLE
// would throw and the user would lose access to their data on every launch.
const reopened = openDatabase(path)
expect(reopened.pragma('user_version', { simple: true })).toBe(HIGHEST_VERSION)
closeDatabase(reopened)
temp = { ...dir, db: reopened }
})
it('enables foreign keys, without which cascades silently do nothing', () => {
temp = createTempDb()
expect(temp.db.pragma('foreign_keys', { simple: true })).toBe(1)
})
it('cascades folder deletion to nested folders and their hosts', () => {
temp = createTempDb()
const parent = createFolder(temp.db, null, 'parent')
const child = createFolder(temp.db, parent, 'child')
createHost(temp.db, {
parentId: child,
label: 'web',
hostname: 'example.com',
port: 22,
username: 'root',
authType: 'password',
keyPath: null,
color: null,
})
temp.db.prepare('DELETE FROM nodes WHERE id = ?').run(parent)
const nodes = temp.db.prepare('SELECT COUNT(*) AS n FROM nodes').get() as { n: number }
const hosts = temp.db.prepare('SELECT COUNT(*) AS n FROM hosts').get() as { n: number }
const folders = temp.db.prepare('SELECT COUNT(*) AS n FROM folders').get() as { n: number }
expect(nodes.n).toBe(0)
expect(folders.n).toBe(0)
expect(hosts.n).toBe(0)
})
it('rejects a credential attached to a folder id', () => {
// The point of splitting attributes out of `nodes`: a `kind` discriminator
// alone could not stop this, but a foreign key to `hosts(id)` can.
temp = createTempDb()
const folder = createFolder(temp.db, null, 'not-a-host')
expect(() =>
temp?.db
.prepare(
`INSERT INTO credentials (host_id, kind, ciphertext, nonce, salt, created_at)
VALUES (?, 'password', X'01', X'02', X'03', ?)`,
)
.run(folder, Date.now()),
).toThrow()
})
it('enforces the single-row constraint on master_key_meta', () => {
temp = createTempDb()
const insert = (): void => {
temp?.db
.prepare(
'INSERT INTO master_key_meta (id, ciphertext, backend, created_at) VALUES (?, ?, ?, ?)',
)
.run(1, Buffer.from('x'), 'test', Date.now())
}
insert()
// A second row would mean two master keys, and therefore secrets that
// silently become undecryptable depending on which row is read first.
expect(insert).toThrow()
})
it('rejects malformed port forwards via the CHECK constraint', () => {
temp = createTempDb()
const now = Date.now()
const host = createHost(temp.db, {
parentId: null,
label: 'web',
hostname: 'example.com',
port: 22,
username: 'root',
authType: 'password',
keyPath: null,
color: null,
})
// A dynamic (SOCKS) forward has no fixed destination.
expect(() =>
temp?.db
.prepare(
`INSERT INTO port_forwards (host_id, type, bind_port, dest_host, dest_port, created_at)
VALUES (?, 'dynamic', 1080, 'example.com', 80, ?)`,
)
.run(host, now),
).toThrow()
// A local forward must have one.
expect(() =>
temp?.db
.prepare(
`INSERT INTO port_forwards (host_id, type, bind_port, dest_host, dest_port, created_at)
VALUES (?, 'local', 8080, NULL, NULL, ?)`,
)
.run(host, now),
).toThrow()
})
})