import { Command } from "commander"; import { createServer } from "node:http"; import { randomBytes } from "node:crypto"; import open from "open"; import { saveConfig, getConfig } from "../lib/config.js"; export const loginCommand = new Command("login") .description("Authenticate with skills-hub.ai") .option("--api-key ", "Authenticate with an API key instead of OAuth") .option("--provider ", "OAuth provider: github or google", "github") .action(async (options) => { if (options.apiKey) { saveConfig({ apiKey: options.apiKey }); console.log("API key saved. You are now authenticated."); return; } const provider: "github" | "google" = options.provider === "google" ? "google" : "github"; console.log( `Opening ${provider === "github" ? "GitHub" : "Google"} login in your browser...`, ); // Generate CSRF state parameter const expectedState = randomBytes(32).toString("hex"); // Start local server to receive OAuth callback const port = 9876; const server = createServer(async (req, res) => { const url = new URL(req.url!, `http://localhost:${port}`); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code) { res.writeHead(400); res.end("Missing code parameter"); return; } // Validate CSRF state parameter if (state !== expectedState) { res.writeHead(403, { "Content-Type": "text/html" }); res.end( "

Authentication failed

Invalid state parameter (possible CSRF attack).

", ); console.error("Login failed: state parameter mismatch"); server.close(); process.exit(1); return; } try { const config = getConfig(); const tokenRes = await fetch( `${config.apiUrl}/api/v1/auth/${provider}/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, state }), }, ); if (!tokenRes.ok) throw new Error("Auth failed"); const data = (await tokenRes.json()) as { accessToken: string; user: { username: string }; }; saveConfig({ accessToken: data.accessToken }); res.writeHead(200, { "Content-Type": "text/html" }); res.end("

Authenticated!

You can close this tab.

"); console.log(`\nLogged in as ${data.user.username}`); server.close(); process.exit(0); } catch (err) { res.writeHead(500); res.end("Authentication failed"); console.error( "Login failed:", err instanceof Error ? err.message : err, ); server.close(); process.exit(1); } }); server.listen(port, () => { const config = getConfig(); const redirectUri = encodeURIComponent(`http://localhost:${port}`); open( `${config.apiUrl}/api/v1/auth/${provider}?redirect_uri=${redirectUri}&state=${expectedState}`, ); }); // Timeout the login flow after 120 seconds to avoid hanging indefinitely setTimeout(() => { console.error( "\nLogin timed out, no callback received within 2 minutes.", ); server.close(); process.exit(1); }, 120_000).unref(); });