@@ -42,6 +42,46 @@ function generateState(): string {
4242 return base64UrlEncode ( crypto . getRandomValues ( new Uint8Array ( 32 ) ) . buffer )
4343}
4444
45+ export interface IdTokenClaims {
46+ chatgpt_account_id ?: string
47+ organizations ?: Array < { id : string } >
48+ email ?: string
49+ "https://api.openai.com/auth" ?: {
50+ chatgpt_account_id ?: string
51+ }
52+ }
53+
54+ export function parseJwtClaims ( token : string ) : IdTokenClaims | undefined {
55+ const parts = token . split ( "." )
56+ if ( parts . length !== 3 ) return undefined
57+ try {
58+ return JSON . parse ( Buffer . from ( parts [ 1 ] , "base64url" ) . toString ( ) )
59+ } catch {
60+ return undefined
61+ }
62+ }
63+
64+ export function extractAccountIdFromClaims ( claims : IdTokenClaims ) : string | undefined {
65+ return (
66+ claims . chatgpt_account_id ||
67+ claims [ "https://api.openai.com/auth" ] ?. chatgpt_account_id ||
68+ claims . organizations ?. [ 0 ] ?. id
69+ )
70+ }
71+
72+ export function extractAccountId ( tokens : TokenResponse ) : string | undefined {
73+ if ( tokens . id_token ) {
74+ const claims = parseJwtClaims ( tokens . id_token )
75+ const accountId = claims && extractAccountIdFromClaims ( claims )
76+ if ( accountId ) return accountId
77+ }
78+ if ( tokens . access_token ) {
79+ const claims = parseJwtClaims ( tokens . access_token )
80+ return claims ? extractAccountIdFromClaims ( claims ) : undefined
81+ }
82+ return undefined
83+ }
84+
4585function buildAuthorizeUrl ( redirectUri : string , pkce : PkceCodes , state : string ) : string {
4686 const params = new URLSearchParams ( {
4787 response_type : "code" ,
@@ -380,20 +420,26 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
380420 const currentAuth = await getAuth ( )
381421 if ( currentAuth . type !== "oauth" ) return fetch ( requestInput , init )
382422
423+ // Cast to include accountId field
424+ const authWithAccount = currentAuth as typeof currentAuth & { accountId ?: string }
425+
383426 // Check if token needs refresh
384427 if ( ! currentAuth . access || currentAuth . expires < Date . now ( ) ) {
385428 log . info ( "refreshing codex access token" )
386429 const tokens = await refreshAccessToken ( currentAuth . refresh )
430+ const newAccountId = extractAccountId ( tokens ) || authWithAccount . accountId
387431 await input . client . auth . set ( {
388432 path : { id : "codex" } ,
389433 body : {
390434 type : "oauth" ,
391435 refresh : tokens . refresh_token ,
392436 access : tokens . access_token ,
393437 expires : Date . now ( ) + ( tokens . expires_in ?? 3600 ) * 1000 ,
438+ ...( newAccountId && { accountId : newAccountId } ) ,
394439 } ,
395440 } )
396441 currentAuth . access = tokens . access_token
442+ authWithAccount . accountId = newAccountId
397443 }
398444
399445 // Build headers
@@ -415,20 +461,20 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
415461 // Set authorization header with access token
416462 headers . set ( "authorization" , `Bearer ${ currentAuth . access } ` )
417463
418- // Rewrite URL to Codex endpoint
419- let url : URL
420- if ( typeof requestInput === "string" ) {
421- url = new URL ( requestInput )
422- } else if ( requestInput instanceof URL ) {
423- url = requestInput
424- } else {
425- url = new URL ( requestInput . url )
464+ // Set ChatGPT-Account-Id header for organization subscriptions
465+ if ( authWithAccount . accountId ) {
466+ headers . set ( "ChatGPT-Account-Id" , authWithAccount . accountId )
426467 }
427468
428- // If this is a messages/responses request, redirect to Codex endpoint
429- if ( url . pathname . includes ( "/v1/responses" ) || url . pathname . includes ( "/chat/completions" ) ) {
430- url = new URL ( CODEX_API_ENDPOINT )
431- }
469+ // Rewrite URL to Codex endpoint
470+ const parsed =
471+ requestInput instanceof URL
472+ ? requestInput
473+ : new URL ( typeof requestInput === "string" ? requestInput : requestInput . url )
474+ const url =
475+ parsed . pathname . includes ( "/v1/responses" ) || parsed . pathname . includes ( "/chat/completions" )
476+ ? new URL ( CODEX_API_ENDPOINT )
477+ : parsed
432478
433479 return fetch ( url , {
434480 ...init ,
@@ -456,11 +502,13 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
456502 callback : async ( ) => {
457503 const tokens = await callbackPromise
458504 stopOAuthServer ( )
505+ const accountId = extractAccountId ( tokens )
459506 return {
460507 type : "success" as const ,
461508 refresh : tokens . refresh_token ,
462509 access : tokens . access_token ,
463510 expires : Date . now ( ) + ( tokens . expires_in ?? 3600 ) * 1000 ,
511+ accountId,
464512 }
465513 } ,
466514 }
0 commit comments