fix(core): preserve OAuth refresh tokens during rotation and retrieval (#26924)

This commit is contained in:
Coco Sheng
2026-05-13 13:19:05 -04:00
committed by GitHub
parent 749657cbf9
commit 297d3a3067
6 changed files with 146 additions and 5 deletions

View File

@@ -242,6 +242,39 @@ describe('OAuthCredentialStorage', () => {
);
});
it('should merge existing refresh token when new payload lacks one', async () => {
const oldCredentials: OAuthCredentials = {
serverName: 'main-account',
token: {
accessToken: 'old-access-token',
refreshToken: 'persistent-refresh-token',
tokenType: 'Bearer',
expiresAt: Date.now() + 3600000,
scope: 'email',
},
updatedAt: Date.now(),
};
vi.spyOn(mockHybridTokenStorage, 'getCredentials').mockResolvedValue(
oldCredentials,
);
const newTokens: Credentials = {
access_token: 'new-access-token',
expiry_date: Date.now() + 3600000,
};
await OAuthCredentialStorage.saveCredentials(newTokens);
expect(mockHybridTokenStorage.setCredentials).toHaveBeenCalledWith(
expect.objectContaining({
token: expect.objectContaining({
accessToken: 'new-access-token',
refreshToken: 'persistent-refresh-token', // correctly merged
}),
}),
);
});
it('should throw an error if access_token is missing', async () => {
const invalidCredentials: Credentials = {
...mockCredentials,

View File

@@ -66,12 +66,16 @@ export class OAuthCredentialStorage {
throw new Error('Attempted to save credentials without an access token.');
}
const existing = await this.storage.getCredentials(MAIN_ACCOUNT_KEY);
const mergedRefreshToken =
credentials.refresh_token || existing?.token.refreshToken;
// Convert Google Credentials to OAuthCredentials format
const mcpCredentials: OAuthCredentials = {
serverName: MAIN_ACCOUNT_KEY,
token: {
accessToken: credentials.access_token,
refreshToken: credentials.refresh_token || undefined,
refreshToken: mergedRefreshToken || undefined,
tokenType: credentials.token_type || 'Bearer',
scope: credentials.scope || undefined,
expiresAt: credentials.expiry_date || undefined,