diff --git a/extensions/remote-control/server/server.ts b/extensions/remote-control/server/server.ts index a04c93e..e218bfa 100644 --- a/extensions/remote-control/server/server.ts +++ b/extensions/remote-control/server/server.ts @@ -28,7 +28,12 @@ import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; -import { consumePairingToken } from "../auth/pairing.js"; +import { + buildPairingUrl, + consumePairingToken, + generatePairingToken, + printPairingQr, +} from "../auth/pairing.js"; import { createToken, validateBearer } from "../auth/tokens.js"; import { generateSessionId, @@ -226,6 +231,46 @@ export async function startServer( // New API routes (IC-2) — bearer token auth const asyncHandler = async (): Promise => { + // GET /pair-qr — generates pairing token + prints QR to response + if (pathname === "/pair-qr" && req.method === "GET") { + if (!isAuthenticated(req)) { + sendJson(res, 403, { error: "forbidden" }); + return true; + } + const addr = httpServer.address() as { + address: string; + port: number; + } | null; + const port = addr?.port ?? 7777; + const rawHost = + (config.publicBaseUrl ?? config.advertisedBaseUrl) + ?.replace(/^https?:\/\//, "") + .replace(/\/$/, "") ?? + addr?.address ?? + "localhost"; + // Strip :port suffix if publicBaseUrl already contains it + const host = rawHost.includes(":") + ? rawHost.slice(0, rawHost.lastIndexOf(":")) + : rawHost; + const pairingEntry = generatePairingToken(); + const url = buildPairingUrl({ + host, + port, + pairingToken: pairingEntry.token, + fingerprint: "", + sidecarName: "pi-remote", + }); + res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); + // Render QR inline + const qrcode = await import("qrcode"); + const qr = await qrcode.toString(url, { + type: "terminal", + small: true, + }); + res.end(`${qr}\n${url}\n\nExpires in 10 minutes.`); + return true; + } + // POST /pair — unauthenticated, uses one-time pairingToken if (pathname === "/pair" && req.method === "POST") { let body: Record = {};