Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/auth/account-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class AccountPool {
return this.registry.removeAccount(id);
}

updateToken(entryId: string, newToken: string, refreshToken?: string | null): void {
updateToken(entryId: string, newToken: string, refreshToken?: string): void {
this.registry.updateToken(entryId, newToken, refreshToken);
}

Expand Down
11 changes: 6 additions & 5 deletions src/auth/account-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export class AccountRegistry {
if (accountId) {
if (existing.accountId === accountId && existing.userId === userId) {
existing.token = token;
if (refreshToken !== undefined) {
existing.refreshToken = refreshToken ?? null;
if (typeof refreshToken === "string" && refreshToken.length > 0) {
existing.refreshToken = refreshToken;
}
existing.email = profile?.email ?? existing.email;
existing.planType = profile?.chatgpt_plan_type ?? existing.planType;
Expand Down Expand Up @@ -99,13 +99,14 @@ export class AccountRegistry {
return deleted;
}

updateToken(entryId: string, newToken: string, refreshToken?: string | null): void {
updateToken(entryId: string, newToken: string, refreshToken?: string): void {
const entry = this.accounts.get(entryId);
if (!entry) return;

entry.token = newToken;
if (refreshToken !== undefined) {
entry.refreshToken = refreshToken ?? null;
// Never clear an existing RT — only replace with a new non-empty value
if (typeof refreshToken === "string" && refreshToken.length > 0) {
entry.refreshToken = refreshToken;
}
const profile = extractUserProfile(newToken);
entry.email = profile?.email ?? entry.email;
Expand Down
18 changes: 5 additions & 13 deletions src/auth/refresh-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,12 @@ export class RefreshScheduler {
const isOneTimeRT = entry.refreshToken.startsWith("oaistb_rt_");
const tokens = await refreshAccessToken(entry.refreshToken, accountProxyUrl);

// oaistb_rt_ tokens are single-use: the old RT is revoked after exchange.
// If the server didn't return a new RT, mark the account as unable to refresh.
if (isOneTimeRT && !tokens.refresh_token) {
console.warn(`[RefreshScheduler] Account ${entryId}: oaistb_rt_ used but no new RT returned — future refresh will fail`);
this.pool.updateToken(entryId, tokens.access_token, null);
this.scheduleOne(entryId, tokens.access_token);
return;
// updateToken guards against clearing RT — safe to pass tokens.refresh_token directly.
// If the server returned no new RT, the existing one is preserved.
if (!tokens.refresh_token) {
console.warn(`[RefreshScheduler] Account ${entryId}: server returned no new RT, keeping existing`);
}

this.pool.updateToken(
entryId,
tokens.access_token,
tokens.refresh_token,
);
this.pool.updateToken(entryId, tokens.access_token, tokens.refresh_token ?? undefined);
const rtType = isOneTimeRT ? " (oaistb_rt_ → rotated)" : "";
console.log(`[RefreshScheduler] Account ${entryId} refreshed successfully${rtType}`);
this.scheduleOne(entryId, tokens.access_token);
Expand Down
Loading