()
| 152 | let pendingOAuth: PendingOAuth | undefined |
| 153 | |
| 154 | async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> { |
| 155 | if (oauthServer) { |
| 156 | return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` } |
| 157 | } |
| 158 | |
| 159 | oauthServer = createServer((req, res) => { |
| 160 | const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`) |
| 161 | |
| 162 | if (url.pathname === "/auth/callback") { |
| 163 | const code = url.searchParams.get("code") |
| 164 | const state = url.searchParams.get("state") |
| 165 | const error = url.searchParams.get("error") |
| 166 | const errorDescription = url.searchParams.get("error_description") |
| 167 | |
| 168 | if (error) { |
| 169 | const errorMsg = errorDescription || error |
| 170 | pendingOAuth?.reject(new Error(errorMsg)) |
| 171 | pendingOAuth = undefined |
| 172 | res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }) |
| 173 | res.end(renderOAuthError(errorMsg)) |
| 174 | return |
| 175 | } |
| 176 | |
| 177 | if (!code) { |
| 178 | const errorMsg = "Missing authorization code" |
| 179 | pendingOAuth?.reject(new Error(errorMsg)) |
| 180 | pendingOAuth = undefined |
| 181 | res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }) |
| 182 | res.end(renderOAuthError(errorMsg)) |
| 183 | return |
| 184 | } |
| 185 | |
| 186 | if (!pendingOAuth || state !== pendingOAuth.state) { |
| 187 | const errorMsg = "Invalid state - potential CSRF attack" |
| 188 | pendingOAuth?.reject(new Error(errorMsg)) |
| 189 | pendingOAuth = undefined |
| 190 | res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }) |
| 191 | res.end(renderOAuthError(errorMsg)) |
| 192 | return |
| 193 | } |
| 194 | |
| 195 | const current = pendingOAuth |
| 196 | pendingOAuth = undefined |
| 197 | |
| 198 | exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce) |
| 199 | .then((tokens) => current.resolve(tokens)) |
| 200 | .catch((err) => current.reject(err)) |
| 201 | |
| 202 | res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }) |
| 203 | res.end(OauthCallbackPage.success({ provider: "ChatGPT" })) |
| 204 | return |
| 205 | } |
| 206 | |
| 207 | if (url.pathname === "/cancel") { |
| 208 | pendingOAuth?.reject(new Error("Login cancelled")) |
| 209 | pendingOAuth = undefined |
| 210 | res.writeHead(200) |
| 211 | res.end("Login cancelled") |
no test coverage detected