@hienlh/ppm 0.8.21 → 0.8.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.23] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Stop disabling accounts on background refresh failure**: Startup and background auto-refresh no longer disable accounts when refresh fails — only actual query-time failures disable accounts. Prevents all accounts being disabled after server restart.
|
|
7
|
+
- **Better refresh error logging**: Log response body from Anthropic when token refresh fails (was only logging status code)
|
|
8
|
+
|
|
9
|
+
## [0.8.22] - 2026-03-24
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **OAuth pre-flight token refresh**: Check token freshness before each SDK query and refresh proactively if expired or expiring within 60s
|
|
13
|
+
- **Auto-refresh on startup**: Run token refresh check immediately on server start instead of waiting 5 minutes for first interval
|
|
14
|
+
- **OAuth auto-refresh on auth failure**: Automatically refresh expired OAuth token and retry when SDK returns `authentication_failed`, instead of just showing error to user
|
|
15
|
+
|
|
3
16
|
## [0.8.21] - 2026-03-24
|
|
4
17
|
|
|
5
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -545,7 +545,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
545
545
|
// Account-based auth injection (multi-account mode)
|
|
546
546
|
// Fallback to existing env (ANTHROPIC_API_KEY) when no accounts configured.
|
|
547
547
|
const accountsEnabled = accountSelector.isEnabled();
|
|
548
|
-
|
|
548
|
+
let account = accountsEnabled ? accountSelector.next() : null;
|
|
549
549
|
if (accountsEnabled && !account) {
|
|
550
550
|
// All accounts in DB but none usable
|
|
551
551
|
const reason = accountSelector.lastFailReason;
|
|
@@ -557,7 +557,12 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
557
557
|
yield { type: "done" as const, sessionId, resultSubtype: "error_auth" };
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
|
+
// Pre-flight: ensure OAuth token is fresh before sending to SDK
|
|
560
561
|
if (account) {
|
|
562
|
+
const fresh = await accountService.ensureFreshToken(account.id);
|
|
563
|
+
if (fresh) {
|
|
564
|
+
account = fresh;
|
|
565
|
+
}
|
|
561
566
|
console.log(`[sdk] Using account ${account.id} (${account.email ?? "no-email"})`);
|
|
562
567
|
yield { type: "account_info" as const, accountId: account.id, accountLabel: account.label ?? account.email ?? "Unknown" };
|
|
563
568
|
}
|
|
@@ -116,6 +116,29 @@ class AccountService {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Ensure the access token for an OAuth account is still fresh.
|
|
121
|
+
* If it's expired or about to expire (within 60s), refresh it proactively.
|
|
122
|
+
* Returns the refreshed account with fresh tokens, or null if refresh failed.
|
|
123
|
+
*/
|
|
124
|
+
async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
|
|
125
|
+
const acc = this.getWithTokens(id);
|
|
126
|
+
if (!acc) return null;
|
|
127
|
+
// Only OAuth tokens need refresh
|
|
128
|
+
if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
|
|
129
|
+
if (!acc.expiresAt) return acc;
|
|
130
|
+
const nowS = Math.floor(Date.now() / 1000);
|
|
131
|
+
if (acc.expiresAt - nowS > 60) return acc; // still fresh
|
|
132
|
+
try {
|
|
133
|
+
console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s)`);
|
|
134
|
+
await this.refreshAccessToken(id, false);
|
|
135
|
+
return this.getWithTokens(id);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(`[accounts] Pre-flight refresh failed for ${id}:`, e);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
/** Find existing account by email or profile UUID */
|
|
120
143
|
private findDuplicate(email?: string | null, profileData?: OAuthProfileData | null): Account | null {
|
|
121
144
|
if (!email && !profileData?.account?.uuid) return null;
|
|
@@ -479,7 +502,12 @@ class AccountService {
|
|
|
479
502
|
};
|
|
480
503
|
}
|
|
481
504
|
|
|
482
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Refresh an OAuth access token using the stored refresh token.
|
|
507
|
+
* @param disableOnFail - if true, disable the account when refresh fails (default: true).
|
|
508
|
+
* Background/startup refresh should pass false to avoid disabling accounts prematurely.
|
|
509
|
+
*/
|
|
510
|
+
async refreshAccessToken(accountId: string, disableOnFail = true): Promise<void> {
|
|
483
511
|
const account = this.getWithTokens(accountId);
|
|
484
512
|
if (!account) throw new Error(`Account ${accountId} not found`);
|
|
485
513
|
const res = await fetch(OAUTH_TOKEN_URL, {
|
|
@@ -492,14 +520,19 @@ class AccountService {
|
|
|
492
520
|
}),
|
|
493
521
|
});
|
|
494
522
|
if (!res.ok) {
|
|
495
|
-
|
|
496
|
-
|
|
523
|
+
const errorBody = await res.text().catch(() => "");
|
|
524
|
+
console.error(`[accounts] Refresh failed for ${accountId}: ${res.status} ${errorBody}`);
|
|
525
|
+
if (disableOnFail) {
|
|
526
|
+
this.setDisabled(accountId);
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Token refresh failed for account ${accountId}: ${res.status} ${errorBody}`);
|
|
497
529
|
}
|
|
498
530
|
const data = await res.json() as {
|
|
499
531
|
access_token: string;
|
|
500
532
|
refresh_token?: string;
|
|
501
533
|
expires_in: number;
|
|
502
534
|
};
|
|
535
|
+
console.log(`[accounts] Token refreshed for ${account.email ?? accountId} (expires_in=${data.expires_in}s, new_refresh=${!!data.refresh_token})`);
|
|
503
536
|
this.updateTokens(
|
|
504
537
|
accountId,
|
|
505
538
|
data.access_token,
|
|
@@ -586,7 +619,7 @@ class AccountService {
|
|
|
586
619
|
const CHECK_INTERVAL_MS = 5 * 60_000;
|
|
587
620
|
const REFRESH_BUFFER_S = 5 * 60;
|
|
588
621
|
|
|
589
|
-
|
|
622
|
+
const refreshExpiring = async () => {
|
|
590
623
|
const accounts = this.list();
|
|
591
624
|
const nowS = Math.floor(Date.now() / 1000);
|
|
592
625
|
for (const acc of accounts) {
|
|
@@ -595,12 +628,16 @@ class AccountService {
|
|
|
595
628
|
if (acc.expiresAt - nowS > REFRESH_BUFFER_S) continue;
|
|
596
629
|
console.log(`[accounts] Auto-refreshing token for ${acc.email ?? acc.id}`);
|
|
597
630
|
try {
|
|
598
|
-
await this.refreshAccessToken(acc.id);
|
|
631
|
+
await this.refreshAccessToken(acc.id, false);
|
|
599
632
|
} catch (e) {
|
|
600
633
|
console.error(`[accounts] Auto-refresh failed for ${acc.id}:`, e);
|
|
601
634
|
}
|
|
602
635
|
}
|
|
603
|
-
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// Run immediately on startup, then every 5 minutes
|
|
639
|
+
refreshExpiring().catch(() => {});
|
|
640
|
+
this.refreshTimer = setInterval(refreshExpiring, CHECK_INTERVAL_MS);
|
|
604
641
|
|
|
605
642
|
if (typeof this.refreshTimer === "object" && this.refreshTimer !== null && "unref" in this.refreshTimer) {
|
|
606
643
|
(this.refreshTimer as NodeJS.Timeout).unref();
|