import AppKitimport CodexBarCoreimport Foundation extension SettingsStore { func tokenAccountsData(for provider: UsageProvider) -> ProviderTokenAccountData? { guard TokenAccountSupportCatalog.support(for: provider) != nil else { return nil } return self.configSnapshot.providerConfig(for: provider)?.tokenAccounts } func tokenAccounts(for provider: UsageProvider) -> [ProviderTokenAccount] { self.tokenAccountsData(for: provider)?.accounts ?? [] } func selectedTokenAccount(for provider: UsageProvider) -> ProviderTokenAccount? { guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return nil } let index = data.clampedActiveIndex() return data.accounts[index] } func setActiveTokenAccountIndex(_ index: Int, for provider: UsageProvider) { guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return } let clamped = min(max(index, 0), data.accounts.count - 1) let updated = ProviderTokenAccountData( version: data.version, accounts: data.accounts, activeIndex: clamped) self.updateProviderConfig(provider: provider) { entry in entry.tokenAccounts = updated } CodexBarLog.logger(LogCategories.tokenAccounts).info( "Active token account updated", metadata: [ "provider": provider.rawValue, "index": "\(clamped)", ]) } func addTokenAccount( provider: UsageProvider, label: String, token: String, externalIdentifier: String? = nil, organizationID: String? = nil) { guard TokenAccountSupportCatalog.support(for: provider) != nil else { return } let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedToken.isEmpty else { return } let trimmedLabel = label.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedIdentifier = externalIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) let normalisedIdentifier = (trimmedIdentifier?.isEmpty ?? true) ? nil : trimmedIdentifier let trimmedOrganizationID = organizationID?.trimmingCharacters(in: .whitespacesAndNewlines) let normalisedOrganizationID = (trimmedOrganizationID?.isEmpty ?? true) ? nil : trimmedOrganizationID let existing = self.tokenAccountsData(for: provider) let accounts = existing?.accounts ?? [] let fallbackLabel = trimmedLabel.isEmpty ? "Account \(accounts.count + 1)" : trimmedLabel let account = ProviderTokenAccount( id: UUID(), label: fallbackLabel, token: trimmedToken, addedAt: Date().timeIntervalSince1970, lastUsed: nil, externalIdentifier: normalisedIdentifier, organizationID: normalisedOrganizationID) let updated = ProviderTokenAccountData( version: existing?.version ?? 1, accounts: accounts + [account], activeIndex: accounts.count) self.updateProviderConfig(provider: provider) { entry in entry.tokenAccounts = updated if provider == .copilot { entry.apiKey = nil } } self.applyTokenAccountCookieSourceIfNeeded(provider: provider) CodexBarLog.logger(LogCategories.tokenAccounts).info( "Token account added", metadata: [ "provider": provider.rawValue, "count": "\(updated.accounts.count)", ]) } func updateTokenAccount( provider: UsageProvider, accountID: UUID, label: String? = nil, token: String? = nil, externalIdentifier: String?? = nil, organizationID: String?? = nil) { guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return } guard let index = data.accounts.firstIndex(where: { $0.id == accountID }) else { return } let trimmedLabel = label?.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) if let trimmedToken, trimmedToken.isEmpty { return } let existing = data.accounts[index] let resolvedIdentifier: String? if let externalIdentifier { let trimmed = externalIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) resolvedIdentifier = (trimmed?.isEmpty ?? true) ? nil : trimmed } else { resolvedIdentifier = existing.externalIdentifier } let resolvedOrganizationID: String? if let organizationID { let trimmed = organizationID?.trimmingCharacters(in: .whitespacesAndNewlines) resolvedOrganizationID = (trimmed?.isEmpty ?? true) ? nil : trimmed } else { resolvedOrganizationID = existing.organizationID } let updatedAccount = ProviderTokenAccount( id: existing.id, label: (trimmedLabel?.isEmpty == false) ? trimmedLabel! : existing.label, token: trimmedToken ?? existing.token, addedAt: existing.addedAt, lastUsed: existing.lastUsed, externalIdentifier: resolvedIdentifier, organizationID: resolvedOrganizationID) var accounts = data.accounts accounts[index] = updatedAccount let updated = ProviderTokenAccountData( version: data.version, accounts: accounts, activeIndex: data.clampedActiveIndex()) self.updateProviderConfig(provider: provider) { entry in entry.tokenAccounts = updated if provider == .copilot { entry.apiKey = nil } } self.applyTokenAccountCookieSourceIfNeeded(provider: provider) CodexBarLog.logger(LogCategories.tokenAccounts).info( "Token account updated", metadata: [ "provider": provider.rawValue, "count": "\(updated.accounts.count)", ]) } func removeTokenAccount(provider: UsageProvider, accountID: UUID) { guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return } let activeAccountID = data.accounts[data.clampedActiveIndex()].id guard let removedIndex = data.accounts.firstIndex(where: { $0.id == accountID }) else { return } let removedAccount = data.accounts[removedIndex] let filtered = data.accounts.filter { $0.id != accountID } self.updateProviderConfig(provider: provider) { entry in if filtered.isEmpty { entry.tokenAccounts = nil } else { let nextActiveIndex = if activeAccountID != accountID, let preservedIndex = filtered.firstIndex(where: { $0.id == activeAccountID }) { preservedIndex } else { min(removedIndex, filtered.count - 1) } entry.tokenAccounts = ProviderTokenAccountData( version: data.version, accounts: filtered, activeIndex: nextActiveIndex) } if provider == .copilot { entry.apiKey = nil } } self.applyTokenAccountRemovalSideEffectsIfNeeded( provider: provider, removedAccount: removedAccount, remainingAccounts: filtered) CodexBarLog.logger(LogCategories.tokenAccounts).info( "Token account removed", metadata: [ "provider": provider.rawValue, "count": "\(filtered.count)", ]) } func ensureTokenAccountsLoaded() { if self.tokenAccountsLoaded { return } self.tokenAccountsLoaded = true } func reloadTokenAccounts() { let log = CodexBarLog.logger(LogCategories.tokenAccounts) let accounts: [UsageProvider: ProviderTokenAccountData] do { guard let loaded = try self.configStore.load() else { return } accounts = Dictionary(uniqueKeysWithValues: loaded.providers.compactMap { entry in guard let data = entry.tokenAccounts else { return nil } return (entry.id, data) }) } catch { log.error("Failed to reload token accounts: \(error)") return } self.tokenAccountsLoaded = true self.updateProviderTokenAccounts(accounts) } func openTokenAccountsFile() { do { try self.configStore.save(self.config) } catch { CodexBarLog.logger(LogCategories.tokenAccounts).error("Failed to persist config: \(error)") return } NSWorkspace.shared.open(self.configStore.fileURL) } private func applyTokenAccountCookieSourceIfNeeded(provider: UsageProvider) { guard let support = TokenAccountSupportCatalog.support(for: provider), support.requiresManualCookieSource else { return } ProviderCatalog.implementation(for: provider)?.applyTokenAccountCookieSource(settings: self) } private func applyTokenAccountRemovalSideEffectsIfNeeded( provider: UsageProvider, removedAccount: ProviderTokenAccount, remainingAccounts: [ProviderTokenAccount]) { guard provider == .antigravity else { return } guard let removedCredentials = AntigravityOAuthCredentialsStore.credentials( fromTokenAccountValue: removedAccount.token) else { return } let hasMatchingRemainingAccount = remainingAccounts.contains { account in guard let credentials = AntigravityOAuthCredentialsStore.credentials(fromTokenAccountValue: account.token) else { return false } return Self.antigravityCredentialsMatchAccount(credentials, removedCredentials) } guard !hasMatchingRemainingAccount else { return } Self.clearMatchingAntigravitySharedCredentials( store: self.antigravityOAuthCredentialsStore, removedCredentials: removedCredentials) } private nonisolated static func clearMatchingAntigravitySharedCredentials( store: AntigravityOAuthCredentialsStore, removedCredentials: AntigravityOAuthCredentials) { do { try store.deleteIfPresent { sharedCredentials in self.antigravitySharedCredentialsMatchRemovedAccount( sharedCredentials, removedCredentials) } } catch { CodexBarLog.logger(LogCategories.tokenAccounts).warning( "Failed to clear Antigravity OAuth cache after account removal", metadata: ["error": error.localizedDescription]) } } private nonisolated static func antigravitySharedCredentialsMatchRemovedAccount( _ shared: AntigravityOAuthCredentials, _ removed: AntigravityOAuthCredentials) -> Bool { if let sharedRefreshToken = self.normalizedAntigravityCredentialToken(shared.refreshToken), let removedRefreshToken = self.normalizedAntigravityCredentialToken(removed.refreshToken) { return sharedRefreshToken == removedRefreshToken } if let sharedAccessToken = self.normalizedAntigravityCredentialToken(shared.accessToken), let removedAccessToken = self.normalizedAntigravityCredentialToken(removed.accessToken) { return sharedAccessToken == removedAccessToken } guard self.normalizedAntigravityCredentialToken(shared.refreshToken) == nil, self.normalizedAntigravityCredentialToken(removed.refreshToken) == nil, self.normalizedAntigravityCredentialToken(shared.accessToken) == nil, self.normalizedAntigravityCredentialToken(removed.accessToken) == nil else { return false } return self.normalizedAntigravityAccountEmail(shared.resolvedAccountEmail) == self.normalizedAntigravityAccountEmail(removed.resolvedAccountEmail) } private nonisolated static func antigravityCredentialsMatchAccount( _ lhs: AntigravityOAuthCredentials, _ rhs: AntigravityOAuthCredentials) -> Bool { if let lhsEmail = self.normalizedAntigravityAccountEmail(lhs.resolvedAccountEmail), let rhsEmail = self.normalizedAntigravityAccountEmail(rhs.resolvedAccountEmail) { return lhsEmail == rhsEmail } if let lhsRefreshToken = self.normalizedAntigravityCredentialToken(lhs.refreshToken), let rhsRefreshToken = self.normalizedAntigravityCredentialToken(rhs.refreshToken) { return lhsRefreshToken == rhsRefreshToken } if let lhsAccessToken = self.normalizedAntigravityCredentialToken(lhs.accessToken), let rhsAccessToken = self.normalizedAntigravityCredentialToken(rhs.accessToken) { return lhsAccessToken == rhsAccessToken } return false } private nonisolated static func normalizedAntigravityAccountEmail(_ email: String?) -> String? { guard let value = email?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), !value.isEmpty else { return nil } return value } private nonisolated static func normalizedAntigravityCredentialToken(_ token: String?) -> String? { guard let value = token?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil } return value }}
import AppKit
import CodexBarCore
import Foundation
extension SettingsStore {
func tokenAccountsData(for provider: UsageProvider) -> ProviderTokenAccountData? {
guard TokenAccountSupportCatalog.support(for: provider) != nil else { return nil }
return self.configSnapshot.providerConfig(for: provider)?.tokenAccounts
}
func tokenAccounts(for provider: UsageProvider) -> [ProviderTokenAccount] {
self.tokenAccountsData(for: provider)?.accounts ?? []
}
func selectedTokenAccount(for provider: UsageProvider) -> ProviderTokenAccount? {
guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return nil }
let index = data.clampedActiveIndex()
return data.accounts[index]
}
func setActiveTokenAccountIndex(_ index: Int, for provider: UsageProvider) {
guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return }
let clamped = min(max(index, 0), data.accounts.count - 1)
let updated = ProviderTokenAccountData(
version: data.version,
accounts: data.accounts,
activeIndex: clamped)
self.updateProviderConfig(provider: provider) { entry in
entry.tokenAccounts = updated
}
CodexBarLog.logger(LogCategories.tokenAccounts).info(
"Active token account updated",
metadata: [
"provider": provider.rawValue,
"index": "\(clamped)",
])
}
func addTokenAccount(
provider: UsageProvider,
label: String,
token: String,
externalIdentifier: String? = nil,
organizationID: String? = nil)
{
guard TokenAccountSupportCatalog.support(for: provider) != nil else { return }
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedToken.isEmpty else { return }
let trimmedLabel = label.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedIdentifier = externalIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
let normalisedIdentifier = (trimmedIdentifier?.isEmpty ?? true) ? nil : trimmedIdentifier
let trimmedOrganizationID = organizationID?.trimmingCharacters(in: .whitespacesAndNewlines)
let normalisedOrganizationID = (trimmedOrganizationID?.isEmpty ?? true) ? nil : trimmedOrganizationID
let existing = self.tokenAccountsData(for: provider)
let accounts = existing?.accounts ?? []
let fallbackLabel = trimmedLabel.isEmpty ? "Account \(accounts.count + 1)" : trimmedLabel
let account = ProviderTokenAccount(
id: UUID(),
label: fallbackLabel,
token: trimmedToken,
addedAt: Date().timeIntervalSince1970,
lastUsed: nil,
externalIdentifier: normalisedIdentifier,
organizationID: normalisedOrganizationID)
let updated = ProviderTokenAccountData(
version: existing?.version ?? 1,
accounts: accounts + [account],
activeIndex: accounts.count)
self.updateProviderConfig(provider: provider) { entry in
entry.tokenAccounts = updated
if provider == .copilot {
entry.apiKey = nil
}
}
self.applyTokenAccountCookieSourceIfNeeded(provider: provider)
CodexBarLog.logger(LogCategories.tokenAccounts).info(
"Token account added",
metadata: [
"provider": provider.rawValue,
"count": "\(updated.accounts.count)",
])
}
func updateTokenAccount(
provider: UsageProvider,
accountID: UUID,
label: String? = nil,
token: String? = nil,
externalIdentifier: String?? = nil,
organizationID: String?? = nil)
{
guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return }
guard let index = data.accounts.firstIndex(where: { $0.id == accountID }) else { return }
let trimmedLabel = label?.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if let trimmedToken, trimmedToken.isEmpty { return }
let existing = data.accounts[index]
let resolvedIdentifier: String?
if let externalIdentifier {
let trimmed = externalIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines)
resolvedIdentifier = (trimmed?.isEmpty ?? true) ? nil : trimmed
} else {
resolvedIdentifier = existing.externalIdentifier
}
let resolvedOrganizationID: String?
if let organizationID {
let trimmed = organizationID?.trimmingCharacters(in: .whitespacesAndNewlines)
resolvedOrganizationID = (trimmed?.isEmpty ?? true) ? nil : trimmed
} else {
resolvedOrganizationID = existing.organizationID
}
let updatedAccount = ProviderTokenAccount(
id: existing.id,
label: (trimmedLabel?.isEmpty == false) ? trimmedLabel! : existing.label,
token: trimmedToken ?? existing.token,
addedAt: existing.addedAt,
lastUsed: existing.lastUsed,
externalIdentifier: resolvedIdentifier,
organizationID: resolvedOrganizationID)
var accounts = data.accounts
accounts[index] = updatedAccount
let updated = ProviderTokenAccountData(
version: data.version,
accounts: accounts,
activeIndex: data.clampedActiveIndex())
self.updateProviderConfig(provider: provider) { entry in
entry.tokenAccounts = updated
if provider == .copilot {
entry.apiKey = nil
}
}
self.applyTokenAccountCookieSourceIfNeeded(provider: provider)
CodexBarLog.logger(LogCategories.tokenAccounts).info(
"Token account updated",
metadata: [
"provider": provider.rawValue,
"count": "\(updated.accounts.count)",
])
}
func removeTokenAccount(provider: UsageProvider, accountID: UUID) {
guard let data = self.tokenAccountsData(for: provider), !data.accounts.isEmpty else { return }
let activeAccountID = data.accounts[data.clampedActiveIndex()].id
guard let removedIndex = data.accounts.firstIndex(where: { $0.id == accountID }) else { return }
let removedAccount = data.accounts[removedIndex]
let filtered = data.accounts.filter { $0.id != accountID }
self.updateProviderConfig(provider: provider) { entry in
if filtered.isEmpty {
entry.tokenAccounts = nil
} else {
let nextActiveIndex = if activeAccountID != accountID,
let preservedIndex = filtered.firstIndex(where: { $0.id == activeAccountID })
{
preservedIndex
} else {
min(removedIndex, filtered.count - 1)
}
entry.tokenAccounts = ProviderTokenAccountData(
version: data.version,
accounts: filtered,
activeIndex: nextActiveIndex)
}
if provider == .copilot {
entry.apiKey = nil
}
}
self.applyTokenAccountRemovalSideEffectsIfNeeded(
provider: provider,
removedAccount: removedAccount,
remainingAccounts: filtered)
CodexBarLog.logger(LogCategories.tokenAccounts).info(
"Token account removed",
metadata: [
"provider": provider.rawValue,
"count": "\(filtered.count)",
])
}
func ensureTokenAccountsLoaded() {
if self.tokenAccountsLoaded { return }
self.tokenAccountsLoaded = true
}
func reloadTokenAccounts() {
let log = CodexBarLog.logger(LogCategories.tokenAccounts)
let accounts: [UsageProvider: ProviderTokenAccountData]
do {
guard let loaded = try self.configStore.load() else { return }
accounts = Dictionary(uniqueKeysWithValues: loaded.providers.compactMap { entry in
guard let data = entry.tokenAccounts else { return nil }
return (entry.id, data)
})
} catch {
log.error("Failed to reload token accounts: \(error)")
return
}
self.tokenAccountsLoaded = true
self.updateProviderTokenAccounts(accounts)
}
func openTokenAccountsFile() {
do {
try self.configStore.save(self.config)
} catch {
CodexBarLog.logger(LogCategories.tokenAccounts).error("Failed to persist config: \(error)")
return
}
NSWorkspace.shared.open(self.configStore.fileURL)
}
private func applyTokenAccountCookieSourceIfNeeded(provider: UsageProvider) {
guard let support = TokenAccountSupportCatalog.support(for: provider),
support.requiresManualCookieSource
else { return }
ProviderCatalog.implementation(for: provider)?.applyTokenAccountCookieSource(settings: self)
}
private func applyTokenAccountRemovalSideEffectsIfNeeded(
provider: UsageProvider,
removedAccount: ProviderTokenAccount,
remainingAccounts: [ProviderTokenAccount])
{
guard provider == .antigravity else { return }
guard let removedCredentials = AntigravityOAuthCredentialsStore.credentials(
fromTokenAccountValue: removedAccount.token)
else {
return
}
let hasMatchingRemainingAccount = remainingAccounts.contains { account in
guard let credentials = AntigravityOAuthCredentialsStore.credentials(fromTokenAccountValue: account.token)
else {
return false
}
return Self.antigravityCredentialsMatchAccount(credentials, removedCredentials)
}
guard !hasMatchingRemainingAccount else { return }
Self.clearMatchingAntigravitySharedCredentials(
store: self.antigravityOAuthCredentialsStore,
removedCredentials: removedCredentials)
}
private nonisolated static func clearMatchingAntigravitySharedCredentials(
store: AntigravityOAuthCredentialsStore,
removedCredentials: AntigravityOAuthCredentials)
{
do {
try store.deleteIfPresent { sharedCredentials in
self.antigravitySharedCredentialsMatchRemovedAccount(
sharedCredentials,
removedCredentials)
}
} catch {
CodexBarLog.logger(LogCategories.tokenAccounts).warning(
"Failed to clear Antigravity OAuth cache after account removal",
metadata: ["error": error.localizedDescription])
}
}
private nonisolated static func antigravitySharedCredentialsMatchRemovedAccount(
_ shared: AntigravityOAuthCredentials,
_ removed: AntigravityOAuthCredentials) -> Bool
{
if let sharedRefreshToken = self.normalizedAntigravityCredentialToken(shared.refreshToken),
let removedRefreshToken = self.normalizedAntigravityCredentialToken(removed.refreshToken)
{
return sharedRefreshToken == removedRefreshToken
}
if let sharedAccessToken = self.normalizedAntigravityCredentialToken(shared.accessToken),
let removedAccessToken = self.normalizedAntigravityCredentialToken(removed.accessToken)
{
return sharedAccessToken == removedAccessToken
}
guard self.normalizedAntigravityCredentialToken(shared.refreshToken) == nil,
self.normalizedAntigravityCredentialToken(removed.refreshToken) == nil,
self.normalizedAntigravityCredentialToken(shared.accessToken) == nil,
self.normalizedAntigravityCredentialToken(removed.accessToken) == nil
else {
return false
}
return self.normalizedAntigravityAccountEmail(shared.resolvedAccountEmail)
== self.normalizedAntigravityAccountEmail(removed.resolvedAccountEmail)
}
private nonisolated static func antigravityCredentialsMatchAccount(
_ lhs: AntigravityOAuthCredentials,
_ rhs: AntigravityOAuthCredentials) -> Bool
{
if let lhsEmail = self.normalizedAntigravityAccountEmail(lhs.resolvedAccountEmail),
let rhsEmail = self.normalizedAntigravityAccountEmail(rhs.resolvedAccountEmail)
{
return lhsEmail == rhsEmail
}
if let lhsRefreshToken = self.normalizedAntigravityCredentialToken(lhs.refreshToken),
let rhsRefreshToken = self.normalizedAntigravityCredentialToken(rhs.refreshToken)
{
return lhsRefreshToken == rhsRefreshToken
}
if let lhsAccessToken = self.normalizedAntigravityCredentialToken(lhs.accessToken),
let rhsAccessToken = self.normalizedAntigravityCredentialToken(rhs.accessToken)
{
return lhsAccessToken == rhsAccessToken
}
return false
}
private nonisolated static func normalizedAntigravityAccountEmail(_ email: String?) -> String? {
guard let value = email?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
!value.isEmpty
else {
return nil
}
return value
}
private nonisolated static func normalizedAntigravityCredentialToken(_ token: String?) -> String? {
guard let value = token?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
else {
return nil
}
return value
}
}