@hienlh/ppm 0.8.26 → 0.8.28

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,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## [0.8.26] - 2026-03-24
3
+ ## [0.8.27] - 2026-03-24
4
4
 
5
5
  ### Changed
6
6
  - **Temporary export/import**: Exported accounts no longer include refresh tokens — imported accounts are temporary (~1h access-only). Prevents token rotation conflicts between machines.
@@ -8,6 +8,7 @@
8
8
  - **Auto-cleanup**: Expired temporary accounts (no refresh token) are automatically deleted after 7 days.
9
9
  - **Export warning**: Export dialog now explains that exported accounts are temporary and the importing machine should login directly for permanent access.
10
10
  - **Invalid refresh token cleanup**: When refresh fails with `invalid_grant`, clears the refresh token so the account becomes temporary (same lifecycle rules apply: can't re-enable when expired, auto-deleted after 7 days)
11
+ - **Skip expired accounts in usage**: Expired temporary accounts are excluded from usage polling and usage panel — no wasted API calls or UI clutter
11
12
 
12
13
  ## [0.8.24] - 2026-03-24
13
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.26",
3
+ "version": "0.8.28",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -518,6 +518,10 @@ class AccountService {
518
518
  async refreshAccessToken(accountId: string, disableOnFail = true): Promise<void> {
519
519
  const account = this.getWithTokens(accountId);
520
520
  if (!account) throw new Error(`Account ${accountId} not found`);
521
+ // Skip refresh for temporary accounts (no refresh token)
522
+ if (!account.refreshToken || account.refreshToken === "") {
523
+ throw new Error(`Account ${accountId} has no refresh token (temporary account)`);
524
+ }
521
525
  const res = await fetch(OAUTH_TOKEN_URL, {
522
526
  method: "POST",
523
527
  headers: { "Content-Type": "application/json" },
@@ -530,8 +534,8 @@ class AccountService {
530
534
  if (!res.ok) {
531
535
  const errorBody = await res.text().catch(() => "");
532
536
  console.error(`[accounts] Refresh failed for ${accountId}: ${res.status} ${errorBody}`);
533
- // invalid_grant = refresh token permanently dead → clear it so account becomes temporary
534
- if (errorBody.includes("invalid_grant")) {
537
+ // invalid_grant or invalid_request = refresh token permanently dead → clear it so account becomes temporary
538
+ if (errorBody.includes("invalid_grant") || errorBody.includes("invalid_request")) {
535
539
  console.log(`[accounts] Clearing invalid refresh token for ${account.email ?? accountId} — account is now temporary`);
536
540
  updateAccount(accountId, { refresh_token: encrypt("") });
537
541
  }
@@ -169,8 +169,11 @@ function persistIfChanged(data: ClaudeUsage, accountId: string | null): void {
169
169
 
170
170
  async function fetchAllAccountUsages(): Promise<void> {
171
171
  const accounts = accountService.list();
172
+ const nowS = Math.floor(Date.now() / 1000);
172
173
  for (const acc of accounts) {
173
174
  if (acc.status === "disabled") continue;
175
+ // Skip expired temporary accounts (no refresh token)
176
+ if (!accountService.hasRefreshToken(acc.id) && acc.expiresAt && acc.expiresAt < nowS) continue;
174
177
  const withTokens = accountService.getWithTokens(acc.id);
175
178
  if (!withTokens) continue;
176
179
  const token = withTokens.accessToken;
@@ -247,9 +250,14 @@ export function getUsageForAccount(accountId: string): ClaudeUsage {
247
250
  return row ? snapshotToUsage(row) : {};
248
251
  }
249
252
 
250
- /** Get usage for all accounts */
253
+ /** Get usage for all accounts (excludes expired temporary accounts) */
251
254
  export function getAllAccountUsages(): AccountUsageEntry[] {
252
- const accounts = accountService.list();
255
+ const nowS = Math.floor(Date.now() / 1000);
256
+ const accounts = accountService.list().filter(acc => {
257
+ // Exclude expired accounts without refresh token (temporary/invalid)
258
+ if (!accountService.hasRefreshToken(acc.id) && acc.expiresAt && acc.expiresAt < nowS) return false;
259
+ return true;
260
+ });
253
261
  const snapshots = getAllLatestSnapshots();
254
262
  const snapshotMap = new Map(snapshots.map(s => [s.account_id, s]));
255
263
  return accounts.map(acc => {