instar 1.3.431 → 1.3.432
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/dashboard/subscriptions.js +21 -0
- package/dist/core/OAuthRefresher.d.ts +112 -0
- package/dist/core/OAuthRefresher.d.ts.map +1 -0
- package/dist/core/OAuthRefresher.js +222 -0
- package/dist/core/OAuthRefresher.js.map +1 -0
- package/dist/core/QuotaPoller.d.ts +33 -6
- package/dist/core/QuotaPoller.d.ts.map +1 -1
- package/dist/core/QuotaPoller.js +73 -66
- package/dist/core/QuotaPoller.js.map +1 -1
- package/dist/core/SubscriptionPool.d.ts +8 -0
- package/dist/core/SubscriptionPool.d.ts.map +1 -1
- package/dist/core/SubscriptionPool.js +3 -0
- package/dist/core/SubscriptionPool.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.432.md +27 -0
- package/upgrades/side-effects/subscription-token-autorefresh.md +29 -0
|
@@ -90,6 +90,20 @@ export function countdown(iso, now = Date.now(), { expiredWord = 'expired' } = {
|
|
|
90
90
|
return `${sec}s`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** A coarse "N ago" for a PAST ISO timestamp (token-refresh recency). '' if invalid. */
|
|
94
|
+
export function relativeAge(iso, now = Date.now()) {
|
|
95
|
+
const t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
96
|
+
if (Number.isNaN(t)) return '';
|
|
97
|
+
const sec = Math.floor((now - t) / 1000);
|
|
98
|
+
if (sec < 0) return 'just now';
|
|
99
|
+
if (sec < 60) return 'just now';
|
|
100
|
+
const min = Math.floor(sec / 60);
|
|
101
|
+
if (min < 60) return `${min}m ago`;
|
|
102
|
+
const hr = Math.floor(min / 60);
|
|
103
|
+
if (hr < 24) return `${hr}h ago`;
|
|
104
|
+
return `${Math.floor(hr / 24)}d ago`;
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
// ── DOM helpers (textContent ONLY — never innerHTML) ────────────────────────
|
|
94
108
|
function el(doc, tag, cls, text) {
|
|
95
109
|
const node = doc.createElement(tag);
|
|
@@ -142,6 +156,13 @@ export function renderAccounts(doc, target, accounts, now = Date.now()) {
|
|
|
142
156
|
} else {
|
|
143
157
|
card.appendChild(el(doc, 'div', 'sub-account-noquota', 'No quota reading yet.'));
|
|
144
158
|
}
|
|
159
|
+
// Token health: when the poller silently refreshed the access token from the
|
|
160
|
+
// refresh token, show it — so a routine access-token expiry reads as healthy
|
|
161
|
+
// (auto-handled) rather than looking like a re-auth event.
|
|
162
|
+
const refAge = a && a.lastRefreshAt ? relativeAge(a.lastRefreshAt, now) : null;
|
|
163
|
+
if (refAge) {
|
|
164
|
+
card.appendChild(el(doc, 'div', 'sub-account-refresh', `Token auto-refreshed ${refAge}`));
|
|
165
|
+
}
|
|
145
166
|
target.appendChild(card);
|
|
146
167
|
}
|
|
147
168
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuthRefresher — mint a fresh Claude Code OAuth access token from its stored
|
|
3
|
+
* refresh token, so the QuotaPoller never falsely flags a still-valid login as
|
|
4
|
+
* `needs-reauth` (P1.2 hardening of the Subscription & Auth Standard).
|
|
5
|
+
*
|
|
6
|
+
* ── Why this exists (the bug it fixes) ──
|
|
7
|
+
* A Claude Code login holds TWO tokens in its config home's credential store:
|
|
8
|
+
* - a short-lived ACCESS token (`sk-ant-oat…`, ~8–12h), and
|
|
9
|
+
* - a long-lived REFRESH token (`sk-ant-ort…`, weeks→months).
|
|
10
|
+
* The `claude` client silently exchanges the refresh token for a new access
|
|
11
|
+
* token on every real use. The QuotaPoller reads the access token out-of-band
|
|
12
|
+
* and calls the usage endpoint directly — so when the access token has expired
|
|
13
|
+
* (which is routine, daily) but the refresh token is still perfectly valid, the
|
|
14
|
+
* usage read returns 401 and the poller wrongly marks the account `needs-reauth`.
|
|
15
|
+
* That cried wolf: the login is intact, only the access token lapsed. This module
|
|
16
|
+
* performs the same refresh-token exchange the client does, so a routine expiry
|
|
17
|
+
* recovers silently and `needs-reauth` is reserved for a genuinely dead login
|
|
18
|
+
* (refresh token revoked / password change) — exactly what SubscriptionPool's
|
|
19
|
+
* status comment already promises.
|
|
20
|
+
*
|
|
21
|
+
* ── Corruption safety (the load-bearing invariant) ──
|
|
22
|
+
* The ONLY way this module could harm a working login is by writing a bad
|
|
23
|
+
* credential back. So the write is gated three ways:
|
|
24
|
+
* 1. it happens ONLY on a fully-validated 200 response (new access token shaped
|
|
25
|
+
* `sk-ant-oat…`, a positive numeric `expires_in`);
|
|
26
|
+
* 2. it is a READ-MERGE-WRITE — the existing credential JSON is re-read and only
|
|
27
|
+
* the access token / refresh token / expiry are overwritten, so scopes,
|
|
28
|
+
* subscriptionType, rateLimitTier and any unknown fields are preserved; and
|
|
29
|
+
* 3. if the server rotates the refresh token, the NEW one is persisted; if the
|
|
30
|
+
* response omits a refresh token (non-rotating server), the existing one is
|
|
31
|
+
* kept — never dropped.
|
|
32
|
+
* A wrong endpoint / client id / network failure can therefore only ever make the
|
|
33
|
+
* exchange FAIL (→ the caller's existing `needs-reauth` path), never corrupt.
|
|
34
|
+
*
|
|
35
|
+
* Token values are NEVER logged or returned to any persisted surface. The OAuth
|
|
36
|
+
* token endpoint + client id are the public Claude Code values, extracted from
|
|
37
|
+
* the official client binary (verified 2026-06-08).
|
|
38
|
+
*
|
|
39
|
+
* Testability: the credential store, the fetch surface, and the clock are all
|
|
40
|
+
* injectable, so the whole refresh runs hermetically with zero keychain, zero
|
|
41
|
+
* network, and a deterministic clock in tests.
|
|
42
|
+
*/
|
|
43
|
+
/** Public Claude Code OAuth token endpoint (from the official client binary). */
|
|
44
|
+
export declare const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
45
|
+
/** Public Claude Code OAuth client id (from the official client binary). */
|
|
46
|
+
export declare const CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
47
|
+
export type RefreshFailReason = 'unsupported-account' | 'read-failed' | 'no-refresh-token' | 'exchange-failed' | 'malformed-response' | 'write-failed';
|
|
48
|
+
export type RefreshResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
accessToken: string;
|
|
51
|
+
expiresAt: number;
|
|
52
|
+
rotated: boolean;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: RefreshFailReason;
|
|
56
|
+
status?: number;
|
|
57
|
+
};
|
|
58
|
+
/** The OAuth access + refresh tokens parsed out of a credential store entry. */
|
|
59
|
+
export interface ClaudeOauth {
|
|
60
|
+
accessToken?: string;
|
|
61
|
+
refreshToken?: string;
|
|
62
|
+
expiresAt?: number;
|
|
63
|
+
[k: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A credential store for one config home. `read` returns the RAW JSON string of
|
|
67
|
+
* the stored entry (so the refresher can merge-preserve every field); `write`
|
|
68
|
+
* persists a replacement raw JSON string. Injectable so tests use a fake.
|
|
69
|
+
*/
|
|
70
|
+
export interface CredentialStore {
|
|
71
|
+
read(configHome: string): string | null;
|
|
72
|
+
write(configHome: string, rawJson: string): boolean;
|
|
73
|
+
}
|
|
74
|
+
/** POST-capable fetch surface (distinct from QuotaPoller's GET-only FetchImpl). */
|
|
75
|
+
export type RefreshFetch = (url: string, init: {
|
|
76
|
+
method: string;
|
|
77
|
+
headers: Record<string, string>;
|
|
78
|
+
body: string;
|
|
79
|
+
}) => Promise<{
|
|
80
|
+
ok: boolean;
|
|
81
|
+
status: number;
|
|
82
|
+
json: () => Promise<unknown>;
|
|
83
|
+
}>;
|
|
84
|
+
export interface RefreshDeps {
|
|
85
|
+
store?: CredentialStore;
|
|
86
|
+
fetchImpl?: RefreshFetch;
|
|
87
|
+
now?: () => number;
|
|
88
|
+
tokenUrl?: string;
|
|
89
|
+
clientId?: string;
|
|
90
|
+
}
|
|
91
|
+
export declare function expandHome(p: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* macOS keychain service name for a config home's Claude Code credentials.
|
|
94
|
+
* The default home (`~/.claude`) has no hash suffix; every other config home is
|
|
95
|
+
* suffixed with the first 8 hex of sha256(configHome) — verified empirically and
|
|
96
|
+
* matched by the official client.
|
|
97
|
+
*/
|
|
98
|
+
export declare function claudeCredentialService(configHome: string): string;
|
|
99
|
+
/** Non-darwin credential file for a config home. */
|
|
100
|
+
export declare function claudeCredentialFilePath(configHome: string): string;
|
|
101
|
+
/** Default store: macOS keychain, else a per-config-home credentials file. */
|
|
102
|
+
export declare const defaultCredentialStore: CredentialStore;
|
|
103
|
+
/** Parse the `claudeAiOauth` block out of a config home's credential store. */
|
|
104
|
+
export declare function readClaudeOauth(configHome: string, store?: CredentialStore): ClaudeOauth | null;
|
|
105
|
+
/**
|
|
106
|
+
* Refresh a config home's Claude Code access token from its stored refresh token.
|
|
107
|
+
* Returns the new access token + expiry on success; otherwise a typed failure
|
|
108
|
+
* reason the caller maps to its `needs-reauth` decision. Writes NOTHING on any
|
|
109
|
+
* failure — a working login is never put at risk by an unsuccessful refresh.
|
|
110
|
+
*/
|
|
111
|
+
export declare function refreshClaudeToken(configHome: string, deps?: RefreshDeps): Promise<RefreshResult>;
|
|
112
|
+
//# sourceMappingURL=OAuthRefresher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthRefresher.d.ts","sourceRoot":"","sources":["../../src/core/OAuthRefresher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAQH,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,+CAA+C,CAAC;AAC7E,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAK5E,MAAM,MAAM,iBAAiB,GACzB,qBAAqB,GACrB,aAAa,GACb,kBAAkB,GAClB,iBAAiB,GACjB,oBAAoB,GACpB,cAAc,CAAC;AAEnB,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACtE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACxC,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACrD;AAED,mFAAmF;AACnF,MAAM,MAAM,YAAY,GAAG,CACzB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK5C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAMlE;AAED,oDAAoD;AACpD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,EAAE,eAoDpC,CAAC;AAEF,+EAA+E;AAC/E,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,eAAwC,GAC9C,WAAW,GAAG,IAAI,CAUpB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,aAAa,CAAC,CA+ExB"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuthRefresher — mint a fresh Claude Code OAuth access token from its stored
|
|
3
|
+
* refresh token, so the QuotaPoller never falsely flags a still-valid login as
|
|
4
|
+
* `needs-reauth` (P1.2 hardening of the Subscription & Auth Standard).
|
|
5
|
+
*
|
|
6
|
+
* ── Why this exists (the bug it fixes) ──
|
|
7
|
+
* A Claude Code login holds TWO tokens in its config home's credential store:
|
|
8
|
+
* - a short-lived ACCESS token (`sk-ant-oat…`, ~8–12h), and
|
|
9
|
+
* - a long-lived REFRESH token (`sk-ant-ort…`, weeks→months).
|
|
10
|
+
* The `claude` client silently exchanges the refresh token for a new access
|
|
11
|
+
* token on every real use. The QuotaPoller reads the access token out-of-band
|
|
12
|
+
* and calls the usage endpoint directly — so when the access token has expired
|
|
13
|
+
* (which is routine, daily) but the refresh token is still perfectly valid, the
|
|
14
|
+
* usage read returns 401 and the poller wrongly marks the account `needs-reauth`.
|
|
15
|
+
* That cried wolf: the login is intact, only the access token lapsed. This module
|
|
16
|
+
* performs the same refresh-token exchange the client does, so a routine expiry
|
|
17
|
+
* recovers silently and `needs-reauth` is reserved for a genuinely dead login
|
|
18
|
+
* (refresh token revoked / password change) — exactly what SubscriptionPool's
|
|
19
|
+
* status comment already promises.
|
|
20
|
+
*
|
|
21
|
+
* ── Corruption safety (the load-bearing invariant) ──
|
|
22
|
+
* The ONLY way this module could harm a working login is by writing a bad
|
|
23
|
+
* credential back. So the write is gated three ways:
|
|
24
|
+
* 1. it happens ONLY on a fully-validated 200 response (new access token shaped
|
|
25
|
+
* `sk-ant-oat…`, a positive numeric `expires_in`);
|
|
26
|
+
* 2. it is a READ-MERGE-WRITE — the existing credential JSON is re-read and only
|
|
27
|
+
* the access token / refresh token / expiry are overwritten, so scopes,
|
|
28
|
+
* subscriptionType, rateLimitTier and any unknown fields are preserved; and
|
|
29
|
+
* 3. if the server rotates the refresh token, the NEW one is persisted; if the
|
|
30
|
+
* response omits a refresh token (non-rotating server), the existing one is
|
|
31
|
+
* kept — never dropped.
|
|
32
|
+
* A wrong endpoint / client id / network failure can therefore only ever make the
|
|
33
|
+
* exchange FAIL (→ the caller's existing `needs-reauth` path), never corrupt.
|
|
34
|
+
*
|
|
35
|
+
* Token values are NEVER logged or returned to any persisted surface. The OAuth
|
|
36
|
+
* token endpoint + client id are the public Claude Code values, extracted from
|
|
37
|
+
* the official client binary (verified 2026-06-08).
|
|
38
|
+
*
|
|
39
|
+
* Testability: the credential store, the fetch surface, and the clock are all
|
|
40
|
+
* injectable, so the whole refresh runs hermetically with zero keychain, zero
|
|
41
|
+
* network, and a deterministic clock in tests.
|
|
42
|
+
*/
|
|
43
|
+
import { execFileSync } from 'node:child_process';
|
|
44
|
+
import crypto from 'node:crypto';
|
|
45
|
+
import fs from 'node:fs';
|
|
46
|
+
import os from 'node:os';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
/** Public Claude Code OAuth token endpoint (from the official client binary). */
|
|
49
|
+
export const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
50
|
+
/** Public Claude Code OAuth client id (from the official client binary). */
|
|
51
|
+
export const CLAUDE_CODE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
52
|
+
const ACCESS_PREFIX = 'sk-ant-oat';
|
|
53
|
+
const REFRESH_PREFIX = 'sk-ant-ort';
|
|
54
|
+
export function expandHome(p) {
|
|
55
|
+
if (p === '~' || p.startsWith('~/')) {
|
|
56
|
+
return path.join(process.env.HOME ?? '', p.slice(1));
|
|
57
|
+
}
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* macOS keychain service name for a config home's Claude Code credentials.
|
|
62
|
+
* The default home (`~/.claude`) has no hash suffix; every other config home is
|
|
63
|
+
* suffixed with the first 8 hex of sha256(configHome) — verified empirically and
|
|
64
|
+
* matched by the official client.
|
|
65
|
+
*/
|
|
66
|
+
export function claudeCredentialService(configHome) {
|
|
67
|
+
const home = expandHome(configHome);
|
|
68
|
+
const defaultHome = expandHome('~/.claude');
|
|
69
|
+
return home === defaultHome
|
|
70
|
+
? 'Claude Code-credentials'
|
|
71
|
+
: `Claude Code-credentials-${crypto.createHash('sha256').update(home).digest('hex').slice(0, 8)}`;
|
|
72
|
+
}
|
|
73
|
+
/** Non-darwin credential file for a config home. */
|
|
74
|
+
export function claudeCredentialFilePath(configHome) {
|
|
75
|
+
return path.join(expandHome(configHome), '.credentials.json');
|
|
76
|
+
}
|
|
77
|
+
/** Default store: macOS keychain, else a per-config-home credentials file. */
|
|
78
|
+
export const defaultCredentialStore = {
|
|
79
|
+
read(configHome) {
|
|
80
|
+
if (process.platform === 'darwin') {
|
|
81
|
+
try {
|
|
82
|
+
const raw = execFileSync('security', ['find-generic-password', '-s', claudeCredentialService(configHome), '-w'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
83
|
+
return raw || null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null; // @silent-fallback-ok: no keychain entry → unreadable
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const p = claudeCredentialFilePath(configHome);
|
|
91
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8') : null;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null; // @silent-fallback-ok: missing/unreadable creds file
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
write(configHome, rawJson) {
|
|
98
|
+
if (process.platform === 'darwin') {
|
|
99
|
+
try {
|
|
100
|
+
execFileSync('security', [
|
|
101
|
+
'add-generic-password',
|
|
102
|
+
'-U', // update the existing entry in place
|
|
103
|
+
'-a',
|
|
104
|
+
os.userInfo().username,
|
|
105
|
+
'-s',
|
|
106
|
+
claudeCredentialService(configHome),
|
|
107
|
+
'-w',
|
|
108
|
+
rawJson,
|
|
109
|
+
], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false; // @silent-fallback-ok: keychain write failed → caller falls to needs-reauth
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const p = claudeCredentialFilePath(configHome);
|
|
118
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
119
|
+
fs.writeFileSync(p, rawJson, { mode: 0o600 });
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false; // @silent-fallback-ok: file write failed
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
/** Parse the `claudeAiOauth` block out of a config home's credential store. */
|
|
128
|
+
export function readClaudeOauth(configHome, store = defaultCredentialStore) {
|
|
129
|
+
const raw = store.read(configHome);
|
|
130
|
+
if (!raw)
|
|
131
|
+
return null;
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
const oauth = parsed?.claudeAiOauth;
|
|
135
|
+
return oauth && typeof oauth === 'object' ? oauth : null;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null; // @silent-fallback-ok: unparseable entry
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Refresh a config home's Claude Code access token from its stored refresh token.
|
|
143
|
+
* Returns the new access token + expiry on success; otherwise a typed failure
|
|
144
|
+
* reason the caller maps to its `needs-reauth` decision. Writes NOTHING on any
|
|
145
|
+
* failure — a working login is never put at risk by an unsuccessful refresh.
|
|
146
|
+
*/
|
|
147
|
+
export async function refreshClaudeToken(configHome, deps = {}) {
|
|
148
|
+
const store = deps.store ?? defaultCredentialStore;
|
|
149
|
+
const fetchImpl = deps.fetchImpl ??
|
|
150
|
+
((url, init) => fetch(url, init));
|
|
151
|
+
const now = deps.now ?? (() => Date.now());
|
|
152
|
+
const tokenUrl = deps.tokenUrl ?? CLAUDE_TOKEN_URL;
|
|
153
|
+
const clientId = deps.clientId ?? CLAUDE_CODE_CLIENT_ID;
|
|
154
|
+
const raw = store.read(configHome);
|
|
155
|
+
if (!raw)
|
|
156
|
+
return { ok: false, reason: 'read-failed' };
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(raw);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return { ok: false, reason: 'read-failed' };
|
|
163
|
+
}
|
|
164
|
+
const oauth = (parsed?.claudeAiOauth ?? null);
|
|
165
|
+
const refreshToken = oauth?.refreshToken;
|
|
166
|
+
if (typeof refreshToken !== 'string' || !refreshToken) {
|
|
167
|
+
return { ok: false, reason: 'no-refresh-token' };
|
|
168
|
+
}
|
|
169
|
+
let res;
|
|
170
|
+
try {
|
|
171
|
+
res = await fetchImpl(tokenUrl, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
grant_type: 'refresh_token',
|
|
176
|
+
refresh_token: refreshToken,
|
|
177
|
+
client_id: clientId,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return { ok: false, reason: 'exchange-failed' };
|
|
183
|
+
}
|
|
184
|
+
if (!res.ok)
|
|
185
|
+
return { ok: false, reason: 'exchange-failed', status: res.status };
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
data = (await res.json());
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return { ok: false, reason: 'malformed-response' };
|
|
192
|
+
}
|
|
193
|
+
const newAccess = data?.access_token;
|
|
194
|
+
const expiresIn = data?.expires_in;
|
|
195
|
+
if (typeof newAccess !== 'string' || !newAccess.startsWith(ACCESS_PREFIX)) {
|
|
196
|
+
return { ok: false, reason: 'malformed-response' };
|
|
197
|
+
}
|
|
198
|
+
if (typeof expiresIn !== 'number' || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
199
|
+
return { ok: false, reason: 'malformed-response' };
|
|
200
|
+
}
|
|
201
|
+
const newRefresh = data?.refresh_token;
|
|
202
|
+
const rotated = typeof newRefresh === 'string' &&
|
|
203
|
+
newRefresh.startsWith(REFRESH_PREFIX) &&
|
|
204
|
+
newRefresh !== refreshToken;
|
|
205
|
+
const expiresAt = now() + expiresIn * 1000;
|
|
206
|
+
// READ-MERGE-WRITE: preserve every existing field, overwrite only the tokens
|
|
207
|
+
// + expiry. Keep the old refresh token if the server didn't rotate.
|
|
208
|
+
const updatedOauth = {
|
|
209
|
+
...oauth,
|
|
210
|
+
accessToken: newAccess,
|
|
211
|
+
refreshToken: typeof newRefresh === 'string' && newRefresh.startsWith(REFRESH_PREFIX)
|
|
212
|
+
? newRefresh
|
|
213
|
+
: refreshToken,
|
|
214
|
+
expiresAt,
|
|
215
|
+
};
|
|
216
|
+
const updatedRaw = { ...parsed, claudeAiOauth: updatedOauth };
|
|
217
|
+
if (!store.write(configHome, JSON.stringify(updatedRaw))) {
|
|
218
|
+
return { ok: false, reason: 'write-failed' };
|
|
219
|
+
}
|
|
220
|
+
return { ok: true, accessToken: newAccess, expiresAt, rotated };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=OAuthRefresher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthRefresher.js","sourceRoot":"","sources":["../../src/core/OAuthRefresher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,4CAA4C,CAAC;AAC7E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,sCAAsC,CAAC;AAE5E,MAAM,aAAa,GAAG,YAAY,CAAC;AACnC,MAAM,cAAc,GAAG,YAAY,CAAC;AA8CpC,MAAM,UAAU,UAAU,CAAC,CAAS;IAClC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAkB;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAC5C,OAAO,IAAI,KAAK,WAAW;QACzB,CAAC,CAAC,yBAAyB;QAC3B,CAAC,CAAC,2BAA2B,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AACtG,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,wBAAwB,CAAC,UAAkB;IACzD,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAC,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAoB;IACrD,IAAI,CAAC,UAAkB;QACrB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,YAAY,CACtB,UAAU,EACV,CAAC,uBAAuB,EAAE,IAAI,EAAE,uBAAuB,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,EAC1E,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAC3D,CAAC,IAAI,EAAE,CAAC;gBACT,OAAO,GAAG,IAAI,IAAI,CAAC;YACrB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC,CAAC,sDAAsD;YACrE,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAC;YAC/C,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,CAAC,qDAAqD;QACpE,CAAC;IACH,CAAC;IACD,KAAK,CAAC,UAAkB,EAAE,OAAe;QACvC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,YAAY,CACV,UAAU,EACV;oBACE,sBAAsB;oBACtB,IAAI,EAAE,qCAAqC;oBAC3C,IAAI;oBACJ,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ;oBACtB,IAAI;oBACJ,uBAAuB,CAAC,UAAU,CAAC;oBACnC,IAAI;oBACJ,OAAO;iBACR,EACD,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAC1C,CAAC;gBACF,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC,CAAC,4EAA4E;YAC5F,CAAC;QACH,CAAC;QACD,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAC;YAC/C,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC,CAAC,yCAAyC;QACzD,CAAC;IACH,CAAC;CACF,CAAC;AAEF,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAC7B,UAAkB,EAClB,QAAyB,sBAAsB;IAE/C,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QAC1D,MAAM,KAAK,GAAG,MAAM,EAAE,aAAa,CAAC;QACpC,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAqB,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,yCAAyC;IACxD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,UAAkB,EAClB,OAAoB,EAAE;IAEtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,sBAAsB,CAAC;IACnD,MAAM,SAAS,GACb,IAAI,CAAC,SAAS;QACd,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,IAAmB,CAAwC,CAAC,CAAC;IAC1F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,qBAAqB,CAAC;IAExD,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACtD,IAAI,MAA+B,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAC9C,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,aAAa,IAAI,IAAI,CAAuB,CAAC;IACpE,MAAM,YAAY,GAAG,KAAK,EAAE,YAAY,CAAC;IACzC,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,CAAC,YAAY,EAAE,CAAC;QACtD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACnD,CAAC;IAED,IAAI,GAAkE,CAAC;IACvE,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE;YAC9B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,UAAU,EAAE,eAAe;gBAC3B,aAAa,EAAE,YAAY;gBAC3B,SAAS,EAAE,QAAQ;aACpB,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAEjF,IAAI,IAA6B,CAAC;IAClC,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,EAAE,YAAY,CAAC;IACrC,MAAM,SAAS,GAAG,IAAI,EAAE,UAAU,CAAC;IACnC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC1E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,EAAE,aAAa,CAAC;IACvC,MAAM,OAAO,GACX,OAAO,UAAU,KAAK,QAAQ;QAC9B,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC;QACrC,UAAU,KAAK,YAAY,CAAC;IAE9B,MAAM,SAAS,GAAG,GAAG,EAAE,GAAG,SAAS,GAAG,IAAI,CAAC;IAC3C,6EAA6E;IAC7E,oEAAoE;IACpE,MAAM,YAAY,GAAgB;QAChC,GAAG,KAAK;QACR,WAAW,EAAE,SAAS;QACtB,YAAY,EACV,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,UAAU,CAAC,cAAc,CAAC;YACrE,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,YAAY;QAClB,SAAS;KACV,CAAC;IACF,MAAM,UAAU,GAAG,EAAE,GAAG,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;IAE9D,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAClE,CAAC"}
|
|
@@ -32,8 +32,15 @@
|
|
|
32
32
|
* poller runs hermetically with zero credentials and zero network in tests.
|
|
33
33
|
*/
|
|
34
34
|
import type { SubscriptionPool, SubscriptionAccount, AccountQuotaSnapshot } from './SubscriptionPool.js';
|
|
35
|
+
import { type RefreshResult } from './OAuthRefresher.js';
|
|
35
36
|
/** Injectable token resolver — returns an account's OAuth access token or null. */
|
|
36
37
|
export type TokenResolver = (account: SubscriptionAccount) => string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Injectable account refresher — exchanges a config home's stored refresh token
|
|
40
|
+
* for a fresh access token (see OAuthRefresher). Defaults to the real keychain/
|
|
41
|
+
* file-backed refresh; tests inject a stub so the poller runs hermetically.
|
|
42
|
+
*/
|
|
43
|
+
export type AccountRefresher = (account: SubscriptionAccount) => Promise<RefreshResult>;
|
|
37
44
|
/** Minimal fetch surface so tests inject a stub (no global fetch dependency). */
|
|
38
45
|
export type FetchImpl = (url: string, init: {
|
|
39
46
|
headers: Record<string, string>;
|
|
@@ -50,6 +57,12 @@ export interface QuotaPollerConfig {
|
|
|
50
57
|
fetchImpl?: FetchImpl;
|
|
51
58
|
/** Injected for tests; defaults to the config-home credential resolver. */
|
|
52
59
|
tokenResolver?: TokenResolver;
|
|
60
|
+
/**
|
|
61
|
+
* Injected for tests; defaults to the real OAuth refresh-token exchange. On a
|
|
62
|
+
* usage-read auth failure the poller calls this BEFORE declaring needs-reauth,
|
|
63
|
+
* so a routine access-token expiry recovers silently instead of crying wolf.
|
|
64
|
+
*/
|
|
65
|
+
refresher?: AccountRefresher;
|
|
53
66
|
/** Logger (defaults to console). */
|
|
54
67
|
logger?: {
|
|
55
68
|
log: (m: string) => void;
|
|
@@ -66,10 +79,12 @@ export interface BurnRate {
|
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Resolve a claude-code account's OAuth access token from its config home,
|
|
69
|
-
* TRANSIENTLY. Never persisted, never logged.
|
|
70
|
-
* keychain
|
|
71
|
-
*
|
|
72
|
-
*
|
|
82
|
+
* TRANSIENTLY. Never persisted, never logged. Reads via the shared OAuthRefresher
|
|
83
|
+
* locator (macOS keychain `Claude Code-credentials-<sha256(configHome)[0:8]>`,
|
|
84
|
+
* else `<configHome>/.credentials.json`) so the resolver and the refresher always
|
|
85
|
+
* agree on WHERE a config home's credentials live. NOTE: an EXPIRED access token
|
|
86
|
+
* is still returned here (it's a valid string) — expiry is detected by the usage
|
|
87
|
+
* read's 401 and recovered by the refresher, not by this resolver.
|
|
73
88
|
*/
|
|
74
89
|
export declare function defaultTokenResolver(account: SubscriptionAccount): string | null;
|
|
75
90
|
/**
|
|
@@ -92,6 +107,7 @@ export declare class QuotaPoller {
|
|
|
92
107
|
private readonly pollIntervalMs;
|
|
93
108
|
private readonly fetchImpl;
|
|
94
109
|
private readonly tokenResolver;
|
|
110
|
+
private readonly refresher;
|
|
95
111
|
private readonly logger;
|
|
96
112
|
private interval;
|
|
97
113
|
/** Most-recent snapshot per account id. */
|
|
@@ -101,10 +117,21 @@ export declare class QuotaPoller {
|
|
|
101
117
|
constructor(config: QuotaPollerConfig);
|
|
102
118
|
start(): void;
|
|
103
119
|
stop(): void;
|
|
120
|
+
/**
|
|
121
|
+
* One usage read. Returns the parsed body (or null body on a non-auth non-ok),
|
|
122
|
+
* an auth-failed marker (401/403), or null on a network failure. NEVER logs the
|
|
123
|
+
* token.
|
|
124
|
+
*/
|
|
125
|
+
private readUsage;
|
|
126
|
+
private markNeedsReauth;
|
|
104
127
|
/**
|
|
105
128
|
* Poll one account: resolve token transiently, read usage, map to snapshot.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
129
|
+
* On a usage-read auth failure (401/403) the access token may simply have
|
|
130
|
+
* EXPIRED while the refresh token is still valid — so the poller attempts a
|
|
131
|
+
* refresh-token exchange and ONE retry BEFORE declaring needs-reauth. Only a
|
|
132
|
+
* genuinely dead login (no refresh token / refresh rejected / still 401 after
|
|
133
|
+
* a fresh token) yields needs-reauth. Returns null when the token is
|
|
134
|
+
* unresolvable or the read fails. NEVER logs or returns the token.
|
|
108
135
|
*/
|
|
109
136
|
pollAccount(account: SubscriptionAccount): Promise<AccountQuotaSnapshot | null>;
|
|
110
137
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaPoller.d.ts","sourceRoot":"","sources":["../../src/core/QuotaPoller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;
|
|
1
|
+
{"version":3,"file":"QuotaPoller.d.ts","sourceRoot":"","sources":["../../src/core/QuotaPoller.ts"],"names":[],"mappings":"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"}
|
package/dist/core/QuotaPoller.js
CHANGED
|
@@ -31,61 +31,26 @@
|
|
|
31
31
|
* Testability: `fetchImpl` and `tokenResolver` are injectable so the whole
|
|
32
32
|
* poller runs hermetically with zero credentials and zero network in tests.
|
|
33
33
|
*/
|
|
34
|
-
import { execFileSync } from 'node:child_process';
|
|
35
|
-
import crypto from 'node:crypto';
|
|
36
34
|
import fs from 'node:fs';
|
|
37
35
|
import path from 'node:path';
|
|
36
|
+
import { readClaudeOauth, refreshClaudeToken, expandHome, } from './OAuthRefresher.js';
|
|
38
37
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
39
38
|
/**
|
|
40
39
|
* Resolve a claude-code account's OAuth access token from its config home,
|
|
41
|
-
* TRANSIENTLY. Never persisted, never logged.
|
|
42
|
-
* keychain
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* TRANSIENTLY. Never persisted, never logged. Reads via the shared OAuthRefresher
|
|
41
|
+
* locator (macOS keychain `Claude Code-credentials-<sha256(configHome)[0:8]>`,
|
|
42
|
+
* else `<configHome>/.credentials.json`) so the resolver and the refresher always
|
|
43
|
+
* agree on WHERE a config home's credentials live. NOTE: an EXPIRED access token
|
|
44
|
+
* is still returned here (it's a valid string) — expiry is detected by the usage
|
|
45
|
+
* read's 401 and recovered by the refresher, not by this resolver.
|
|
45
46
|
*/
|
|
46
47
|
export function defaultTokenResolver(account) {
|
|
47
48
|
if (account.provider !== 'anthropic' || account.framework !== 'claude-code') {
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const credPath = path.join(configHome, '.credentials.json');
|
|
55
|
-
if (fs.existsSync(credPath)) {
|
|
56
|
-
const data = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
57
|
-
const tok = data?.claudeAiOauth?.accessToken;
|
|
58
|
-
return typeof tok === 'string' && tok.startsWith('sk-ant-oat') ? tok : null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
// @silent-fallback-ok: missing/unreadable creds → unresolvable token (null)
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
// macOS: keychain service name, hash-suffixed by config-home path.
|
|
67
|
-
const defaultHome = expandHome('~/.claude');
|
|
68
|
-
const service = configHome === defaultHome
|
|
69
|
-
? 'Claude Code-credentials'
|
|
70
|
-
: `Claude Code-credentials-${crypto.createHash('sha256').update(configHome).digest('hex').slice(0, 8)}`;
|
|
71
|
-
try {
|
|
72
|
-
const raw = execFileSync('security', ['find-generic-password', '-s', service, '-w'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
73
|
-
if (!raw)
|
|
74
|
-
return null;
|
|
75
|
-
const data = JSON.parse(raw);
|
|
76
|
-
const tok = data?.claudeAiOauth?.accessToken;
|
|
77
|
-
return typeof tok === 'string' && tok.startsWith('sk-ant-oat') ? tok : null;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// @silent-fallback-ok: no keychain entry for this config home → null
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function expandHome(p) {
|
|
85
|
-
if (p === '~' || p.startsWith('~/')) {
|
|
86
|
-
return path.join(process.env.HOME ?? '', p.slice(1));
|
|
87
|
-
}
|
|
88
|
-
return p;
|
|
51
|
+
const oauth = readClaudeOauth(account.configHome);
|
|
52
|
+
const tok = oauth?.accessToken;
|
|
53
|
+
return typeof tok === 'string' && tok.startsWith('sk-ant-oat') ? tok : null;
|
|
89
54
|
}
|
|
90
55
|
/**
|
|
91
56
|
* Read the account email (`oauthAccount.emailAddress`) Claude Code records for a
|
|
@@ -169,6 +134,7 @@ export class QuotaPoller {
|
|
|
169
134
|
pollIntervalMs;
|
|
170
135
|
fetchImpl;
|
|
171
136
|
tokenResolver;
|
|
137
|
+
refresher;
|
|
172
138
|
logger;
|
|
173
139
|
interval = null;
|
|
174
140
|
/** Most-recent snapshot per account id. */
|
|
@@ -182,6 +148,8 @@ export class QuotaPoller {
|
|
|
182
148
|
config.fetchImpl ??
|
|
183
149
|
((url, init) => fetch(url, init));
|
|
184
150
|
this.tokenResolver = config.tokenResolver ?? defaultTokenResolver;
|
|
151
|
+
this.refresher =
|
|
152
|
+
config.refresher ?? ((account) => refreshClaudeToken(expandHome(account.configHome)));
|
|
185
153
|
this.logger = config.logger ?? { log: () => { }, warn: () => { } };
|
|
186
154
|
}
|
|
187
155
|
start() {
|
|
@@ -199,18 +167,11 @@ export class QuotaPoller {
|
|
|
199
167
|
}
|
|
200
168
|
}
|
|
201
169
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
170
|
+
* One usage read. Returns the parsed body (or null body on a non-auth non-ok),
|
|
171
|
+
* an auth-failed marker (401/403), or null on a network failure. NEVER logs the
|
|
172
|
+
* token.
|
|
205
173
|
*/
|
|
206
|
-
async
|
|
207
|
-
const token = this.tokenResolver(account);
|
|
208
|
-
if (!token) {
|
|
209
|
-
this.logger.warn(`[QuotaPoller] no resolvable token for account ${account.id} — skipping`);
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
let body = null;
|
|
213
|
-
let authFailed = false;
|
|
174
|
+
async readUsage(token) {
|
|
214
175
|
try {
|
|
215
176
|
const res = await this.fetchImpl(USAGE_URL, {
|
|
216
177
|
headers: {
|
|
@@ -220,26 +181,72 @@ export class QuotaPoller {
|
|
|
220
181
|
},
|
|
221
182
|
});
|
|
222
183
|
if (res.ok) {
|
|
223
|
-
body
|
|
224
|
-
}
|
|
225
|
-
else if (res.status === 401 || res.status === 403) {
|
|
226
|
-
authFailed = true;
|
|
184
|
+
return { authFailed: false, body: (await res.json()) };
|
|
227
185
|
}
|
|
186
|
+
if (res.status === 401 || res.status === 403)
|
|
187
|
+
return { authFailed: true };
|
|
188
|
+
return { authFailed: false, body: null }; // 5xx etc. → no snapshot, not auth
|
|
228
189
|
}
|
|
229
190
|
catch {
|
|
230
191
|
// @silent-fallback-ok: network failure → no snapshot this cycle (retry next)
|
|
231
192
|
return null;
|
|
232
193
|
}
|
|
233
|
-
|
|
234
|
-
|
|
194
|
+
}
|
|
195
|
+
markNeedsReauth(account, reason) {
|
|
196
|
+
try {
|
|
197
|
+
this.pool.update(account.id, { status: 'needs-reauth' });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// @silent-fallback-ok: pool update best-effort; status reflects next read
|
|
201
|
+
}
|
|
202
|
+
this.logger.warn(`[QuotaPoller] account ${account.id} → needs-reauth (${reason})`);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Poll one account: resolve token transiently, read usage, map to snapshot.
|
|
206
|
+
* On a usage-read auth failure (401/403) the access token may simply have
|
|
207
|
+
* EXPIRED while the refresh token is still valid — so the poller attempts a
|
|
208
|
+
* refresh-token exchange and ONE retry BEFORE declaring needs-reauth. Only a
|
|
209
|
+
* genuinely dead login (no refresh token / refresh rejected / still 401 after
|
|
210
|
+
* a fresh token) yields needs-reauth. Returns null when the token is
|
|
211
|
+
* unresolvable or the read fails. NEVER logs or returns the token.
|
|
212
|
+
*/
|
|
213
|
+
async pollAccount(account) {
|
|
214
|
+
const token = this.tokenResolver(account);
|
|
215
|
+
if (!token) {
|
|
216
|
+
this.logger.warn(`[QuotaPoller] no resolvable token for account ${account.id} — skipping`);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
const read = await this.readUsage(token);
|
|
220
|
+
if (read === null)
|
|
221
|
+
return null; // network failure
|
|
222
|
+
let body;
|
|
223
|
+
if (read.authFailed) {
|
|
224
|
+
const refreshed = await this.refresher(account);
|
|
225
|
+
if (!refreshed.ok) {
|
|
226
|
+
// No refresh token, or the exchange was rejected — genuine re-auth.
|
|
227
|
+
this.markNeedsReauth(account, refreshed.reason);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const retry = await this.readUsage(refreshed.accessToken);
|
|
231
|
+
if (retry === null)
|
|
232
|
+
return null; // network blip on the retry → next cycle
|
|
233
|
+
if (retry.authFailed) {
|
|
234
|
+
// Fresh token still rejected — treat as genuinely failed.
|
|
235
|
+
this.markNeedsReauth(account, 'usage still auth-failed after refresh');
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
// Recovered silently — no operator action needed. Record for visibility.
|
|
235
239
|
try {
|
|
236
|
-
this.pool.update(account.id, {
|
|
240
|
+
this.pool.update(account.id, { lastRefreshAt: new Date().toISOString() });
|
|
237
241
|
}
|
|
238
242
|
catch {
|
|
239
|
-
// @silent-fallback-ok:
|
|
243
|
+
// @silent-fallback-ok: visibility-only write
|
|
240
244
|
}
|
|
241
|
-
this.logger.
|
|
242
|
-
|
|
245
|
+
this.logger.log(`[QuotaPoller] account ${account.id} access token refreshed silently (no re-auth needed)`);
|
|
246
|
+
body = retry.body;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
body = read.body;
|
|
243
250
|
}
|
|
244
251
|
if (!body)
|
|
245
252
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuotaPoller.js","sourceRoot":"","sources":["../../src/core/QuotaPoller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"QuotaPoller.js","sourceRoot":"","sources":["../../src/core/QuotaPoller.ts"],"names":[],"mappings":"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"}
|
|
@@ -92,6 +92,13 @@ export interface SubscriptionAccount {
|
|
|
92
92
|
enrolledAt: string;
|
|
93
93
|
/** ISO timestamp the account was last selected for a session. */
|
|
94
94
|
lastUsedAt?: string;
|
|
95
|
+
/**
|
|
96
|
+
* ISO timestamp the poller last silently refreshed this account's access token
|
|
97
|
+
* from its refresh token (P1.2 hardening). Visibility only — lets the dashboard
|
|
98
|
+
* show "token auto-refreshed" so a routine access-token expiry reads as healthy
|
|
99
|
+
* rather than a re-auth event.
|
|
100
|
+
*/
|
|
101
|
+
lastRefreshAt?: string;
|
|
95
102
|
/** Monotonic version for optimistic CAS in update(). */
|
|
96
103
|
version: number;
|
|
97
104
|
}
|
|
@@ -117,6 +124,7 @@ export interface UpdateAccountInput {
|
|
|
117
124
|
status?: SubscriptionAccountStatus;
|
|
118
125
|
lastQuota?: AccountQuotaSnapshot | null;
|
|
119
126
|
lastUsedAt?: string;
|
|
127
|
+
lastRefreshAt?: string;
|
|
120
128
|
email?: string;
|
|
121
129
|
}
|
|
122
130
|
export declare class ValidationError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionPool.d.ts","sourceRoot":"","sources":["../../src/core/SubscriptionPool.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"SubscriptionPool.d.ts","sourceRoot":"","sources":["../../src/core/SubscriptionPool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlD,uEAAuE;AACvE,MAAM,MAAM,oBAAoB,GAC5B,WAAW,GACX,QAAQ,GACR,gBAAgB,GAChB,QAAQ,CAAC;AAEb,gDAAgD;AAChD,MAAM,MAAM,qBAAqB,GAC7B,aAAa,GACb,WAAW,GACX,YAAY,GACZ,QAAQ,CAAC;AAEb;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,cAAc,GACd,cAAc,GACd,UAAU,CAAC;AAEf;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,QAAQ,CAAC,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE;QACX,SAAS,EAAE,OAAO,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,sEAAsE;IACtE,MAAM,CAAC,EAAE,0BAA0B,GAAG,+BAA+B,CAAC;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kDAAkD;IAClD,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;gFAG4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,iDAAiD;IACjD,SAAS,EAAE,qBAAqB,CAAC;IACjC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,MAAM,EAAE,yBAAyB,CAAC;IAClC,mEAAmE;IACnE,SAAS,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACxC,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,MAAM,WAAW,sBAAsB;IACrC,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,SAAS,EAAE,qBAAqB,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,SAAS,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyCD,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAwB;gBAEzB,MAAM,EAAE,sBAAsB;IAO1C,sEAAsE;IACtE,IAAI,IAAI,mBAAmB,EAAE;IAI7B,kCAAkC;IAClC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAK3C,gEAAgE;IAChE,IAAI,IAAI,MAAM;IAMd;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,mBAAmB;IA4CpF;;;;OAIG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,mBAAmB,GAAG,IAAI;IAgD7G,0DAA0D;IAC1D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU3B,SAAS,IAAI,eAAe;IAc5B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,IAAI;IAoBZ,OAAO,CAAC,IAAI;CAab"}
|
|
@@ -186,6 +186,9 @@ export class SubscriptionPool {
|
|
|
186
186
|
if (patch.lastUsedAt !== undefined) {
|
|
187
187
|
acct.lastUsedAt = patch.lastUsedAt;
|
|
188
188
|
}
|
|
189
|
+
if (patch.lastRefreshAt !== undefined) {
|
|
190
|
+
acct.lastRefreshAt = patch.lastRefreshAt;
|
|
191
|
+
}
|
|
189
192
|
if (patch.email !== undefined) {
|
|
190
193
|
const em = patch.email.trim();
|
|
191
194
|
acct.email = em || undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionPool.js","sourceRoot":"","sources":["../../src/core/SubscriptionPool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"SubscriptionPool.js","sourceRoot":"","sources":["../../src/core/SubscriptionPool.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-08T20:59:17.649Z",
|
|
5
|
+
"instarVersion": "1.3.432",
|
|
6
6
|
"entryCount": 199,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The subscription-pool quota poller now auto-refreshes an expired OAuth access token from its stored refresh token before declaring an account `needs-reauth`. Previously any 401/403 from the usage endpoint was treated as a dead login — but a Claude Code access token expires routinely (~8–12h) while its refresh token stays valid for weeks, so the poller was wrongly flagging healthy accounts as needing re-authentication every time the short-lived token lapsed.
|
|
9
|
+
|
|
10
|
+
New `OAuthRefresher` performs the `refresh_token` grant against the public Claude Code OAuth token endpoint and writes the rotated credential back corruption-safely (validated-200 only, read-merge-write preserving all fields, fail-closed on any error). `QuotaPoller` calls it on a 401 and retries once; only a genuinely dead refresh token now yields `needs-reauth`. A new optional `lastRefreshAt` field plus a dashboard "Token auto-refreshed" line make the silent refresh visible.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
Your accounts no longer ask you to sign in again just because a day went by. A Claude login actually holds two keys — a short-lived one that turns over every several hours, and a long-lived one that lasts weeks. I now renew the short-lived key automatically using the long-lived one, exactly the way the Claude app does, so a routine daily expiry is invisible to you. I'll only ask you to sign in again when the real login is genuinely gone — a password change, a sign-out, or a new login elsewhere. The dashboard also shows a small "token auto-refreshed" note so you can see I'm handling it.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to Use |
|
|
19
|
+
|-----------|-----------|
|
|
20
|
+
| Auto-refresh of expired access tokens | automatic (subscription-pool quota poller) |
|
|
21
|
+
| Token-health visibility | Subscriptions dashboard tab — "Token auto-refreshed" line |
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
Reproduction (live, 2026-06-08, echo's two enrolled max accounts): both showed `status: needs-reauth` and a fresh `POST /subscription-pool/poll` failed 2/2. Inspecting the macOS keychain entries (`Claude Code-credentials-5939f6c8` / `-63465191`) showed `hasRefreshToken: true` with the access token `expiresAt` ~11–12h in the past — i.e. only the short-lived token had lapsed; the login was intact. The old code path mapped that 401 straight to `needs-reauth`.
|
|
26
|
+
|
|
27
|
+
After the fix: the unit/integration/e2e suites reproduce the exact sequence (usage read 401 → refresh-token exchange → retry 200) and assert the account stays `active` with a `lastRefreshAt` stamp rather than flipping to `needs-reauth`; the dead-refresh-token case still flips to `needs-reauth`. All three tiers green (`tests/unit/oauth-refresher.test.ts`, `tests/unit/quota-poller.test.ts`, `tests/integration/subscription-quota-routes.test.ts`, `tests/e2e/subscription-quota-lifecycle.test.ts`), tsc + repo lint clean.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Side-effects review — subscription-pool OAuth access-token auto-refresh
|
|
2
|
+
|
|
3
|
+
## What changed
|
|
4
|
+
- New `src/core/OAuthRefresher.ts`: a corruption-safe refresh-token→access-token exchange for a config home's Claude Code credential (keychain on macOS, `<configHome>/.credentials.json` elsewhere). Endpoint + client id are the public Claude Code values extracted from the official client binary.
|
|
5
|
+
- `src/core/QuotaPoller.ts`: on a usage-read 401/403 the poller now attempts a refresh + one retry BEFORE marking the account `needs-reauth`. `defaultTokenResolver` refactored to share the OAuthRefresher locator (single source of truth for where a config home's credential lives).
|
|
6
|
+
- `src/core/SubscriptionPool.ts`: new optional `lastRefreshAt` field (visibility only).
|
|
7
|
+
- `dashboard/subscriptions.js`: a "Token auto-refreshed <ago>" line + `relativeAge()` helper.
|
|
8
|
+
|
|
9
|
+
## Blast radius / who is affected
|
|
10
|
+
- Only anthropic/claude-code accounts in a SubscriptionPool. A pool of zero accounts (single-account agents) is entirely unaffected — the poller only runs when accounts are enrolled.
|
|
11
|
+
- Behavior change is strictly a NARROWING of when `needs-reauth` fires: it no longer fires on a recoverable access-token expiry. Genuinely dead logins still flip to `needs-reauth` exactly as before.
|
|
12
|
+
|
|
13
|
+
## Corruption safety (the load-bearing concern)
|
|
14
|
+
- The ONLY mutation is the credential write-back. It is gated three ways: (1) only on a fully-validated 200 (new access token shaped `sk-ant-oat…`, positive numeric `expires_in`); (2) read-merge-write that preserves every existing field (scopes, subscriptionType, rateLimitTier, unknown fields); (3) refresh-token rotation persisted when present, old token kept when the server doesn't rotate — never dropped.
|
|
15
|
+
- Any failure (wrong endpoint, wrong client id, network error, malformed body, write failure) writes NOTHING and returns a typed failure → the caller's existing `needs-reauth` path. A misconfiguration can only ever fail to improve, never corrupt a working login. This is fail-CLOSED, not silent-degrade-to-heuristic (consistent with the no-silent-llm-fallback standard, though this path is auth not inference).
|
|
16
|
+
|
|
17
|
+
## Secrets handling
|
|
18
|
+
- Token values are never logged and never returned to any persisted surface. The pool registry's existing `assertNoCredentialFields` guard still rejects any credential-bearing field name; `lastRefreshAt` is a timestamp, not a secret.
|
|
19
|
+
|
|
20
|
+
## Framework generality
|
|
21
|
+
- Scoped to claude-code OAuth specifically (the refresh-token grant is provider/framework-specific). It does NOT touch the session launch/inject abstraction (frameworkSessionLaunch / MessageDelivery), so it cannot regress codex-cli / gemini-cli / pi-cli. Non-claude-code accounts are skipped by the same provider/framework guard the poller already applies.
|
|
22
|
+
|
|
23
|
+
## Migration parity
|
|
24
|
+
- No agent-installed files change (no settings.json hooks, no config defaults, no CLAUDE.md template, no hook scripts, no skills). This is an in-process monitoring component; existing agents pick it up on the next server update. The new `lastRefreshAt` field is optional and back-compatible with existing `subscription-pool.json` on disk.
|
|
25
|
+
|
|
26
|
+
## Tests
|
|
27
|
+
- Tier 1: `tests/unit/oauth-refresher.test.ts` (rotation, no-rotation, no-refresh-token, exchange-failed, malformed, read-failed, write-failed, locator, request shape) + extended `tests/unit/quota-poller.test.ts` (refresh-recovers, still-401-after-refresh, dead-refresh-token) + `tests/unit/subscriptions-render.test.ts` (token-health line + relativeAge).
|
|
28
|
+
- Tier 2: `tests/integration/subscription-quota-routes.test.ts` — recovery + dead-login through the real `/subscription-pool/poll` HTTP route.
|
|
29
|
+
- Tier 3: `tests/e2e/subscription-quota-lifecycle.test.ts` — expired token auto-refreshes end-to-end, account stays active, stamped to the on-disk registry.
|