11import { join } from 'node:path' ;
22
3- import { FileTokenStorage , resolveOAuthTokenStorageName } from '@pymodel/pythinker-code-oauth' ;
3+ import {
4+ FileTokenStorage ,
5+ refreshKimiOAuthToken ,
6+ refreshMiniMaxOAuthToken ,
7+ resolveOAuthTokenStorageName ,
8+ type TokenInfo ,
9+ } from '@pymodel/pythinker-code-oauth' ;
410
511import type { OAuthRef } from '../../config' ;
612import { ErrorCodes , PythinkerError } from '../../errors' ;
713import type { OAuthTokenProviderResolver } from '../../session/provider-manager' ;
814
15+ const REFRESH_BUFFER_SECONDS = 5 * 60 ;
16+
917export class OAuthTokenReader {
1018 private readonly storage : FileTokenStorage ;
19+ private readonly refreshInflight = new Map < string , Promise < TokenInfo > > ( ) ;
1120
1221 constructor ( homeDir : string ) {
1322 this . storage = new FileTokenStorage ( join ( homeDir , 'credentials' ) ) ;
@@ -24,14 +33,86 @@ export class OAuthTokenReader {
2433 readonly resolveOAuthTokenProvider : OAuthTokenProviderResolver = ( providerName , oauthRef ) => {
2534 if ( oauthRef === undefined || oauthRef . storage !== 'file' ) return undefined ;
2635 return {
27- getAccessToken : async ( ) => {
28- const token = await this . getCachedAccessToken ( oauthRef ) ;
29- if ( token !== undefined ) return token ;
30- throw new PythinkerError (
31- ErrorCodes . AUTH_LOGIN_REQUIRED ,
32- `OAuth provider "${ providerName } " requires login before it can be used.` ,
33- ) ;
36+ getAccessToken : async ( options ) => {
37+ const storageName = resolveOAuthTokenStorageName ( oauthRef . key ) ;
38+ const token = await this . storage . load ( storageName ) ;
39+ if ( token === undefined || token . accessToken . trim ( ) . length === 0 ) {
40+ throw loginRequired ( providerName ) ;
41+ }
42+ const nowSeconds = Math . floor ( Date . now ( ) / 1000 ) ;
43+ const force = options ?. force === true ;
44+ if ( ! force && token . expiresAt - nowSeconds > REFRESH_BUFFER_SECONDS ) return token . accessToken ;
45+ if ( token . refreshToken . trim ( ) . length === 0 || token . metadata ?. [ 'provider' ] === undefined ) {
46+ if ( ! force && token . expiresAt > nowSeconds ) return token . accessToken ;
47+ throw loginRequired ( providerName ) ;
48+ }
49+ try {
50+ return ( await this . refreshSingleFlight ( storageName , token ) ) . accessToken ;
51+ } catch ( error ) {
52+ if ( ! force && token . expiresAt > nowSeconds ) return token . accessToken ;
53+ throw error ;
54+ }
3455 } ,
3556 } ;
3657 } ;
58+
59+ private refreshSingleFlight ( storageName : string , token : TokenInfo ) : Promise < TokenInfo > {
60+ const existing = this . refreshInflight . get ( storageName ) ;
61+ if ( existing !== undefined ) return existing ;
62+ const refresh = this . refreshToken ( token )
63+ . then ( async ( next ) => {
64+ await this . storage . save ( storageName , next ) ;
65+ return next ;
66+ } )
67+ . finally ( ( ) => {
68+ if ( this . refreshInflight . get ( storageName ) === refresh ) this . refreshInflight . delete ( storageName ) ;
69+ } ) ;
70+ this . refreshInflight . set ( storageName , refresh ) ;
71+ return refresh ;
72+ }
73+
74+ private async refreshToken ( token : TokenInfo ) : Promise < TokenInfo > {
75+ const provider = token . metadata ?. [ 'provider' ] ;
76+ if ( provider === 'kimi' ) {
77+ const deviceId = token . metadata ?. [ 'deviceId' ] ;
78+ if ( deviceId === undefined || deviceId . length === 0 ) throw new Error ( 'Kimi OAuth credential is missing deviceId metadata.' ) ;
79+ const refreshed = await refreshKimiOAuthToken ( token . refreshToken , deviceId ) ;
80+ return toTokenInfo ( refreshed , token . metadata ) ;
81+ }
82+ if ( provider === 'minimax' ) {
83+ const region = token . metadata ?. [ 'region' ] ;
84+ if ( region !== 'global' && region !== 'cn' ) throw new Error ( 'MiniMax OAuth credential has invalid region metadata.' ) ;
85+ const refreshed = await refreshMiniMaxOAuthToken ( region , token . refreshToken ) ;
86+ return toTokenInfo ( refreshed , token . metadata ) ;
87+ }
88+ throw loginRequired ( provider ?? 'unknown' ) ;
89+ }
90+ }
91+
92+ function toTokenInfo (
93+ token : {
94+ readonly accessToken : string ;
95+ readonly refreshToken : string ;
96+ readonly expiresAtMs : number ;
97+ readonly scope : string ;
98+ readonly tokenType : string ;
99+ } ,
100+ metadata : Readonly < Record < string , string > > | undefined ,
101+ ) : TokenInfo {
102+ return {
103+ accessToken : token . accessToken ,
104+ refreshToken : token . refreshToken ,
105+ expiresAt : Math . floor ( token . expiresAtMs / 1000 ) ,
106+ expiresIn : Math . max ( 1 , Math . floor ( ( token . expiresAtMs - Date . now ( ) ) / 1000 ) ) ,
107+ scope : token . scope ,
108+ tokenType : token . tokenType ,
109+ metadata,
110+ } ;
111+ }
112+
113+ function loginRequired ( providerName : string ) : PythinkerError {
114+ return new PythinkerError (
115+ ErrorCodes . AUTH_LOGIN_REQUIRED ,
116+ `OAuth provider "${ providerName } " requires login before it can be used.` ,
117+ ) ;
37118}
0 commit comments