fix(bar): bound native credential and quota waits

This commit is contained in:
Tam Nhu Tran committed 2026-08-08 21:12:28 -04:00
1 parent 833473c5f6
commit da2de60015
8 files changed
+201 -89

No files matched your search

+1 -1
View File
@@ -725,7 +725,7 @@
"topOver400": [
{
"file": "src/web-server/usage/native-quota-collector.ts",
"loc": 1730
"loc": 1758
},
{
"file": "src/web-server/routes/cliproxy-auth-routes.ts",
+1 -1
View File
@@ -89,7 +89,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| File | LOC |
|---|---:|
| `src/web-server/usage/native-quota-collector.ts` | 1730 |
| `src/web-server/usage/native-quota-collector.ts` | 1758 |
| `src/web-server/routes/cliproxy-auth-routes.ts` | 1531 |
| `src/cliproxy/auth/oauth-handler.ts` | 1510 |
| `src/cursor/cursor-executor.ts` | 1234 |
+23 -11
View File
@@ -243,6 +243,11 @@ function withTimeout<T>(p: Promise<T>, ms: number): Promise<T | null> {
});
}
/** Remaining milliseconds before one absolute request deadline. */
function remainingRequestBudget(deadlineAt: number): number {
return Math.max(0, deadlineAt - Date.now());
}
/**
* Map a row to its wire shape. The native-only additions use snake_case parent
* keys ("quota_windows" / "stale_as_of") to match the existing payload's mixed
@@ -486,6 +491,7 @@ export function createBarRouter(deps: BarRouterDeps): Router {
*/
router.get('/summary', async (req: Request, res: Response): Promise<void> => {
try {
const deadlineAt = Date.now() + REQUEST_DEADLINE_MS;
const wantsRefresh = req.query['refresh'] === 'true';
// Determine effective refresh mode after applying debounce.
@@ -503,10 +509,20 @@ export function createBarRouter(deps: BarRouterDeps): Router {
// else: debounce active — fall through to cache path
}
// Start the native side-load immediately. It shares the same absolute
// response deadline as cost and CLIProxy quota work, so their individual
// fallback waits cannot stack into a multi-second tail.
const getNative = deps.getNativeAccountRows ?? (async () => [] as BarSummaryRow[]);
const getCachedNative = deps.getCachedNativeRows ?? (() => [] as BarSummaryRow[]);
const nativePromise = Promise.resolve().then(() => getNative({ force: doForceRefresh }));
// Cost side-load is bounded so a slow usage-snapshot read can't stall the
// glance. (Health is per-account, derived from each quota result below —
// no blocking system audit on the request path.)
const details = await withTimeout(deps.loadCliproxyDetails(), SIDELOAD_TIMEOUT_MS);
const details = await withTimeout(
deps.loadCliproxyDetails(),
Math.min(SIDELOAD_TIMEOUT_MS, remainingRequestBudget(deadlineAt))
);
const costByAccount: Record<string, number> = details
? deps.getTodayCostByAccount(details)
: {};
@@ -565,24 +581,20 @@ export function createBarRouter(deps: BarRouterDeps): Router {
return rows;
})();
const deadline = new Promise<BarSummaryRow[]>((resolve) => {
setTimeout(() => resolve(cacheRows()), REQUEST_DEADLINE_MS);
});
const rows = (await withTimeout(gather, remainingRequestBudget(deadlineAt))) ?? cacheRows();
const rows = await Promise.race([gather, deadline]);
// Native subscription rows (Claude Code + Codex) are side-loaded AFTER the
// Native subscription rows (Claude Code + Codex) are joined after the
// CLIProxy rows resolve, bounded so a slow/failed native fetch degrades
// rather than blocking or erroring the response. Pass force so a
// debounce-passing refresh also re-pulls native rows live. On timeout fall
// back to the last-known cached native rows (NOT []) so a slow forced
// re-pull never momentarily drops the Claude/Codex cards; the in-flight
// fetch keeps warming the cache for the next poll.
const getNative = deps.getNativeAccountRows ?? (async () => [] as BarSummaryRow[]);
const getCachedNative = deps.getCachedNativeRows ?? (() => [] as BarSummaryRow[]);
const nativeRows =
(await withTimeout(getNative({ force: doForceRefresh }), NATIVE_SIDELOAD_TIMEOUT_MS)) ??
getCachedNative();
(await withTimeout(
nativePromise,
Math.min(NATIVE_SIDELOAD_TIMEOUT_MS, remainingRequestBudget(deadlineAt))
)) ?? getCachedNative();
res.json([...rows, ...nativeRows].map(serializeBarRow));
} catch (err) {
@@ -15,7 +15,7 @@
*/
import { existsSync, readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { execFile } from 'node:child_process';
import * as crypto from 'node:crypto';
import * as os from 'node:os';
import * as path from 'node:path';
@@ -37,10 +37,20 @@ export interface CredentialReaderDeps {
homedir?: string;
existsSyncImpl?: (p: string) => boolean;
readFileSyncImpl?: (p: string) => string;
execSyncImpl?: (cmd: string, opts: Record<string, unknown>) => string | Buffer;
execFileImpl?: ExecFileImpl;
keychainTimeoutMs?: number;
}
type ExecFileCallback = (error: Error | null, stdout: string | Buffer) => void;
type ExecFileImpl = (
file: string,
args: readonly string[],
options: Record<string, unknown>,
callback: ExecFileCallback
) => { kill?: () => void } | void;
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
const SECURITY_PATH = '/usr/bin/security';
const KEYCHAIN_TIMEOUT_MS = 5000;
/** Subscription types that mean "no real subscription" -> skip the fetch. */
@@ -68,14 +78,13 @@ function parseCredentials(raw: string): ClaudeNativeCredentials | null {
* Keychain as a fallback. Returns null when neither source yields a parseable
* object.
*/
export function readClaudeCredentials(
export async function readClaudeCredentials(
deps: CredentialReaderDeps = {}
): ClaudeNativeCredentials | null {
): Promise<ClaudeNativeCredentials | null> {
const platform = deps.platform ?? os.platform();
const homedir = deps.homedir ?? os.homedir();
const existsImpl = deps.existsSyncImpl ?? existsSync;
const readImpl = deps.readFileSyncImpl ?? ((p: string) => readFileSync(p, 'utf8'));
const execImpl = deps.execSyncImpl ?? execSync;
const credentialsPath = path.join(homedir, '.claude', '.credentials.json');
if (existsImpl(credentialsPath)) {
@@ -88,7 +97,7 @@ export function readClaudeCredentials(
}
if (platform === 'darwin') {
const parsed = readCredentialsFromKeychainService(KEYCHAIN_SERVICE, execImpl);
const parsed = await readCredentialsFromKeychainService(KEYCHAIN_SERVICE, deps);
if (parsed) return parsed;
}
@@ -96,25 +105,49 @@ export function readClaudeCredentials(
}
/** Read + parse one Keychain generic-password item. Returns null on any failure. */
function readCredentialsFromKeychainService(
async function readCredentialsFromKeychainService(
service: string,
execImpl: NonNullable<CredentialReaderDeps['execSyncImpl']>
): ClaudeNativeCredentials | null {
try {
const out = execImpl(`security find-generic-password -s "${service}" -w`, {
timeout: KEYCHAIN_TIMEOUT_MS,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
const raw = (typeof out === 'string' ? out : out.toString('utf8')).trim();
if (raw) {
const parsed = parseCredentials(raw);
if (parsed) return parsed;
deps: CredentialReaderDeps
): Promise<ClaudeNativeCredentials | null> {
const timeoutMs = deps.keychainTimeoutMs ?? KEYCHAIN_TIMEOUT_MS;
const execImpl: ExecFileImpl =
deps.execFileImpl ??
((file, args, options, callback) =>
execFile(file, [...args], options, (error, stdout) => callback(error, stdout)));
return new Promise((resolve) => {
let settled = false;
let child: { kill?: () => void } | void;
const finish = (credentials: ClaudeNativeCredentials | null): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(credentials);
};
const timer = setTimeout(() => {
child?.kill?.();
finish(null);
}, timeoutMs);
try {
child = execImpl(
SECURITY_PATH,
['find-generic-password', '-s', service, '-w'],
{
timeout: timeoutMs,
encoding: 'utf8',
windowsHide: true,
maxBuffer: 1024 * 1024,
},
(error, stdout) => {
if (error) return finish(null);
const raw = (typeof stdout === 'string' ? stdout : stdout.toString('utf8')).trim();
finish(raw ? parseCredentials(raw) : null);
}
);
} catch {
finish(null);
}
} catch {
// no Keychain entry / access denied -> null
}
return null;
});
}
/**
@@ -135,14 +168,13 @@ export function claudeKeychainServiceForConfigDir(configDir: string): string {
* OAuth tokens in the Keychain by default, so without the Keychain fallback
* every isolated profile looks permanently logged-out to the bar.
*/
export function readClaudeCredentialsForConfigDir(
export async function readClaudeCredentialsForConfigDir(
configDir: string,
deps: CredentialReaderDeps = {}
): ClaudeNativeCredentials | null {
): Promise<ClaudeNativeCredentials | null> {
const platform = deps.platform ?? os.platform();
const existsImpl = deps.existsSyncImpl ?? existsSync;
const readImpl = deps.readFileSyncImpl ?? ((p: string) => readFileSync(p, 'utf8'));
const execImpl = deps.execSyncImpl ?? execSync;
const credentialsPath = path.join(configDir, '.credentials.json');
if (existsImpl(credentialsPath)) {
@@ -155,10 +187,7 @@ export function readClaudeCredentialsForConfigDir(
}
if (platform === 'darwin') {
return readCredentialsFromKeychainService(
claudeKeychainServiceForConfigDir(configDir),
execImpl
);
return readCredentialsFromKeychainService(claudeKeychainServiceForConfigDir(configDir), deps);
}
return null;
+19 -14
View File
@@ -110,13 +110,15 @@ const CODEX_PROVIDER = CODEX_NATIVE_PROVIDER;
export interface NativeQuotaDeps {
/** Read the native Claude Code credentials (global default path). */
readCredentials?: () => ClaudeNativeCredentials | null;
readCredentials?: () => ClaudeNativeCredentials | null | Promise<ClaudeNativeCredentials | null>;
/**
* Read credentials for a specific Claude profile (file-first, Keychain fallback).
* Injected so tests never touch real fs or Keychain.
* profile: the profile name (e.g. "work"); returns null when absent/unparseable.
*/
readClaudeCredentialsForProfile?: (profile: string) => ClaudeNativeCredentials | null;
readClaudeCredentialsForProfile?: (
profile: string
) => ClaudeNativeCredentials | null | Promise<ClaudeNativeCredentials | null>;
/** Fetch Claude quota with a directly-supplied native token. */
fetchClaudeQuota?: (accessToken: string, accountId?: string) => Promise<ClaudeQuotaResult>;
/**
@@ -495,14 +497,17 @@ function serveCached(state: ProviderState): BarSummaryRow | null {
/**
* Read credentials for a specific Claude Code profile (file-first, Keychain fallback).
*
* Looks for .credentials.json in the profile's instance directory. If the file
* is absent or unparseable, returns null — the caller emits a parked row.
* Never calls security/Keychain — zero new keychain access from this feature.
* Looks for .credentials.json in the profile's instance directory, then uses
* the bounded per-config-dir macOS Keychain fallback. If neither yields a
* parseable credential object, the caller emits a parked row.
*/
function readClaudeCredentialsForProfileFromDisk(
async function readClaudeCredentialsForProfileFromDisk(
profile: string,
readDefaultCredentials: () => ClaudeNativeCredentials | null = readClaudeCredentials
): ClaudeNativeCredentials | null {
readDefaultCredentials: () =>
| ClaudeNativeCredentials
| null
| Promise<ClaudeNativeCredentials | null> = readClaudeCredentials
): Promise<ClaudeNativeCredentials | null> {
try {
const instanceDir = path.join(getCcsDir(), 'instances', profile);
@@ -510,7 +515,7 @@ function readClaudeCredentialsForProfileFromDisk(
// ~/.claude/.credentials.json, falling back to the single global
// "Claude Code-credentials" Keychain item that Claude Code itself maintains.
if (profile === DEFAULT_PROFILE && !fs.existsSync(instanceDir)) {
return readDefaultCredentials();
return await readDefaultCredentials();
}
// Isolated `ccs auth` instance: <instanceDir>/.credentials.json first, then
@@ -518,7 +523,7 @@ function readClaudeCredentialsForProfileFromDisk(
// CLAUDE_CONFIG_DIR ("Claude Code-credentials-<sha256(dir)[0..8]>"). On
// macOS Claude Code stores tokens in the Keychain by default, so without
// the Keychain read every isolated profile is permanently parked.
return readClaudeCredentialsForConfigDir(instanceDir);
return await readClaudeCredentialsForConfigDir(instanceDir);
} catch {
return null;
}
@@ -916,12 +921,12 @@ async function collectClaudeRowForProfile(
// pending DURING assignment, leaving a stale resolved promise that the
// next call's coalescing check would return instead of re-evaluating.
await Promise.resolve();
const creds = readCreds(profile);
const creds = await readCreds(profile);
// No credentials file found -> emit parked row (needs auth, file absent).
// This is the expected case when the profile exists in the registry but the
// user has not logged in via 'ccs auth' for this machine or the credentials
// are stored only in keychain (which we deliberately do not access here).
// user has not logged in via 'ccs auth' for this machine or neither the
// profile file nor its bounded macOS Keychain fallback yielded credentials.
if (!creds) {
const parkedRow = buildParkedClaudeProfileRow(profile, now);
// Cache the parked row so repeated calls don't re-stat the fs.
@@ -1238,7 +1243,7 @@ async function collectClaudeRow(
state.pending = (async (): Promise<BarSummaryRow | null> => {
try {
const creds = readCredentialsFn();
const creds = await readCredentialsFn();
// No token / unsupported subscription -> never spend a call, omit the row.
if (!creds || !hasSupportedSubscription(creds)) {
return serveCached(state);
+43
View File
@@ -1116,6 +1116,49 @@ describe('/summary force flag passed to getNativeAccountRows', () => {
expect(status).toBe(200);
expect(body.some((r) => r.provider === 'codex')).toBe(true);
});
it('enforces one absolute deadline across cost, quota, and blocked native work', async () => {
const { createBarRouter, resetForceFreshDebounce: resetDebounce } = await import(
'../../../src/web-server/routes/bar-routes'
);
const app = express();
app.use(express.json());
const router = createBarRouter({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAllAccountsSummary: () => ({ agy: [makeAccountInfo()] }) as any,
getCachedQuota: () => makeQuotaResult(),
setCachedQuota: () => {},
invalidateQuotaCache: () => {},
fetchAccountQuota: () => new Promise(() => {}),
getTodayCostByAccount: () => ({}),
loadCliproxyDetails: () => new Promise((resolve) => setTimeout(() => resolve([]), 1_400)),
loadDailyUsage: async () => [],
loadHourlyUsage: async () => [],
getNativeAccountRows: () => new Promise(() => {}),
getCachedNativeRows: () => [],
});
app.use('/api/bar', router);
const srv = await new Promise<Server>((resolve, reject) => {
const instance = app.listen(0, '127.0.0.1');
instance.once('error', reject);
instance.once('listening', () => resolve(instance));
});
const addr = srv.address();
if (!addr || typeof addr === 'string') throw new Error('No server address');
resetDebounce();
const startedAt = Date.now();
const { status } = await getJson<BarSummaryRow[]>(
`http://127.0.0.1:${(addr as { port: number }).port}`,
'/api/bar/summary?refresh=true'
);
const elapsed = Date.now() - startedAt;
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(status).toBe(200);
expect(elapsed).toBeGreaterThanOrEqual(2_200);
expect(elapsed).toBeLessThan(3_200);
});
});
// ============================================================================
@@ -27,14 +27,14 @@ function makeCreds(overrides: Record<string, unknown> = {}): ClaudeNativeCredent
}
describe('readClaudeCredentials', () => {
it('parses the on-disk credentials file when present (file-first, no Keychain)', () => {
it('parses the on-disk credentials file when present (file-first, no Keychain)', async () => {
let keychainCalled = false;
const creds = readClaudeCredentials({
const creds = await readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => true,
readFileSyncImpl: () => JSON.stringify(makeCreds()),
execSyncImpl: () => {
execFileImpl: () => {
keychainCalled = true;
return '';
},
@@ -44,44 +44,52 @@ describe('readClaudeCredentials', () => {
expect(keychainCalled).toBe(false);
});
it('falls back to the macOS Keychain when the file is absent', () => {
const creds = readClaudeCredentials({
it('falls back to the macOS Keychain when the file is absent', async () => {
let executable = '';
let args: readonly string[] = [];
const creds = await readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('should not read file');
},
execSyncImpl: () => JSON.stringify(makeCreds({ subscriptionType: 'pro' })),
execFileImpl: (file, receivedArgs, _options, callback) => {
executable = file;
args = receivedArgs;
callback(null, JSON.stringify(makeCreds({ subscriptionType: 'pro' })));
},
});
expect(creds?.claudeAiOauth?.subscriptionType).toBe('pro');
expect(executable).toBe('/usr/bin/security');
expect(args).toEqual(['find-generic-password', '-s', 'Claude Code-credentials', '-w']);
});
it('returns null when both file and Keychain are absent', () => {
const creds = readClaudeCredentials({
it('returns null when both file and Keychain are absent', async () => {
const creds = await readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
throw new Error('no keychain entry');
execFileImpl: (_file, _args, _options, callback) => {
callback(new Error('no keychain entry'), '');
},
});
expect(creds).toBeNull();
});
it('does not consult the Keychain on non-darwin platforms', () => {
it('does not consult the Keychain on non-darwin platforms', async () => {
let keychainCalled = false;
const creds = readClaudeCredentials({
const creds = await readClaudeCredentials({
platform: 'linux',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
execFileImpl: () => {
keychainCalled = true;
return '';
},
@@ -149,16 +157,16 @@ describe('readClaudeCredentialsForConfigDir', () => {
const configDir = '/home/test/.ccs/instances/work';
const credFile = `${configDir}/.credentials.json`;
it('reads <configDir>/.credentials.json when present (file-first, no Keychain)', () => {
it('reads <configDir>/.credentials.json when present (file-first, no Keychain)', async () => {
let keychainCalled = false;
const creds = readClaudeCredentialsForConfigDir(configDir, {
const creds = await readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: (p: string) => p === credFile,
readFileSyncImpl: (p: string) => {
expect(p).toBe(credFile);
return JSON.stringify(makeCreds());
},
execSyncImpl: () => {
execFileImpl: () => {
keychainCalled = true;
return '';
},
@@ -167,46 +175,61 @@ describe('readClaudeCredentialsForConfigDir', () => {
expect(keychainCalled).toBe(false);
});
it('falls back to the per-config-dir Keychain service when the file is absent', () => {
let keychainCmd = '';
const creds = readClaudeCredentialsForConfigDir(configDir, {
it('uses absolute security path and argument array for the per-config-dir Keychain item', async () => {
let executable = '';
let args: readonly string[] = [];
const creds = await readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('should not read file');
},
execSyncImpl: (cmd: string) => {
keychainCmd = cmd;
return JSON.stringify(makeCreds({ subscriptionType: 'team' }));
execFileImpl: (file, receivedArgs, _options, callback) => {
executable = file;
args = receivedArgs;
callback(null, JSON.stringify(makeCreds({ subscriptionType: 'team' })));
},
});
expect(creds?.claudeAiOauth?.subscriptionType).toBe('team');
expect(keychainCmd).toContain('Claude Code-credentials-ffeb4b45');
expect(executable).toBe('/usr/bin/security');
expect(args).toEqual(['find-generic-password', '-s', 'Claude Code-credentials-ffeb4b45', '-w']);
});
it('returns null when both file and Keychain are absent', () => {
const creds = readClaudeCredentialsForConfigDir(configDir, {
it('returns null when both file and Keychain are absent', async () => {
const creds = await readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
throw new Error('no keychain entry');
execFileImpl: (_file, _args, _options, callback) => {
callback(new Error('no keychain entry'), '');
},
});
expect(creds).toBeNull();
});
it('does not consult the Keychain on non-darwin platforms', () => {
it('bounds a blocked Keychain lookup without exposing a credential payload', async () => {
const startedAt = Date.now();
const creds = await readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: () => false,
keychainTimeoutMs: 20,
execFileImpl: () => ({ kill: () => {} }),
});
expect(creds).toBeNull();
expect(Date.now() - startedAt).toBeLessThan(250);
});
it('does not consult the Keychain on non-darwin platforms', async () => {
let keychainCalled = false;
const creds = readClaudeCredentialsForConfigDir(configDir, {
const creds = await readClaudeCredentialsForConfigDir(configDir, {
platform: 'linux',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
execFileImpl: () => {
keychainCalled = true;
return '';
},
@@ -1032,7 +1032,7 @@ function makeMultiProfileDeps(opts: {
listCodexProfiles: () => codexProfiles,
defaultClaudeProfile: () => claudeDefault,
defaultCodexProfile: () => codexDefault,
// Credential seams (file-only, no keychain)
// Credential seams (fully injected; no real filesystem or Keychain access)
readClaudeCredentialsForProfile: credsForProfile,
readCodexNativeAuth: codexNativeAuth,
// Fetch seams
@@ -1195,7 +1195,7 @@ describe('multi-profile: account_id and wire fields', () => {
});
});
describe('multi-profile: Claude file-only reader', () => {
describe('multi-profile: Claude credential reader', () => {
it('profile with .credentials.json present -> live fetch row (paused:false when default)', async () => {
const clock = { now: 1_000_000 };
const deps = makeMultiProfileDeps({