327 lines · 13.6 KB · Swift
RawDownload
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    }}