86 lines
2.4 KiB
TypeScript
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);
|
|
}
|