| 60 | } |
| 61 | |
| 62 | async function startOAuthServer(): Promise<void> { |
| 63 | if (oauthServer) return |
| 64 | oauthServer = createServer((req, res) => { |
| 65 | const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`) |
| 66 | |
| 67 | if (req.method === "GET" && url.pathname === OAUTH_REDIRECT_PATH) { |
| 68 | res.writeHead(200, { "Content-Type": "text/html" }) |
| 69 | res.end(OauthCallbackPage.bootstrap({ tokenPath: OAUTH_TOKEN_PATH, provider: "DigitalOcean" })) |
| 70 | return |
| 71 | } |
| 72 | |
| 73 | if (req.method === "POST" && url.pathname === OAUTH_TOKEN_PATH) { |
| 74 | const chunks: Buffer[] = [] |
| 75 | req.on("data", (chunk: Buffer) => chunks.push(chunk)) |
| 76 | req.on("end", () => { |
| 77 | const raw = Buffer.concat(chunks).toString("utf8") |
| 78 | let body: Record<string, string> = {} |
| 79 | try { |
| 80 | body = raw ? JSON.parse(raw) : {} |
| 81 | } catch { |
| 82 | body = {} |
| 83 | } |
| 84 | if (!pendingOAuth) { |
| 85 | res.writeHead(409, { "Content-Type": "application/json" }) |
| 86 | res.end(JSON.stringify({ error: "no_pending_oauth" })) |
| 87 | return |
| 88 | } |
| 89 | if (body.error) { |
| 90 | const message = body.error_description || body.error || "OAuth error" |
| 91 | pendingOAuth.reject(new Error(String(message))) |
| 92 | pendingOAuth = undefined |
| 93 | res.writeHead(200, { "Content-Type": "application/json" }) |
| 94 | res.end(JSON.stringify({ ok: true })) |
| 95 | return |
| 96 | } |
| 97 | if (!body.access_token) { |
| 98 | pendingOAuth.reject(new Error("Missing access_token in callback")) |
| 99 | pendingOAuth = undefined |
| 100 | res.writeHead(400, { "Content-Type": "application/json" }) |
| 101 | res.end(JSON.stringify({ error: "missing_access_token" })) |
| 102 | return |
| 103 | } |
| 104 | if (body.state !== pendingOAuth.state) { |
| 105 | pendingOAuth.reject(new Error("Invalid state - potential CSRF attack")) |
| 106 | pendingOAuth = undefined |
| 107 | res.writeHead(400, { "Content-Type": "application/json" }) |
| 108 | res.end(JSON.stringify({ error: "invalid_state" })) |
| 109 | return |
| 110 | } |
| 111 | const expires = parseInt(body.expires_in || "0", 10) |
| 112 | pendingOAuth.resolve({ |
| 113 | access_token: body.access_token, |
| 114 | expires_in: Number.isFinite(expires) && expires > 0 ? expires : 60 * 60 * 24 * 30, |
| 115 | state: body.state, |
| 116 | }) |
| 117 | pendingOAuth = undefined |
| 118 | res.writeHead(200, { "Content-Type": "application/json" }) |
| 119 | res.end(JSON.stringify({ ok: true })) |