1import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
3import { providerAuthReturnUrl } from "./providerAuthReturnUrl.ts";
4
5export const CodexAuthHandoff = Schema.Struct({
6 authorizationUrl: Schema.String.check(Schema.isMaxLength(16_384)),
7 returnUrl: Schema.String.check(Schema.isMaxLength(4_096)),
8 environmentId: EnvironmentId,
9 instanceId: ProviderInstanceId,
10 flowId: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
11});
12export type CodexAuthHandoff = typeof CodexAuthHandoff.Type;
13const Delivery = Schema.Struct({
14 environmentId: EnvironmentId,
15 instanceId: ProviderInstanceId,
16 flowId: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
17 callbackUrl: Schema.String.check(Schema.isMaxLength(16_384)),
18 returnHash: Schema.String.check(Schema.isMaxLength(128)),
19});
20
21const encodeHandoff = Schema.encodeSync(Schema.fromJsonString(CodexAuthHandoff));
22const decodeHandoff = Schema.decodeUnknownSync(Schema.fromJsonString(CodexAuthHandoff));
23const encodeDelivery = Schema.encodeSync(Schema.fromJsonString(Delivery));
24const decodeDelivery = Schema.decodeUnknownSync(Schema.fromJsonString(Delivery));
25
26/** The helper only opens OpenAI's authorize endpoint and receives a loopback callback. */
27export function codexAuthorizationRequest(value: string) {
28 const url = new URL(value);
29 if (
30 value.length > 16_384 ||
31 url.origin !== "https://auth.openai.com" ||
32 url.pathname !== "/api/accounts/authorize" ||
33 url.username ||
34 url.password ||
35 url.hash
36 )
37 throw new Error("Invalid ChatGPT sign-in request.");
38 const single = (key: string) => {
39 const values = url.searchParams.getAll(key);
40 if (values.length !== 1 || !values[0]) throw new Error("Invalid ChatGPT sign-in request.");
41 return values[0];
42 };
43 const redirectUri = single("redirect_uri");
44 const redirect = new URL(redirectUri);
45 const state = single("state");
46 if (
47 !/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}\/auth\/callback$/u.test(redirectUri) ||
48 Number(redirect.port) > 65_535 ||
49 !/^[\w-]{16,128}$/u.test(state) ||
50 single("response_type") !== "code" ||
51 single("code_challenge_method") !== "S256" ||
52 !/^[\w-]{43}$/u.test(single("code_challenge")) ||
53 !/^(dynamic_agent_client|oaiapp_[\w-]+)$/u.test(single("client_id"))
54 )
55 throw new Error("Invalid ChatGPT sign-in request.");
56 return { authorizationUrl: url.toString(), redirectUri, state };
57}
58
59/** Validation is shared by the desktop listener and the environment receiving the code. */
60export function codexCallbackUrl(value: string, redirectUri: string, state: string) {
61 const callback = new URL(value);
62 const expected = new URL(redirectUri);
63 const states = callback.searchParams.getAll("state");
64 const codes = callback.searchParams.getAll("code");
65 const errors = callback.searchParams.getAll("error");
66 const clients = callback.searchParams.getAll("client_id");
67 if (
68 value.length > 16_384 ||
69 callback.origin !== expected.origin ||
70 callback.pathname !== expected.pathname ||
71 callback.username ||
72 callback.password ||
73 callback.hash ||
74 states.length !== 1 ||
75 states[0] !== state ||
76 clients.length > 1 ||
77 (clients.length === 1 && !/^oaiapp_[\w-]+$/u.test(clients[0]!)) ||
78 !(
79 (codes.length === 1 && Boolean(codes[0]) && errors.length === 0) ||
80 (errors.length === 1 && Boolean(errors[0]) && codes.length === 0)
81 )
82 )
83 throw new Error("This redirect URL does not belong to the current sign-in.");
84 return callback;
85}
86
87export function codexAuthHandoffUrl(input: CodexAuthHandoff, development = false) {
88 const url = new URL(`${development ? "t3code-dev" : "t3code"}://auth/codex`);
89 url.searchParams.set("request", encodeHandoff(input));
90 return url.toString();
91}
92
93export function readCodexAuthHandoff(value: string, development: boolean) {
94 try {
95 const url = new URL(value);
96 if (
97 value.length > 32_768 ||
98 url.protocol !== (development ? "t3code-dev:" : "t3code:") ||
99 url.host !== "auth" ||
100 url.pathname !== "/codex" ||
101 url.username ||
102 url.password ||
103 url.hash ||
104 url.searchParams.getAll("request").length !== 1
105 )
106 return undefined;
107 const input = decodeHandoff(url.searchParams.get("request"));
108 codexAuthorizationRequest(input.authorizationUrl);
109 if (!providerAuthReturnUrl(input.returnUrl)) return undefined;
110 return input;
111 } catch {
112 return undefined;
113 }
114}
115
116/** Codes travel in a fragment, never in hosted web requests or a token store on the helper. */
117export function codexAuthDeliveryUrl(input: CodexAuthHandoff, callbackUrl: string) {
118 const request = codexAuthorizationRequest(input.authorizationUrl);
119 codexCallbackUrl(callbackUrl, request.redirectUri, request.state);
120 const destination = providerAuthReturnUrl(input.returnUrl);
121 if (!destination) throw new Error("Invalid T3 Code return address.");
122 const url = new URL(destination);
123 const delivery = {
124 environmentId: input.environmentId,
125 instanceId: input.instanceId,
126 flowId: input.flowId,
127 callbackUrl,
128 returnHash: url.hash,
129 };
130 url.hash = `codex-auth=${encodeURIComponent(encodeDelivery(delivery))}`;
131 return url.toString();
132}
133
134export function readCodexAuthDelivery(value: string) {
135 try {
136 const url = new URL(value);
137 if (!url.hash.startsWith("#codex-auth=") || url.hash.length > 32_768) return undefined;
138 const input = decodeDelivery(decodeURIComponent(url.hash.slice("#codex-auth=".length)));
139 const destination = providerAuthReturnUrl(value);
140 if (!destination) return undefined;
141 const returnUrl = new URL(destination);
142 returnUrl.hash = input.returnHash;
143 const sanitized = providerAuthReturnUrl(returnUrl.toString());
144 if (!sanitized) return undefined;
145 return { ...input, returnUrl: sanitized };
146 } catch {
147 return undefined;
148 }
149}