mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
Use managed Claude credentials for selected usage
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user