diff --git a/apps/x/apps/main/src/auth-server.ts b/apps/x/apps/main/src/auth-server.ts index 5c46ca3f..b1e93d67 100644 --- a/apps/x/apps/main/src/auth-server.ts +++ b/apps/x/apps/main/src/auth-server.ts @@ -20,9 +20,46 @@ export interface AuthServerResult { port: number; } +interface CallbackHandlingOpts { + callbackPath: string; + /** Invoked when the provider redirects back with an `error` param. */ + onError?: (error: string) => void; + /** + * Gatekeeper run BEFORE the error/callback handling. Return a message to + * reject the request with a polite close-this-tab error page — without + * invoking onCallback/onError. Lets a caller drop stale callbacks (e.g. the + * browser tab of a cancelled sign-in attempt carrying an old `state`) + * without disturbing the live flow. + */ + validateCallback?: (url: URL) => string | null; +} + +function renderErrorPage(res: import('http').ServerResponse, message: string): void { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + +
+${escapeHtml(message)}
+You can close this window.
+ + + + `); +} + function tryBindPort( port: number, - onCallback: (callbackUrl: URL) => void | PromiseError: ${escapeHtml(error)}
-You can close this window.
- - - - `); + // Surface the provider error (e.g. access_denied when the user + // cancels consent) so the caller can settle its flow instead of + // waiting for the timeout. Callers that don't opt in keep the old + // behaviour: the error page renders and the flow times out. + opts.onError?.(error); + renderErrorPage(res, `Error: ${error}`); return; } @@ -117,18 +149,29 @@ function tryBindPort( * through `port + PORT_RANGE_SIZE - 1` and binds the first available, handling * both EADDRINUSE and EACCES (the latter is common on Windows when * Hyper-V/WSL2 reserve the port). + * + * `callbackPath` overrides the served path for providers whose registered + * redirect URI differs (ChatGPT/Codex uses /auth/callback). `onError` is + * invoked when the provider redirects back with an `error` param (e.g. + * access_denied), letting the caller settle instead of waiting for timeout. + * `validateCallback` runs before both — see CallbackHandlingOpts. */ export async function createAuthServer( port: number = DEFAULT_PORT, onCallback: (callbackUrl: URL) => void | Promise