Use managed Claude credentials for selected usage

This commit is contained in:
Jinwoo-H
2026-06-12 16:42:12 -07:00
parent 630bcce530
commit 44cceb12a6
2 changed files with 88 additions and 4 deletions
@@ -167,6 +167,50 @@ describe('fetchClaudeRateLimits', () => {
)
})
it('uses selected managed account credentials before stale runtime Keychain credentials', async () => {
const configDir = '/Users/test/.claude'
const authPreparation: ClaudeRuntimeAuthPreparation = {
configDir,
envPatch: {},
stripAuthEnv: true,
provenance: 'managed:account-1'
}
vi.mocked(readManagedClaudeKeychainCredentials).mockResolvedValue(
JSON.stringify({
claudeAiOauth: {
accessToken: 'managed-oauth-token',
expiresAt: Date.now() + 60_000
}
})
)
vi.mocked(readActiveClaudeKeychainCredentialsStrict).mockResolvedValue(
JSON.stringify({
claudeAiOauth: {
accessToken: 'stale-runtime-oauth-token',
expiresAt: Date.now() + 60_000
}
})
)
await expect(fetchClaudeRateLimits({ authPreparation })).resolves.toMatchObject({
provider: 'claude',
status: 'ok',
session: { usedPercent: 12 },
weekly: { usedPercent: 34 }
})
expect(readManagedClaudeKeychainCredentials).toHaveBeenCalledWith('account-1')
expect(readActiveClaudeKeychainCredentialsStrict).not.toHaveBeenCalled()
expect(netFetchMock).toHaveBeenCalledWith(
'https://api.anthropic.com/api/oauth/usage',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer managed-oauth-token'
})
})
)
})
it('falls back to the credentials file when Keychain access fails', async () => {
const configDir = '/Users/test/.claude'
const authPreparation: ClaudeRuntimeAuthPreparation = {
+44 -4
View File
@@ -140,6 +140,29 @@ async function readCredentialsFromStrictKeychain(
}
}
function getManagedAccountIdFromProvenance(provenance?: string): string | null {
const match = provenance?.match(/^managed:([^:]+)(?::|$)/)
return match?.[1] ?? null
}
async function readManagedCredentialsFromProvenance(
provenance?: string
): Promise<OAuthCredentialReadResult> {
if (process.platform !== 'darwin') {
return emptyOAuthCredentialReadResult()
}
const accountId = getManagedAccountIdFromProvenance(provenance)
if (!accountId) {
return emptyOAuthCredentialReadResult()
}
try {
const raw = await readManagedClaudeKeychainCredentials(accountId)
return raw ? parseOAuthCredentialsJson(raw) : emptyOAuthCredentialReadResult()
} catch {
return emptyOAuthCredentialReadResult()
}
}
/**
* Read OAuth token from ~/.claude/.credentials.json (legacy path).
* Why: older Claude CLI versions store credentials in this plain JSON
@@ -161,8 +184,22 @@ async function readFromCredentialsFile(configDir?: string): Promise<OAuthCredent
* here — those are API keys which return 401 on the OAuth usage endpoint.
* API-key users are served by the PTY fallback instead.
*/
async function readOAuthCredentials(configDir?: string): Promise<OAuthCredentialReadResult> {
// 1. macOS Keychain (Claude Max/Pro OAuth)
async function readOAuthCredentials(
configDir?: string,
provenance?: string
): Promise<OAuthCredentialReadResult> {
// 1. Orca managed-account storage. The shared Claude runtime Keychain can
// drift when live Claude sessions refresh or rewrite auth, but the status bar
// must report usage for the account Orca has selected.
const fromManagedAccount = await readManagedCredentialsFromProvenance(provenance)
if (fromManagedAccount.token) {
return fromManagedAccount
}
if (fromManagedAccount.hasRefreshableCredentials) {
return fromManagedAccount
}
// 2. macOS Keychain (Claude Max/Pro OAuth)
const fromKeychain = await readFromKeychain(configDir)
if (fromKeychain.token) {
return fromKeychain
@@ -171,7 +208,7 @@ async function readOAuthCredentials(configDir?: string): Promise<OAuthCredential
return fromKeychain
}
// 2. Legacy credentials file
// 3. Legacy credentials file
const fromFile = await readFromCredentialsFile(configDir)
if (fromFile.token) {
return fromFile
@@ -299,7 +336,10 @@ export async function fetchClaudeRateLimits(
}
// Path A: try OAuth API if we have a genuine OAuth token
const oauthCredentials = await readOAuthCredentials(options?.authPreparation?.configDir)
const oauthCredentials = await readOAuthCredentials(
options?.authPreparation?.configDir,
options?.authPreparation?.provenance
)
if (oauthCredentials.token) {
try {
return await fetchViaOAuth(oauthCredentials.token)