|
@@ -42,6 +42,46 @@ function generateState(): string {
|
|
|
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
|
|
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+export interface IdTokenClaims {
|
|
|
|
|
+ chatgpt_account_id?: string
|
|
|
|
|
+ organizations?: Array<{ id: string }>
|
|
|
|
|
+ email?: string
|
|
|
|
|
+ "https://api.openai.com/auth"?: {
|
|
|
|
|
+ chatgpt_account_id?: string
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function parseJwtClaims(token: string): IdTokenClaims | undefined {
|
|
|
|
|
+ const parts = token.split(".")
|
|
|
|
|
+ if (parts.length !== 3) return undefined
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString())
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return undefined
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined {
|
|
|
|
|
+ return (
|
|
|
|
|
+ claims.chatgpt_account_id ||
|
|
|
|
|
+ claims["https://api.openai.com/auth"]?.chatgpt_account_id ||
|
|
|
|
|
+ claims.organizations?.[0]?.id
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function extractAccountId(tokens: TokenResponse): string | undefined {
|
|
|
|
|
+ if (tokens.id_token) {
|
|
|
|
|
+ const claims = parseJwtClaims(tokens.id_token)
|
|
|
|
|
+ const accountId = claims && extractAccountIdFromClaims(claims)
|
|
|
|
|
+ if (accountId) return accountId
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tokens.access_token) {
|
|
|
|
|
+ const claims = parseJwtClaims(tokens.access_token)
|
|
|
|
|
+ return claims ? extractAccountIdFromClaims(claims) : undefined
|
|
|
|
|
+ }
|
|
|
|
|
+ return undefined
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string {
|
|
function buildAuthorizeUrl(redirectUri: string, pkce: PkceCodes, state: string): string {
|
|
|
const params = new URLSearchParams({
|
|
const params = new URLSearchParams({
|
|
|
response_type: "code",
|
|
response_type: "code",
|
|
@@ -380,10 +420,14 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|
|
const currentAuth = await getAuth()
|
|
const currentAuth = await getAuth()
|
|
|
if (currentAuth.type !== "oauth") return fetch(requestInput, init)
|
|
if (currentAuth.type !== "oauth") return fetch(requestInput, init)
|
|
|
|
|
|
|
|
|
|
+ // Cast to include accountId field
|
|
|
|
|
+ const authWithAccount = currentAuth as typeof currentAuth & { accountId?: string }
|
|
|
|
|
+
|
|
|
// Check if token needs refresh
|
|
// Check if token needs refresh
|
|
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
|
log.info("refreshing codex access token")
|
|
log.info("refreshing codex access token")
|
|
|
const tokens = await refreshAccessToken(currentAuth.refresh)
|
|
const tokens = await refreshAccessToken(currentAuth.refresh)
|
|
|
|
|
+ const newAccountId = extractAccountId(tokens) || authWithAccount.accountId
|
|
|
await input.client.auth.set({
|
|
await input.client.auth.set({
|
|
|
path: { id: "codex" },
|
|
path: { id: "codex" },
|
|
|
body: {
|
|
body: {
|
|
@@ -391,9 +435,11 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|
|
refresh: tokens.refresh_token,
|
|
refresh: tokens.refresh_token,
|
|
|
access: tokens.access_token,
|
|
access: tokens.access_token,
|
|
|
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
|
|
|
+ ...(newAccountId && { accountId: newAccountId }),
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
currentAuth.access = tokens.access_token
|
|
currentAuth.access = tokens.access_token
|
|
|
|
|
+ authWithAccount.accountId = newAccountId
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Build headers
|
|
// Build headers
|
|
@@ -415,20 +461,20 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|
|
// Set authorization header with access token
|
|
// Set authorization header with access token
|
|
|
headers.set("authorization", `Bearer ${currentAuth.access}`)
|
|
headers.set("authorization", `Bearer ${currentAuth.access}`)
|
|
|
|
|
|
|
|
- // Rewrite URL to Codex endpoint
|
|
|
|
|
- let url: URL
|
|
|
|
|
- if (typeof requestInput === "string") {
|
|
|
|
|
- url = new URL(requestInput)
|
|
|
|
|
- } else if (requestInput instanceof URL) {
|
|
|
|
|
- url = requestInput
|
|
|
|
|
- } else {
|
|
|
|
|
- url = new URL(requestInput.url)
|
|
|
|
|
|
|
+ // Set ChatGPT-Account-Id header for organization subscriptions
|
|
|
|
|
+ if (authWithAccount.accountId) {
|
|
|
|
|
+ headers.set("ChatGPT-Account-Id", authWithAccount.accountId)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // If this is a messages/responses request, redirect to Codex endpoint
|
|
|
|
|
- if (url.pathname.includes("/v1/responses") || url.pathname.includes("/chat/completions")) {
|
|
|
|
|
- url = new URL(CODEX_API_ENDPOINT)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Rewrite URL to Codex endpoint
|
|
|
|
|
+ const parsed =
|
|
|
|
|
+ requestInput instanceof URL
|
|
|
|
|
+ ? requestInput
|
|
|
|
|
+ : new URL(typeof requestInput === "string" ? requestInput : requestInput.url)
|
|
|
|
|
+ const url =
|
|
|
|
|
+ parsed.pathname.includes("/v1/responses") || parsed.pathname.includes("/chat/completions")
|
|
|
|
|
+ ? new URL(CODEX_API_ENDPOINT)
|
|
|
|
|
+ : parsed
|
|
|
|
|
|
|
|
return fetch(url, {
|
|
return fetch(url, {
|
|
|
...init,
|
|
...init,
|
|
@@ -456,11 +502,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
|
|
callback: async () => {
|
|
callback: async () => {
|
|
|
const tokens = await callbackPromise
|
|
const tokens = await callbackPromise
|
|
|
stopOAuthServer()
|
|
stopOAuthServer()
|
|
|
|
|
+ const accountId = extractAccountId(tokens)
|
|
|
return {
|
|
return {
|
|
|
type: "success" as const,
|
|
type: "success" as const,
|
|
|
refresh: tokens.refresh_token,
|
|
refresh: tokens.refresh_token,
|
|
|
access: tokens.access_token,
|
|
access: tokens.access_token,
|
|
|
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
|
|
|
+ accountId,
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|