// SQLite state: the T3 hosts the gateway connects to, the watches paired with it, and pending // pairing codes. The CLI and the server open the same file, and the server rereads `hosts` so a // `wristcode host add` takes effect without a restart. import { Database } from "bun:sqlite"; export interface HostRow { id: string; label: string; url: string; environment_id: string; token: string; token_expires_at: string | null; created_at: string; } export interface DeviceRow { id: string; name: string; token_hash: string; apns_token: string | null; apns_env: "sandbox" | "production" | null; created_at: string; last_seen_at: string | null; revoked_at: string | null; } const SCHEMA = ` create table if not exists hosts ( id text primary key, label text not null, url text not null unique, environment_id text not null unique, token text not null, token_expires_at text, created_at text not null ); create table if not exists devices ( id text primary key, name text not null, token_hash text not null unique, apns_token text, apns_env text, created_at text not null, last_seen_at text, revoked_at text ); create table if not exists pairing_codes ( code_hash text primary key, expires_at text not null, used_at text ); `; export const sha256 = (value: string) => new Bun.CryptoHasher("sha256").update(value).digest("hex"); const now = () => new Date().toISOString(); export function openDb(path: string) { const db = new Database(path, { create: true, strict: true }); db.run("pragma journal_mode = wal"); db.run("pragma busy_timeout = 5000"); db.run(SCHEMA); return { hosts: { list: () => db.query("select * from hosts order by created_at").all(), upsert(host: Omit) { db.query( `insert into hosts (id, label, url, environment_id, token, token_expires_at, created_at) values ($id, $label, $url, $environment_id, $token, $token_expires_at, $created_at) on conflict(environment_id) do update set label = excluded.label, url = excluded.url, token = excluded.token, token_expires_at = excluded.token_expires_at`, ).run({ ...host, created_at: now() }); }, remove: (idOrLabel: string) => db.query("delete from hosts where id = $v or label = $v").run({ v: idOrLabel }).changes > 0, }, devices: { list: () => db.query("select * from devices order by created_at").all(), active: () => db.query("select * from devices where revoked_at is null").all(), create(name: string, token: string): string { const id = crypto.randomUUID(); db.query( "insert into devices (id, name, token_hash, created_at) values ($id, $name, $hash, $created_at)", ).run({ id, name, hash: sha256(token), created_at: now() }); return id; }, /** The active device that owns this bearer token, marking it seen. */ authenticate(token: string): DeviceRow | null { const device = db .query("select * from devices where token_hash = $hash and revoked_at is null") .get({ hash: sha256(token) }); if (device) db.query("update devices set last_seen_at = $at where id = $id").run({ at: now(), id: device.id }); return device; }, setPush(id: string, apnsToken: string, apnsEnv: "sandbox" | "production") { db.query("update devices set apns_token = $apnsToken, apns_env = $apnsEnv where id = $id").run({ id, apnsToken, apnsEnv, }); }, clearPush: (apnsToken: string) => db.query("update devices set apns_token = null where apns_token = $apnsToken").run({ apnsToken }), revoke: (idOrName: string) => db.query("update devices set revoked_at = $at where (id = $v or name = $v) and revoked_at is null").run({ at: now(), v: idOrName, }).changes, }, pairing: { create(code: string, ttlMs: number) { db.query("insert into pairing_codes (code_hash, expires_at) values ($hash, $expires_at)").run({ hash: sha256(code), expires_at: new Date(Date.now() + ttlMs).toISOString(), }); }, /** Marks the code used and returns true if it was valid. */ redeem(code: string): boolean { return ( db .query( "update pairing_codes set used_at = $at where code_hash = $hash and used_at is null and expires_at > $at", ) .run({ hash: sha256(code), at: now() }).changes > 0 ); }, /** Voids every open code, after too many wrong guesses. */ voidAll: () => db.query("update pairing_codes set used_at = $at where used_at is null").run({ at: now() }), }, close: () => db.close(), }; } export type Db = ReturnType;