import type Database from "better-sqlite3"; import { type Capability, type CapabilityPreset } from "../../services/security/capabilities"; /** * Paired-device registry (C5). * See docs/architecture/2026-07-24-device-identity-and-capabilities.md. * * Stores identity and authority for each paired device. Never stores a usable * credential: only the SHA-256 of a device token, so reading this table cannot * impersonate a device. */ export interface DeviceRow { device_id: string; public_key: string; token_hash: string; name: string | null; capabilities: string; created_at: number; last_seen_at: number | null; revoked_at: number | null; /** Noise static public key, base64. Null on every row that predates E2EE. */ e2ee_static_pub: string | null; /** The downgrade lock. 1 once this device has completed a handshake. */ e2ee_required: number; e2ee_version: number | null; } /** A device as reported over the API. Deliberately carries no credential. */ export interface DeviceView { deviceId: string; name: string | null; capabilities: Capability[]; createdAt: number; lastSeenAt: number | null; revokedAt: number | null; /** * Whether this device is pinned to encryption. * * A boolean, never the key and never a hash of it: `GET /api/devices` exists * so a user can spot a device they did not pair, and answering "is this one * encrypted" needs no key material to do it. Publishing the static key would * hand an attacker who reached this endpoint the value that identifies a * device. */ e2ee: boolean; } export interface RegisteredDevice { deviceId: string; /** Returned to the client exactly once, at pairing. Never persisted raw. */ deviceToken: string; capabilities: Capability[]; } /** Device tokens are opaque high-entropy strings; 32 bytes matches the API key. */ export declare function generateDeviceToken(): string; export declare function hashDeviceToken(token: string): string; /** * Constant-time comparison of two token hashes, matching `validateApiKey`'s * discipline. Both inputs are fixed-length hex digests, so a length mismatch * means a malformed value rather than a secret-dependent branch. */ export declare function safeHashEquals(a: string, b: string): boolean; /** * Parse the stored capability JSON, dropping anything this build does not * recognize. A downgrade must never silently grant a capability it cannot * enforce — unknown entries are discarded rather than trusted. */ export declare function parseCapabilities(raw: string): Capability[]; export declare function toDeviceView(row: DeviceRow): DeviceView; export declare class DevicesRepository { private insertStmt; private byTokenHashStmt; private byIdStmt; private listStmt; private revokeStmt; private touchStmt; private deleteStmt; private deleteRevokedStmt; private byE2eeStaticPubStmt; private repairStmt; constructor(db: Database.Database); /** * Record a newly paired device and mint its token. * * The raw token is returned to the caller and never stored — this is the only * moment it exists outside the client. */ register(args: { publicKey: string; name?: string | null; preset?: CapabilityPreset; now?: number; /** * The device's Noise static public key, base64, when pairing completed a * handshake. Absent on a plaintext pairing, which stays exactly as it is * today: no key, no pin, no behaviour change. */ e2eeStaticPub?: string; e2eeVersion?: number; }): RegisteredDevice; /** The device that owns a Noise static key, or null. */ getByE2eeStaticPub(staticPub: string): DeviceRow | null; /** * Resolve a presented token to a device, or null. * * Returns null for a revoked device, so revocation takes effect on the very * next request with no cache to go stale. */ authenticate(token: string): DeviceRow | null; get(deviceId: string): DeviceRow | null; /** All devices, including revoked ones — an audit surface needs the history. */ list(): DeviceView[]; /** Revoke one device. Others are untouched — no key rotation, no collateral. */ revoke(deviceId: string, now?: number): boolean; /** * Erase one device's record outright. * * Deliberately separate from `revoke`, which is a soft delete that keeps the * row so `list()` can show what happened. That audit trail is the right * default — but it meant a `devices` row, including the user-supplied `name` * ("Ronen's iPhone"), had no removal path at all once the registry moved to * runtime.db, which no command deletes. This is that path. * * Erasure is NOT revocation: deleting a row frees its `token_hash`, so a * device whose token is still on a phone somewhere stops being *known* rather * than being *refused*. Revoke first, delete second, is the safe order, and * `deleteRevoked()` exists so that is the easy thing to do. */ delete(deviceId: string): boolean; /** * Erase every already-revoked device. The bulk companion to `delete`, and the * one that is safe by construction: a revoked device is already refused, so * removing its row cannot restore access to anything. * * Returns the number of rows removed. */ deleteRevoked(): number; touch(deviceId: string, now?: number): void; } //# sourceMappingURL=devices.repository.d.ts.map