1// @effect-diagnostics nodeBuiltinImport:off globalTimers:off - Native loopback helper uses a bounded Node listener with AbortController cleanup.
2import * as Schema from "effect/Schema";
3import * as NodeHttp from "node:http";
4import { codexAuthorizationRequest, codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff";
5
6export class CodexAuthCallbackError extends Schema.TaggedError<CodexAuthCallbackError>()(
7 "CodexAuthCallbackError",
8 { detail: Schema.String },
9) {
10 override get message() {
11 return this.detail;
12 }
13}
14
15const listeners = new Map<string, AbortController>();
16
17export function cancelCodexAuthCallback(authorizationUrl: string) {
18 const { state } = codexAuthorizationRequest(authorizationUrl);
19 listeners.get(state)?.abort();
20}
21
22/** Receive an authorization code locally. Credentials and PKCE stay on the target environment. */
23export async function receiveCodexAuthCallback(
24 authorizationUrl: string,
25 openBrowser: (url: string) => Promise<boolean>,
26 destination?: (callbackUrl: string) => string,
27 signal?: AbortSignal,
28) {
29 const request = codexAuthorizationRequest(authorizationUrl);
30 if (listeners.has(request.state))
31 throw new Error("This sign-in is already open on this computer.");
32 const abort = new AbortController();
33 const interrupted = () => abort.abort();
34 signal?.addEventListener("abort", interrupted, { once: true });
35 listeners.set(request.state, abort);
36 const callback = Promise.withResolvers<string>();
37 // Keep early open/bind failures from leaving an unobserved rejection behind.
38 void callback.promise.catch(() => undefined);
39 const server = NodeHttp.createServer((incoming, response) => {
40 try {
41 if (incoming.method !== "GET") throw new Error("method");
42 const url = codexCallbackUrl(
43 new URL(incoming.url ?? "/", request.redirectUri).toString(),
44 request.redirectUri,
45 request.state,
46 ).toString();
47 const returnUrl = destination?.(url);
48 response.setHeader("cache-control", "no-store");
49 response.setHeader("referrer-policy", "no-referrer");
50 response.setHeader("x-content-type-options", "nosniff");
51 if (returnUrl) {
52 response.writeHead(303, { location: returnUrl }).end();
53 } else {
54 response.setHeader(
55 "content-security-policy",
56 "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'",
57 );
58 response
59 .writeHead(200, { "content-type": "text/html; charset=utf-8" })
60 .end(
61 '<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>T3 Code</title><style>body{font-family:system-ui;display:grid;place-items:center;min-height:90vh;margin:0}main{max-width:360px;padding:32px}h1{font-size:24px}p{line-height:1.6;opacity:.7}</style></head><body><main><h1>Return to T3 Code</h1><p>Your sign-in response has been received. T3 Code is finishing the connection. You can close this tab.</p></main></body></html>',
62 );
63 }
64 callback.resolve(url);
65 } catch {
66 response.writeHead(400).end("This response does not belong to the active sign-in.");
67 }
68 });
69 const cancelled = () => callback.reject(new Error("Sign-in cancelled on this computer."));
70 abort.signal.addEventListener("abort", cancelled, { once: true });
71 const timer = setTimeout(
72 () => callback.reject(new Error("Sign-in expired. Try again.")),
73 300_000,
74 );
75 timer.unref();
76 try {
77 if (signal?.aborted) throw new Error("Sign-in cancelled on this computer.");
78 await new Promise<void>((resolve, reject) => {
79 server.once("error", () =>
80 reject(
81 new Error(
82 "The ChatGPT callback port is in use on this computer. Close the other sign-in and try again, or paste the redirect URL in T3 Code.",
83 ),
84 ),
85 );
86 server.listen(Number(new URL(request.redirectUri).port), "127.0.0.1", resolve);
87 });
88 if (abort.signal.aborted) throw new Error("Sign-in cancelled on this computer.");
89 if (!(await openBrowser(request.authorizationUrl)))
90 throw new Error("Could not open your sign-in browser.");
91 return await callback.promise;
92 } finally {
93 clearTimeout(timer);
94 signal?.removeEventListener("abort", interrupted);
95 abort.signal.removeEventListener("abort", cancelled);
96 listeners.delete(request.state);
97 server.closeAllConnections();
98 await new Promise<void>((resolve) => server.close(() => resolve()));
99 }
100}