Skip to content

Commit f2bd780

Browse files
committed
fix(auth): refresh persisted OAuth tokens in legacy runtime
1 parent 8449756 commit f2bd780

1 file changed

Lines changed: 89 additions & 8 deletions

File tree

‎packages/agent-core/src/services/auth/oauthToken.ts‎

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { 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

511
import type { OAuthRef } from '../../config';
612
import { ErrorCodes, PythinkerError } from '../../errors';
713
import type { OAuthTokenProviderResolver } from '../../session/provider-manager';
814

15+
const REFRESH_BUFFER_SECONDS = 5 * 60;
16+
917
export 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

Comments
 (0)