mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-31 22:32:28 +00:00
fix: add ChatGPT-Account-Id header for organization subscriptions (#7603)
This commit is contained in:
committed by
GitHub
parent
dfe3e79304
commit
1662e149b3
@@ -12,6 +12,7 @@ export namespace Auth {
|
||||
refresh: z.string(),
|
||||
access: z.string(),
|
||||
expires: z.number(),
|
||||
accountId: z.string().optional(),
|
||||
enterpriseUrl: z.string().optional(),
|
||||
})
|
||||
.meta({ ref: "OAuth" })
|
||||
|
||||
@@ -42,6 +42,46 @@ function generateState(): string {
|
||||
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 {
|
||||
const params = new URLSearchParams({
|
||||
response_type: "code",
|
||||
@@ -380,10 +420,14 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
const currentAuth = await getAuth()
|
||||
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
|
||||
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
||||
log.info("refreshing codex access token")
|
||||
const tokens = await refreshAccessToken(currentAuth.refresh)
|
||||
const newAccountId = extractAccountId(tokens) || authWithAccount.accountId
|
||||
await input.client.auth.set({
|
||||
path: { id: "codex" },
|
||||
body: {
|
||||
@@ -391,9 +435,11 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
refresh: tokens.refresh_token,
|
||||
access: tokens.access_token,
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
...(newAccountId && { accountId: newAccountId }),
|
||||
},
|
||||
})
|
||||
currentAuth.access = tokens.access_token
|
||||
authWithAccount.accountId = newAccountId
|
||||
}
|
||||
|
||||
// Build headers
|
||||
@@ -415,20 +461,20 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
// Set authorization header with access token
|
||||
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, {
|
||||
...init,
|
||||
@@ -456,11 +502,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
|
||||
callback: async () => {
|
||||
const tokens = await callbackPromise
|
||||
stopOAuthServer()
|
||||
const accountId = extractAccountId(tokens)
|
||||
return {
|
||||
type: "success" as const,
|
||||
refresh: tokens.refresh_token,
|
||||
access: tokens.access_token,
|
||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||
accountId,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user