Skip to content
Source
Sign inSign up
Open History to see the last commit

codexAuthHandoff.ts

151 lines5.8 KB

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​
26export function codexAuthorizationRequest(value: string) {
27 const url = new URL(value);
28 if (
29 value.length > 16_384 ||
30 url.origin !== "https://auth.openai.com" ||
31 url.pathname !== "/api/accounts/authorize" ||
32 url.username ||
33 url.password ||
34 url.hash
35 )
36 throw new Error("Invalid ChatGPT sign-in request.");
37 const single = (key: string) => {
38 const values = url.searchParams.getAll(key);
39 if (values.length !== 1 || !values[0]) throw new Error("Invalid ChatGPT sign-in request.");
40 return values[0];
41 };
42 const redirectUri = single("redirect_uri");
43 const redirect = new URL(redirectUri);
44 const state = single("state");
45 if (
46 !/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}\/auth\/callback$/u.test(redirectUri) ||
47 Number(redirect.port) > 65_535 ||
48 !/^[\w-]{16,128}$/u.test(state) ||
49 single("response_type") !== "code" ||
50 single("code_challenge_method") !== "S256" ||
51 !/^[\w-]{43}$/u.test(single("code_challenge")) ||
52 !/^(dynamic_agent_client|oaiapp_[\w-]+)$/u.test(single("client_id"))
53 )
54 throw new Error("Invalid ChatGPT sign-in request.");
55 return { authorizationUrl: url.toString(), redirectUri, state };
57​
58export function codexCallbackUrl(value: string, redirectUri: string, state: string) {
59 const expected = new URL(redirectUri);
60 const pasted = value.trimStart();
61 // Browsers can hide `http://` in the address bar, so a pasted callback may lack it.
62 const callback = new URL(
63 /^[a-z][a-z\d+.-]*:\/\//iu.test(pasted) ? pasted : `${expected.protocol}//${pasted}`,
64 );
65 const states = callback.searchParams.getAll("state");
66 const codes = callback.searchParams.getAll("code");
67 const errors = callback.searchParams.getAll("error");
68 const clients = callback.searchParams.getAll("client_id");
69 if (
70 value.length > 16_384 ||
71 callback.origin !== expected.origin ||
72 callback.pathname !== expected.pathname ||
73 callback.username ||
74 callback.password ||
75 callback.hash ||
76 states.length !== 1 ||
77 states[0] !== state ||
78 clients.length > 1 ||
79 (clients.length === 1 && !/^oaiapp_[\w-]+$/u.test(clients[0]!)) ||
80 !(
81 (codes.length === 1 && Boolean(codes[0]) && errors.length === 0) ||
82 (errors.length === 1 && Boolean(errors[0]) && codes.length === 0)
83 )
84 )
85 throw new Error("This redirect URL does not belong to the current sign-in.");
86 return callback;
88​
89export function codexAuthHandoffUrl(input: CodexAuthHandoff, development = false) {
90 const url = new URL(`${development ? "t3code-dev" : "t3code"}://auth/codex`);
91 url.searchParams.set("request", encodeHandoff(input));
92 return url.toString();
94​
95export function readCodexAuthHandoff(value: string, development: boolean) {
96 try {
97 const url = new URL(value);
98 if (
99 value.length > 32_768 ||
100 url.protocol !== (development ? "t3code-dev:" : "t3code:") ||
101 url.host !== "auth" ||
102 url.pathname !== "/codex" ||
103 url.username ||
104 url.password ||
105 url.hash ||
106 url.searchParams.getAll("request").length !== 1
107 )
108 return undefined;
109 const input = decodeHandoff(url.searchParams.get("request"));
110 codexAuthorizationRequest(input.authorizationUrl);
111 if (!providerAuthReturnUrl(input.returnUrl)) return undefined;
112 return input;
113 } catch {
114 return undefined;
115 }
117​
118/** Codes travel in a fragment, never in hosted web requests or a token store on the helper. */
119export function codexAuthDeliveryUrl(input: CodexAuthHandoff, callbackUrl: string) {
120 const request = codexAuthorizationRequest(input.authorizationUrl);
121 codexCallbackUrl(callbackUrl, request.redirectUri, request.state);
122 const destination = providerAuthReturnUrl(input.returnUrl);
123 if (!destination) throw new Error("Invalid T3 Code return address.");
124 const url = new URL(destination);
125 const delivery = {
126 environmentId: input.environmentId,
127 instanceId: input.instanceId,
128 flowId: input.flowId,
129 callbackUrl,
130 returnHash: url.hash,
131 };
132 url.hash = `codex-auth=${encodeURIComponent(encodeDelivery(delivery))}`;
133 return url.toString();
135​
136export function readCodexAuthDelivery(value: string) {
137 try {
138 const url = new URL(value);
139 if (!url.hash.startsWith("#codex-auth=") || url.hash.length > 32_768) return undefined;
140 const input = decodeDelivery(decodeURIComponent(url.hash.slice("#codex-auth=".length)));
141 const destination = providerAuthReturnUrl(value);
142 if (!destination) return undefined;
143 const returnUrl = new URL(destination);
144 returnUrl.hash = input.returnHash;
145 const sanitized = providerAuthReturnUrl(returnUrl.toString());
146 if (!sanitized) return undefined;
147 return { ...input, returnUrl: sanitized };
148 } catch {
149 return undefined;
150 }