@hienlh/ppm 0.11.1 → 0.11.2
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,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.2] - 2026-04-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Token refresh on 401**: Force actual OAuth refresh when API returns 401, even if token appears fresh by expiry timestamp — fixes intermittent "Token refreshed" message that didn't actually refresh
|
|
7
|
+
|
|
3
8
|
## [0.11.1] - 2026-04-19
|
|
4
9
|
|
|
5
10
|
### Added
|
package/package.json
CHANGED
|
@@ -916,9 +916,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
916
916
|
if (subtype === "api_retry" && (msg as any).error_status === 401 && account && !authRetried) {
|
|
917
917
|
authRetried = true;
|
|
918
918
|
try {
|
|
919
|
-
//
|
|
920
|
-
|
|
921
|
-
await accountService.refreshAccessToken(account.id, false);
|
|
919
|
+
// force=true: token got 401, so it's invalid regardless of expiresAt
|
|
920
|
+
await accountService.refreshAccessToken(account.id, false, true);
|
|
922
921
|
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
923
922
|
if (refreshedAccount) {
|
|
924
923
|
const label = refreshedAccount.label ?? refreshedAccount.email ?? "Unknown";
|
|
@@ -1093,9 +1092,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1093
1092
|
if (assistantError === "authentication_failed" && account && !authRetried) {
|
|
1094
1093
|
authRetried = true;
|
|
1095
1094
|
try {
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
await accountService.refreshAccessToken(account.id, false);
|
|
1095
|
+
// force=true: token got 401, so it's invalid regardless of expiresAt
|
|
1096
|
+
await accountService.refreshAccessToken(account.id, false, true);
|
|
1099
1097
|
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
1100
1098
|
if (refreshedAccount) {
|
|
1101
1099
|
const label = refreshedAccount.label ?? refreshedAccount.email ?? "Unknown";
|
|
@@ -1272,9 +1270,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1272
1270
|
if (!authRetried) {
|
|
1273
1271
|
authRetried = true;
|
|
1274
1272
|
try {
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1277
|
-
await accountService.refreshAccessToken(account.id, false);
|
|
1273
|
+
// force=true: token got 401, so it's invalid regardless of expiresAt
|
|
1274
|
+
await accountService.refreshAccessToken(account.id, false, true);
|
|
1278
1275
|
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
1279
1276
|
if (refreshedAccount) {
|
|
1280
1277
|
const label = refreshedAccount.label ?? refreshedAccount.email ?? "Unknown";
|
|
@@ -527,8 +527,10 @@ class AccountService {
|
|
|
527
527
|
* Also skips the OAuth call if the DB token was already refreshed by another session.
|
|
528
528
|
* @param disableOnFail - if true, disable the account when refresh fails (default: true).
|
|
529
529
|
* Background/startup refresh should pass false to avoid disabling accounts prematurely.
|
|
530
|
+
* @param force - if true, bypass the skip-if-fresh check (use after 401 errors where
|
|
531
|
+
* the token is demonstrably invalid despite having a future expiresAt).
|
|
530
532
|
*/
|
|
531
|
-
async refreshAccessToken(accountId: string, disableOnFail = true): Promise<void> {
|
|
533
|
+
async refreshAccessToken(accountId: string, disableOnFail = true, force = false): Promise<void> {
|
|
532
534
|
// Dedup: if a refresh is already in progress for this account, wait for it instead of racing
|
|
533
535
|
const pending = this.pendingRefreshes.get(accountId);
|
|
534
536
|
if (pending) {
|
|
@@ -536,7 +538,7 @@ class AccountService {
|
|
|
536
538
|
return pending;
|
|
537
539
|
}
|
|
538
540
|
|
|
539
|
-
const promise = this._doRefreshAccessToken(accountId, disableOnFail);
|
|
541
|
+
const promise = this._doRefreshAccessToken(accountId, disableOnFail, force);
|
|
540
542
|
this.pendingRefreshes.set(accountId, promise);
|
|
541
543
|
try {
|
|
542
544
|
await promise;
|
|
@@ -545,16 +547,18 @@ class AccountService {
|
|
|
545
547
|
}
|
|
546
548
|
}
|
|
547
549
|
|
|
548
|
-
private async _doRefreshAccessToken(accountId: string, disableOnFail: boolean): Promise<void> {
|
|
550
|
+
private async _doRefreshAccessToken(accountId: string, disableOnFail: boolean, force = false): Promise<void> {
|
|
549
551
|
const account = this.getWithTokens(accountId);
|
|
550
552
|
if (!account) throw new Error(`Account ${accountId} not found`);
|
|
551
553
|
// Skip refresh for temporary accounts (no refresh token)
|
|
552
554
|
if (!account.refreshToken || account.refreshToken === "") {
|
|
553
555
|
throw new Error(`Account ${accountId} has no refresh token (temporary account)`);
|
|
554
556
|
}
|
|
555
|
-
// Skip if token was already refreshed by another session (still fresh)
|
|
557
|
+
// Skip if token was already refreshed by another session (still fresh).
|
|
558
|
+
// But when force=true (after a 401), always refresh — the token may be
|
|
559
|
+
// revoked server-side despite having a future expiresAt.
|
|
556
560
|
const nowS = Math.floor(Date.now() / 1000);
|
|
557
|
-
if (account.expiresAt && account.expiresAt - nowS > 60) {
|
|
561
|
+
if (!force && account.expiresAt && account.expiresAt - nowS > 60) {
|
|
558
562
|
console.log(`[accounts] Token for ${account.email ?? accountId} is already fresh (expires in ${account.expiresAt - nowS}s) — skipping OAuth refresh`);
|
|
559
563
|
return;
|
|
560
564
|
}
|