feat: GET /pair-qr endpoint — QR code in terminal, fix double-port bug

This commit is contained in:
Johannes Merz 2026-05-16 02:56:06 +02:00
parent 1f36636e06
commit 571cf8c9ec
1 changed files with 46 additions and 1 deletions

View File

@ -28,7 +28,12 @@ import type {
ExtensionAPI, ExtensionAPI,
ExtensionContext, ExtensionContext,
} from "@earendil-works/pi-coding-agent"; } 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 { createToken, validateBearer } from "../auth/tokens.js";
import { import {
generateSessionId, generateSessionId,
@ -226,6 +231,46 @@ export async function startServer(
// New API routes (IC-2) — bearer token auth // New API routes (IC-2) — bearer token auth
const asyncHandler = async (): Promise<boolean> => { const asyncHandler = async (): Promise<boolean> => {
// 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 // POST /pair — unauthenticated, uses one-time pairingToken
if (pathname === "/pair" && req.method === "POST") { if (pathname === "/pair" && req.method === "POST") {
let body: Record<string, unknown> = {}; let body: Record<string, unknown> = {};