/** * QR-based pairing. * * Implements IC-3 — the `pi-remote://` URL scheme and `POST /pair` exchange. * * Pairing flow: * 1. Server generates a one-time pairingToken (short-lived, 10 min). * 2. `pi-remote pair` CLI prints the QR code containing the URL. * 3. iOS app scans the QR, extracts host/port/pairingToken/fingerprint. * 4. iOS app calls `POST /pair` with { pairingToken, deviceToken?, environment?, deviceName? }. * 5. Server validates pairingToken, creates a bearer token, returns { bearerToken, sidecarId }. * * Owner: T-1.3 */ import { randomBytes } from "node:crypto"; export interface PairingToken { token: string; expiresAt: number; // unix ms } // In-memory store of active pairing tokens (cleared on server restart) const _pairingTokens = new Map(); const PAIRING_TOKEN_TTL_MS = 10 * 60 * 1000; // 10 minutes /** Generate a new pairing token. Invalidates any existing one. */ export function generatePairingToken(): PairingToken { const token = randomBytes(16).toString("base64url"); const entry: PairingToken = { token, expiresAt: Date.now() + PAIRING_TOKEN_TTL_MS, }; _pairingTokens.set(token, entry); return entry; } /** Validate and consume a pairing token. Returns true if valid. */ export function consumePairingToken(token: string): boolean { const entry = _pairingTokens.get(token); if (!entry) return false; _pairingTokens.delete(token); if (Date.now() > entry.expiresAt) return false; return true; } /** * Build the IC-3 pairing URL. * * pi-remote://:?pair=&fp=&name= */ export function buildPairingUrl(opts: { host: string; port: number; pairingToken: string; fingerprint: string; sidecarName?: string; }): string { const { host, port, pairingToken, fingerprint, sidecarName = "pi-remote", } = opts; const params = new URLSearchParams({ pair: pairingToken, fp: fingerprint, name: sidecarName, }); return `pi-remote://${host}:${port}?${params.toString()}`; } /** * Render the pairing URL as a QR code to the terminal. * Uses the `qrcode` package bundled as a dependency. */ export async function printPairingQr(url: string): Promise { // Dynamic import so this file loads even if qrcode isn't installed const qrcode = await import("qrcode"); const qr = await qrcode.toString(url, { type: "terminal", small: true }); console.log(qr); console.log(url); }