pi-remote-control/extensions/remote-control/auth/pairing.ts

86 lines
2.4 KiB
TypeScript

/**
* 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<string, PairingToken>();
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://<host>:<port>?pair=<pairingToken>&fp=<sha256-hex>&name=<sidecarName>
*/
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<void> {
// 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);
}