import { randomBytes } from "node:crypto"; import { createServer, type Server } from "node:http"; import { openInBrowser } from "../util/runtime.ts"; import { type AuthScope, authLoginUrl } from "./profiles.ts"; export type BrowserAuthResult = { token: string; port: number; returnUrl: string; }; export type BrowserAuthError = { error: string; port: number; }; const DEFAULT_PORT = 14189; function callbackCaptureHtml(state: string): string { return ` trelly

Finishing authorization…

`; } export function browserAuthorizeUrl( apiKey: string, port: number, scope: AuthScope[] = ["read", "write"], ): string { return authLoginUrl(apiKey, { returnUrl: `http://127.0.0.1:${port}/callback`, scope, expiration: "30days", }); } async function readJsonBody( req: import("node:http").IncomingMessage, ): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.from(chunk)); } const text = Buffer.concat(chunks).toString("utf8"); return text ? JSON.parse(text) : {}; } export async function captureTokenViaBrowser( apiKey: string, opts: { port?: number; timeoutMs?: number; openBrowser?: boolean; scope?: AuthScope[]; } = {}, ): Promise { const port = opts.port ?? DEFAULT_PORT; const timeoutMs = opts.timeoutMs ?? 120_000; const returnUrl = `http://127.0.0.1:${port}/callback`; const state = randomBytes(16).toString("hex"); const authorizeUrl = browserAuthorizeUrl(apiKey, port, opts.scope); return new Promise((resolve) => { let settled = false; let httpServer: Server | undefined; const finish = (result: BrowserAuthResult | BrowserAuthError) => { if (settled) return; settled = true; clearTimeout(timer); httpServer?.close(); resolve(result); }; const timer = setTimeout(() => { finish({ error: "Timed out waiting for browser authorization", port, }); }, timeoutMs); httpServer = createServer(async (req, res) => { const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`); if (url.pathname === "/callback" && req.method === "GET") { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(callbackCaptureHtml(state)); return; } if (url.pathname === "/callback/token" && req.method === "POST") { try { const body = (await readJsonBody(req)) as { token?: string; state?: string }; if (body.state !== state) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: "invalid state" })); return; } if (!body.token) { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false })); finish({ error: "Empty token in callback", port }); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); finish({ token: body.token, port, returnUrl }); } catch { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false })); finish({ error: "Failed to read callback token", port }); } return; } if (url.pathname === "/callback/error" && req.method === "POST") { try { const body = (await readJsonBody(req)) as { error?: string; state?: string }; if (body.state !== state) { res.writeHead(403, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false })); return; } res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false })); finish({ error: body.error ?? "Authorization denied", port }); } catch { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false })); finish({ error: "Authorization failed", port }); } return; } if (url.pathname === "/" || url.pathname === "/start") { res.writeHead(302, { Location: authorizeUrl }); res.end(); return; } res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not found"); }); httpServer.listen(port, "127.0.0.1", () => { if (opts.openBrowser !== false) { openInBrowser(`http://127.0.0.1:${port}/start`).catch(() => { process.stderr.write( `Open this URL in your browser:\nhttp://127.0.0.1:${port}/start\n`, ); }); } else { process.stderr.write( `Open this URL in your browser:\nhttp://127.0.0.1:${port}/start\n`, ); } }); httpServer.on("error", (err) => { finish({ error: err instanceof Error ? `Local callback server failed on port ${port}: ${err.message}` : `Local callback server failed on port ${port}`, port, }); }); }); } export const SETUP_ALLOWED_ORIGIN = `http://127.0.0.1:${DEFAULT_PORT}`;