Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
This commit is contained in:
1 parent
2350f744e3
commit
07d0b4587d
1904 files changed
+1133
-282075
No files matched your search
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* OpenAI Codex (ChatGPT Plus/Pro subscription) — authorization-code + PKCE flow.
|
||||
*
|
||||
* Ported from the reference implementation in @mariozechner/pi-ai. Uses a
|
||||
* loopback callback server on the registered redirect URI.
|
||||
*/
|
||||
import { startOAuthCallbackServer } from "./oauth-callback-server";
|
||||
import { generatePkce } from "./pkce";
|
||||
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
|
||||
|
||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
||||
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
||||
const CALLBACK_PORT = 1455;
|
||||
const CALLBACK_PATH = "/auth/callback";
|
||||
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
||||
const SCOPE = "openid profile email offline_access";
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
||||
|
||||
function buildAuthorizeUrl(challenge: string, state: string): string {
|
||||
const url = new URL(AUTHORIZE_URL);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("client_id", CLIENT_ID);
|
||||
url.searchParams.set("redirect_uri", REDIRECT_URI);
|
||||
url.searchParams.set("scope", SCOPE);
|
||||
url.searchParams.set("code_challenge", challenge);
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("id_token_add_organizations", "true");
|
||||
url.searchParams.set("codex_cli_simplified_flow", "true");
|
||||
url.searchParams.set("originator", "agentcompany");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function decodeJwtClaims(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return null;
|
||||
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract the ChatGPT account id embedded in the access token (unit-tested). */
|
||||
export function extractAccountId(accessToken: string): string | null {
|
||||
const claims = decodeJwtClaims(accessToken);
|
||||
const auth = claims?.[JWT_CLAIM_PATH];
|
||||
if (auth && typeof auth === "object" && "chatgpt_account_id" in auth) {
|
||||
const accountId = (auth as { chatgpt_account_id?: unknown }).chatgpt_account_id;
|
||||
if (typeof accountId === "string" && accountId.length > 0) return accountId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function exchangeCode(code: string, verifier: string): Promise<{ access: string; refresh: string; expires: number }> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
client_id: CLIENT_ID,
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(`Token exchange failed: ${response.status} ${text}`);
|
||||
}
|
||||
const json = (await response.json()) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
|
||||
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
|
||||
throw new Error("Token response was missing required fields.");
|
||||
}
|
||||
return { access: json.access_token, refresh: json.refresh_token, expires: Date.now() + json.expires_in * 1000 };
|
||||
}
|
||||
|
||||
/** Refresh an expired Codex access token using the long-lived refresh token. */
|
||||
export async function refreshCodex(refreshToken: string): Promise<OAuthCredentials> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: CLIENT_ID }),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
|
||||
const json = JSON.parse(text) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
|
||||
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
|
||||
throw new Error("Codex token refresh response was missing required fields.");
|
||||
}
|
||||
const accountId = extractAccountId(json.access_token);
|
||||
if (!accountId) throw new Error("Could not read the ChatGPT account id from the refreshed token.");
|
||||
return { access: json.access_token, refresh: json.refresh_token, expires: Date.now() + json.expires_in * 1000, accountId };
|
||||
}
|
||||
|
||||
export const openaiCodexFlow: OAuthFlow = {
|
||||
providerKey: "gpt-account",
|
||||
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
||||
const { verifier, challenge } = generatePkce();
|
||||
const state = generatePkce().verifier;
|
||||
const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal });
|
||||
try {
|
||||
callbacks.onAuth({ url: buildAuthorizeUrl(challenge, state), instructions: "Complete the ChatGPT sign-in in your browser." });
|
||||
const result = await server.waitForCode();
|
||||
if (result.state !== state) throw new Error("OAuth state mismatch.");
|
||||
const tokens = await exchangeCode(result.code, verifier);
|
||||
const accountId = extractAccountId(tokens.access);
|
||||
if (!accountId) throw new Error("Could not read the ChatGPT account id from the token.");
|
||||
return { access: tokens.access, refresh: tokens.refresh, expires: tokens.expires, accountId };
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user