P1 spine: monorepo, shared WS protocol, server daemon

- npm workspaces (shared / server / web), TS strict, project refs
- @arboretum/shared: multiplexed WS protocol (JSON control + binary
  output frames: 1B type + u32le channel), flow-control constants
  (ACK 256K, HIGH 384K, LOW 128K, lagging 2M), REST types
- git-arboretum server: Fastify 5 + node:sqlite (single native dep:
  node-pty prebuilt), token auth (sha256 at rest, HMAC cookie, global
  login rate limit + backoff), strict Origin check on /api and /ws,
  PtyManager (2MiB ring with monotonic offset, resync replay = reset +
  256KiB tail, pause/resume only when ALL interactive clients exceed
  HIGH, observers never throttle, lagging clients resync), WS gateway
  (attach/stdin/resize/ack, heartbeat 30s), SIGTERM→SIGKILL 5s grace
- CLI: arboretum [--port 7317] [--bind 127.0.0.1] — non-loopback bind
  requires an explicit safety flag
- Smoke-tested: login/401/403-origin/spawn bash/kill/grace-SIGKILL all
  green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Johan LEROY
2026-06-11 22:04:09 +02:00
parent 8733c17e44
commit f6f73329b9
25 changed files with 4467 additions and 474 deletions

View File

@@ -0,0 +1,68 @@
import { DatabaseSync } from 'node:sqlite';
const MIGRATIONS: Array<{ id: number; sql: string }> = [
{
id: 1,
sql: `
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE auth_tokens (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
last_used_at TEXT,
revoked_at TEXT
);
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
cwd TEXT NOT NULL,
command TEXT NOT NULL,
title TEXT,
created_at TEXT NOT NULL,
ended_at TEXT,
exit_code INTEGER
);
`,
},
];
export type Db = DatabaseSync;
export function openDb(path: string): Db {
const db = new DatabaseSync(path);
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA foreign_keys = ON');
migrate(db);
return db;
}
function migrate(db: DatabaseSync): void {
db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (id INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)');
const applied = new Set(
(db.prepare('SELECT id FROM schema_migrations').all() as Array<{ id: number }>).map((r) => r.id),
);
for (const m of MIGRATIONS) {
if (applied.has(m.id)) continue;
db.exec('BEGIN');
try {
db.exec(m.sql);
db.prepare('INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)').run(m.id, new Date().toISOString());
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
}
}
export function getSetting(db: Db, key: string): string | null {
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
return row?.value ?? null;
}
export function setSetting(db: Db, key: string, value: string): void {
db.prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
}