mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
Read the scoped Keychain channel the managed bridge actually uses
The scoped Keychain mock keys managed config dirs by path, so tests seeding the shared field left the bridge re-reading whatever the previous launch wrote — the foreign credential never reached the code under test and the identity assertions passed without exercising the guard. Splits the macOS bridge cases into their own file; the combined file crossed the 800-line ceiling. Claude-Session: https://claude.ai/code/session_012E63B2jhajtUbArQHhZjVQ
This commit is contained in:
@@ -41,171 +41,6 @@ describe('ClaudeRuntimeAuthService', () => {
|
||||
cleanupRuntimeAuthTestState()
|
||||
})
|
||||
|
||||
it('bridges host managed credentials into the macOS config-scoped Keychain item', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const credentials = createClaudeCredentialsJson('user@example.com', 'managed')
|
||||
const managedAuthPath = createManagedClaudeAuth(testState.userDataDir, 'account-1', credentials)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.scopedKeychainCredentials).toBe(credentials)
|
||||
expect(testState.legacyKeychainCredentials).toBeNull()
|
||||
})
|
||||
|
||||
it('repairs a stale scoped Keychain item after re-authentication', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const staleCredentials = createClaudeCredentialsJson('user@example.com', 'stale')
|
||||
const freshCredentials = createClaudeCredentialsJson('user@example.com', 'fresh')
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
staleCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
testState.managedKeychainCredentials.set('account-1', freshCredentials)
|
||||
testState.scopedKeychainCredentials = staleCredentials
|
||||
service.clearLastWrittenCredentialsJson('account-1')
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(freshCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(freshCredentials)
|
||||
})
|
||||
|
||||
it('does not import a different identity from the scoped Keychain item', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsJson(
|
||||
'user@example.com',
|
||||
'managed',
|
||||
null,
|
||||
2_000
|
||||
)
|
||||
const foreignCredentials = createClaudeCredentialsJson(
|
||||
'other@example.com',
|
||||
'foreign',
|
||||
null,
|
||||
3_000
|
||||
)
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
testState.scopedKeychainCredentials = foreignCredentials
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(managedCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(managedCredentials)
|
||||
})
|
||||
|
||||
it('does not import an older same-identity scoped Keychain credential', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsJson(
|
||||
'user@example.com',
|
||||
'managed',
|
||||
null,
|
||||
2_000
|
||||
)
|
||||
const staleCredentials = createClaudeCredentialsJson('user@example.com', 'stale', null, 1_000)
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
testState.scopedKeychainCredentials = staleCredentials
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(managedCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(managedCredentials)
|
||||
})
|
||||
|
||||
it('retains a visible degraded provenance after scoped Keychain bridge failure', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const credentials = createClaudeCredentialsJson('user@example.com', 'managed')
|
||||
const managedAuthPath = createManagedClaudeAuth(testState.userDataDir, 'account-1', credentials)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
testState.throwScopedKeychainWrite = true
|
||||
|
||||
const launchPreparation = await service.prepareForClaudeLaunch()
|
||||
expect(launchPreparation.provenance).toBe('system:managed-keychain-unavailable')
|
||||
|
||||
testState.throwScopedKeychainWrite = false
|
||||
await expect(service.prepareForRateLimitFetch()).resolves.toMatchObject({
|
||||
provenance: 'system:managed-keychain-unavailable',
|
||||
stripAuthEnv: false
|
||||
})
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
await expect(service.prepareForRateLimitFetch()).resolves.toMatchObject({
|
||||
provenance: 'managed:account-1',
|
||||
stripAuthEnv: true
|
||||
})
|
||||
})
|
||||
|
||||
it('reads back refreshed active keychain credentials on macOS', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import {
|
||||
cleanupRuntimeAuthTestState,
|
||||
createClaudeAccount,
|
||||
createClaudeCredentialsJson,
|
||||
createClaudeCredentialsWithoutEmail,
|
||||
createElectronMock,
|
||||
createKeychainMock,
|
||||
createManagedClaudeAuth,
|
||||
createOauthRefreshMock,
|
||||
createSettings,
|
||||
createStore,
|
||||
resetRuntimeAuthTestState,
|
||||
setScopedKeychainCredentialsForManagedPath,
|
||||
testState
|
||||
} from './runtime-auth-service-test-harness'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
vi.mock('electron', () => createElectronMock())
|
||||
|
||||
vi.mock('./oauth-refresh', () => createOauthRefreshMock())
|
||||
|
||||
vi.mock('node:os', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:os')>('node:os') // eslint-disable-line @typescript-eslint/consistent-type-imports -- vi.importActual requires inline import()
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => testState.fakeHomeDir
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./keychain', () => createKeychainMock())
|
||||
|
||||
describe('ClaudeRuntimeAuthService', () => {
|
||||
beforeEach(() => {
|
||||
resetRuntimeAuthTestState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanupRuntimeAuthTestState()
|
||||
})
|
||||
|
||||
it('bridges host managed credentials into the macOS config-scoped Keychain item', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const credentials = createClaudeCredentialsJson('user@example.com', 'managed')
|
||||
const managedAuthPath = createManagedClaudeAuth(testState.userDataDir, 'account-1', credentials)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.scopedKeychainCredentials).toBe(credentials)
|
||||
expect(testState.legacyKeychainCredentials).toBeNull()
|
||||
})
|
||||
|
||||
it('repairs a stale scoped Keychain item after re-authentication', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const staleCredentials = createClaudeCredentialsJson('user@example.com', 'stale')
|
||||
const freshCredentials = createClaudeCredentialsJson('user@example.com', 'fresh')
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
staleCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
testState.managedKeychainCredentials.set('account-1', freshCredentials)
|
||||
testState.scopedKeychainCredentials = staleCredentials
|
||||
service.clearLastWrittenCredentialsJson('account-1')
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(freshCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(freshCredentials)
|
||||
})
|
||||
|
||||
it('does not import a different identity from the scoped Keychain item', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsJson(
|
||||
'user@example.com',
|
||||
'managed',
|
||||
null,
|
||||
2_000
|
||||
)
|
||||
const foreignCredentials = createClaudeCredentialsJson(
|
||||
'other@example.com',
|
||||
'foreign',
|
||||
null,
|
||||
3_000
|
||||
)
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
setScopedKeychainCredentialsForManagedPath(managedAuthPath, foreignCredentials)
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(managedCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(managedCredentials)
|
||||
})
|
||||
|
||||
it('does not import an older same-identity scoped Keychain credential', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsJson(
|
||||
'user@example.com',
|
||||
'managed',
|
||||
null,
|
||||
2_000
|
||||
)
|
||||
const staleCredentials = createClaudeCredentialsJson('user@example.com', 'stale', null, 1_000)
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
setScopedKeychainCredentialsForManagedPath(managedAuthPath, staleCredentials)
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(managedCredentials)
|
||||
expect(testState.scopedKeychainCredentials).toBe(managedCredentials)
|
||||
})
|
||||
|
||||
// Why: real credential blobs carry no identity fields, so identity can only come from the
|
||||
// .claude.json the CLI keeps inside the config dir those credentials belong to.
|
||||
it('adopts a rotated scoped credential proven by the managed config dir identity', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsWithoutEmail('managed', null, {
|
||||
expiresAt: 2_000,
|
||||
refreshToken: 'managed-refresh'
|
||||
})
|
||||
const rotatedCredentials = createClaudeCredentialsWithoutEmail('rotated', null, {
|
||||
expiresAt: 3_000,
|
||||
refreshToken: 'rotated-refresh'
|
||||
})
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
writeFileSync(
|
||||
join(managedAuthPath, '.claude.json'),
|
||||
JSON.stringify({
|
||||
oauthAccount: { accountUuid: 'account-1', emailAddress: 'user@example.com' }
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
setScopedKeychainCredentialsForManagedPath(managedAuthPath, rotatedCredentials)
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(rotatedCredentials)
|
||||
})
|
||||
|
||||
// Why: a stale shared .claude.json left by migration must not vouch for a different login that
|
||||
// happened inside the managed pane.
|
||||
it('rejects a scoped credential whose managed config dir identity is a different account', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const managedCredentials = createClaudeCredentialsWithoutEmail('managed', null, {
|
||||
expiresAt: 2_000,
|
||||
refreshToken: 'managed-refresh'
|
||||
})
|
||||
const foreignCredentials = createClaudeCredentialsWithoutEmail('foreign', null, {
|
||||
expiresAt: 3_000,
|
||||
refreshToken: 'foreign-refresh'
|
||||
})
|
||||
const managedAuthPath = createManagedClaudeAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
managedCredentials
|
||||
)
|
||||
writeFileSync(
|
||||
join(testState.fakeHomeDir, '.claude.json'),
|
||||
JSON.stringify({
|
||||
oauthAccount: { accountUuid: 'account-1', emailAddress: 'user@example.com' }
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
writeFileSync(
|
||||
join(managedAuthPath, '.claude.json'),
|
||||
JSON.stringify({
|
||||
oauthAccount: { accountUuid: 'account-2', emailAddress: 'other@example.com' }
|
||||
}),
|
||||
'utf-8'
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
setScopedKeychainCredentialsForManagedPath(managedAuthPath, foreignCredentials)
|
||||
await service.prepareForClaudeLaunch()
|
||||
|
||||
expect(testState.managedKeychainCredentials.get('account-1')).toBe(managedCredentials)
|
||||
})
|
||||
|
||||
it('retains a visible degraded provenance after scoped Keychain bridge failure', async () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
return
|
||||
}
|
||||
const credentials = createClaudeCredentialsJson('user@example.com', 'managed')
|
||||
const managedAuthPath = createManagedClaudeAuth(testState.userDataDir, 'account-1', credentials)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
claudeManagedAccounts: [
|
||||
createClaudeAccount('account-1', managedAuthPath, { managedAuthRuntime: 'host' })
|
||||
],
|
||||
activeClaudeManagedAccountId: 'account-1'
|
||||
})
|
||||
)
|
||||
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
|
||||
const service = new ClaudeRuntimeAuthService(store as never)
|
||||
testState.throwScopedKeychainWrite = true
|
||||
|
||||
const launchPreparation = await service.prepareForClaudeLaunch()
|
||||
expect(launchPreparation.provenance).toBe('system:managed-keychain-unavailable')
|
||||
|
||||
testState.throwScopedKeychainWrite = false
|
||||
await expect(service.prepareForRateLimitFetch()).resolves.toMatchObject({
|
||||
provenance: 'system:managed-keychain-unavailable',
|
||||
stripAuthEnv: false
|
||||
})
|
||||
|
||||
await service.prepareForClaudeLaunch()
|
||||
await expect(service.prepareForRateLimitFetch()).resolves.toMatchObject({
|
||||
provenance: 'managed:account-1',
|
||||
stripAuthEnv: true
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,13 @@
|
||||
import { vi } from 'vitest'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { getDefaultSettings } from '../../shared/constants'
|
||||
@@ -207,6 +215,15 @@ export function createManagedClaudeAuth(
|
||||
return managedAuthPath
|
||||
}
|
||||
|
||||
// Why: the scoped Keychain mock keys managed config dirs by path, so setting the shared field
|
||||
// leaves the bridge reading whatever the previous launch wrote.
|
||||
export function setScopedKeychainCredentialsForManagedPath(
|
||||
managedAuthPath: string,
|
||||
credentialsJson: string
|
||||
): void {
|
||||
testState.scopedKeychainCredentialsByConfigDir.set(realpathSync(managedAuthPath), credentialsJson)
|
||||
}
|
||||
|
||||
export function createClaudeAccount(
|
||||
id: string,
|
||||
managedAuthPath: string,
|
||||
|
||||
Reference in New Issue
Block a user