cli/src/auth-server.tsblame
View source
e93a9781import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
e93a9782
e93a9783interface AuthResult {
e93a9784 token: string;
e93a9785}
e93a9786
46c77797const STYLES = `
46c77798@import url('https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:ital,wght@0,400;1,400&display=swap');
46c77799:root{--bg:#faf8f5;--card:#f2efe9;--border:#e8e3db;--text:#2c2824;--muted:#7a746c;--accent:#4d8a78;--error:#a05050}
46c777910@media(prefers-color-scheme:dark){:root{--bg:#1a1918;--card:#242220;--border:#302e2b;--text:#e8e4df;--muted:#9a948c;--accent:#7aab9c;--error:#cc9292}}
46c777911*{margin:0;box-sizing:border-box}
46c777912body{font-family:'Libre Caslon Text',Georgia,serif;background:var(--bg);color:var(--text);display:flex;justify-content:center;align-items:center;height:100vh}
46c777913.card{text-align:center;padding:2rem;max-width:24rem;width:100%;background:var(--card);border:1px solid var(--border)}
46c777914.logo{margin-bottom:1.5rem}
46c777915h1{font-size:1.125rem;margin-bottom:0.25rem}
46c777916p{font-size:0.875rem;color:var(--muted);margin-top:0.5rem}
46c777917`;
46c777918
46c777919const LOGO = `<svg width="40" height="40" viewBox="-4 -4 72 72" style="color:var(--accent)">
46c777920<path d="M32,58 C32,45 31,35 32,22 C33,14 32,6 32,2" stroke="currentColor" stroke-width="5" fill="none" stroke-linecap="round"/>
46c777921<path d="M32,38 Q43,34 52,38" stroke="currentColor" stroke-width="4.5" fill="none" stroke-linecap="round"/>
46c777922<path d="M32,20 Q22,15 14,10" stroke="currentColor" stroke-width="4" fill="none" stroke-linecap="round"/>
46c777923<circle cx="32" cy="58" r="6" fill="currentColor"/><circle cx="32" cy="38" r="5.5" fill="currentColor"/>
46c777924<circle cx="52" cy="38" r="5" fill="currentColor"/><circle cx="32" cy="20" r="5" fill="currentColor"/>
46c777925<circle cx="14" cy="10" r="4.5" fill="currentColor"/><circle cx="32" cy="2" r="4.5" fill="currentColor"/>
46c777926</svg>`;
46c777927
e93a97828const SUCCESS_HTML = `<!DOCTYPE html>
46c777929<html><head><title>Grove CLI</title><style>${STYLES}</style></head>
46c777930<body><div class="card"><div class="logo">${LOGO}</div>
46c777931<h1>CLI Authorized</h1>
46c777932<p>You can close this tab and return to the terminal.</p>
46c777933</div></body></html>`;
e93a97834
e93a97835const ERROR_HTML = `<!DOCTYPE html>
46c777936<html><head><title>Grove CLI</title><style>${STYLES} h1{color:var(--error)}</style></head>
46c777937<body><div class="card"><div class="logo">${LOGO}</div>
46c777938<h1>Authentication Failed</h1>
46c777939<p>Please try again from the terminal.</p>
46c777940</div></body></html>`;
e93a97841
ff4354542export async function waitForAuthCallback(): Promise<{
e93a97843 port: number;
e93a97844 result: Promise<AuthResult>;
e93a97845 close: () => void;
ff4354546}> {
e93a97847 let resolveResult: (value: AuthResult) => void;
e93a97848 let rejectResult: (reason: Error) => void;
e93a97849
e93a97850 const result = new Promise<AuthResult>((resolve, reject) => {
e93a97851 resolveResult = resolve;
e93a97852 rejectResult = reject;
e93a97853 });
e93a97854
e93a97855 const server = createServer((req: IncomingMessage, res: ServerResponse) => {
e93a97856 const url = new URL(req.url || "/", `http://localhost`);
e93a97857
e93a97858 if (url.pathname === "/callback") {
e93a97859 const token = url.searchParams.get("token");
e93a97860
e93a97861 if (token) {
e93a97862 res.writeHead(200, { "Content-Type": "text/html" });
e93a97863 res.end(SUCCESS_HTML);
e93a97864 resolveResult!({ token });
e93a97865 } else {
e93a97866 res.writeHead(400, { "Content-Type": "text/html" });
e93a97867 res.end(ERROR_HTML);
e93a97868 rejectResult!(new Error("No token received"));
e93a97869 }
e93a97870 } else {
e93a97871 res.writeHead(404);
e93a97872 res.end("Not found");
e93a97873 }
e93a97874 });
e93a97875
ff4354576 const port = await new Promise<number>((resolve) => {
ff4354577 server.listen(0, "127.0.0.1", () => {
ff4354578 const addr = server.address();
ff4354579 resolve(typeof addr === "object" && addr ? addr.port : 0);
ff4354580 });
ff4354581 });
e93a97882
e93a97883 const timeout = setTimeout(() => {
e93a97884 server.close();
e93a97885 rejectResult!(new Error("Authentication timed out (5 minutes)"));
e93a97886 }, 5 * 60 * 1000);
e93a97887
e93a97888 const close = () => {
e93a97889 clearTimeout(timeout);
e93a97890 server.close();
e93a97891 };
e93a97892
e93a97893 result.finally(close);
e93a97894
e93a97895 return { port, result, close };
e93a97896}