Files
steipete--codexbar/Sources/CodexBar/Providers/Codex/CodexSettingsStore.swift
T
2026-07-13 12:22:33 +08:00

742 lines
28 KiB
Swift

import CodexBarCore
import Foundation
extension SettingsStore {
private var codexPersistedActiveSource: CodexActiveSource {
self.providerConfig(for: .codex)?.codexActiveSource ?? .liveSystem
}
private enum ManagedCodexAccountStoreState {
case none
case selected(ManagedCodexAccount)
case unreadable
}
private static func failClosedManagedCodexHomePath(fileManager: FileManager = .default) -> String {
ManagedCodexHomeFactory.defaultRootURL(fileManager: fileManager)
.appendingPathComponent("managed-store-unreadable", isDirectory: true)
.path
}
private static func normalizedCodexProfileHomePaths(_ paths: [String]?) -> [String] {
var seen: Set<String> = []
var result: [String] = []
for path in (paths ?? []).compactMap({ CodexHomeScope.normalizedHomePath($0) }) {
guard seen.insert(path).inserted else { continue }
result.append(path)
}
return result
}
private func loadManagedCodexAccounts() throws -> ManagedCodexAccountSet {
#if DEBUG
if CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self) {
throw CodexManagedRemoteHomeTestingOverrideError.unreadableManagedStore
}
if let override = CodexManagedRemoteHomeTestingOverride.account(for: self) {
return ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [override])
}
let store = if let storeURL = CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self) {
FileManagedCodexAccountStore(fileURL: storeURL)
} else {
FileManagedCodexAccountStore()
}
#else
let store = FileManagedCodexAccountStore()
#endif
return try store.loadAccounts()
}
private func managedCodexAccountStoreState(
activeSource: CodexActiveSource? = nil) -> ManagedCodexAccountStoreState
{
let source = activeSource ?? self.codexResolvedActiveSource
guard case let .managedAccount(id) = source else {
return .none
}
do {
let accounts = try self.loadManagedCodexAccounts()
guard let account = accounts.account(id: id)
else {
return .none
}
return .selected(account)
} catch {
return .unreadable
}
}
var activeManagedCodexAccount: ManagedCodexAccount? {
guard case let .selected(account) = self.managedCodexAccountStoreState() else {
return nil
}
return account
}
var activeManagedCodexRemoteHomePath: String? {
self.managedCodexRemoteHomePath(forActiveSource: self.codexResolvedActiveSource)
}
func liveSystemCodexHomePath(forActiveSource source: CodexActiveSource) -> String? {
guard source == .liveSystem else {
return nil
}
let path = self.codexAccountReconciliationSnapshot(activeSourceOverride: source)
.liveSystemAccount?.codexHomePath.trimmingCharacters(in: .whitespacesAndNewlines)
guard let path, !path.isEmpty else {
return nil
}
return path
}
var codexProfileHomePaths: [String] {
Self.normalizedCodexProfileHomePaths(
self.configSnapshot.providerConfig(for: .codex)?.codexProfileHomePaths)
}
func profileCodexHomePath(forActiveSource source: CodexActiveSource) -> String? {
guard case let .profileHome(path) = source else {
return nil
}
guard let normalizedPath = CodexHomeScope.normalizedHomePath(path),
self.codexProfileHomePaths.contains(normalizedPath)
else {
return nil
}
return normalizedPath
}
func managedCodexRemoteHomePath(forActiveSource source: CodexActiveSource) -> String? {
guard case let .managedAccount(id) = source else {
return nil
}
#if DEBUG
if let override = CodexManagedRemoteHomeTestingOverride.homePath(for: self) {
return override
}
#endif
do {
let accounts = try self.loadManagedCodexAccounts()
// A selected managed source must never fall back to ambient ~/.codex.
return accounts.account(id: id)?.managedHomePath ?? Self.failClosedManagedCodexHomePath()
} catch {
return Self.failClosedManagedCodexHomePath()
}
}
var activeManagedCodexCookieCacheScope: CookieHeaderCache.Scope? {
switch self.managedCodexAccountStoreState() {
case let .selected(account):
.managedAccount(account.id)
case .unreadable:
.managedStoreUnreadable
case .none:
nil
}
}
var hasUnreadableManagedCodexAccountStore: Bool {
self.codexAccountReconciliationSnapshot.hasUnreadableAddedAccountStore
}
var codexUsageDataSource: CodexUsageDataSource {
get {
let source = self.configSnapshot.providerConfig(for: .codex)?.source
return Self.codexUsageDataSource(from: source)
}
set {
let source: ProviderSourceMode? = switch newValue {
case .auto: .auto
case .oauth: .oauth
case .cli: .cli
}
self.updateProviderConfig(provider: .codex) { entry in
entry.source = source
}
self.logProviderModeChange(provider: .codex, field: "usageSource", value: newValue.rawValue)
}
}
var codexActiveSource: CodexActiveSource {
get {
self.codexPersistedActiveSource
}
set {
self.invalidateCodexAccountReconciliationSnapshotCache()
self.cachedCodexAccountMenuProjection = nil
self.updateProviderConfig(provider: .codex) { entry in
entry.codexActiveSource = newValue
}
}
}
var codexResolvedActiveSource: CodexActiveSource {
self.codexResolvedActiveSourceState.resolvedSource
}
var codexResolvedActiveSourceState: CodexResolvedActiveSource {
CodexActiveSourceResolver.resolve(from: self.codexAccountReconciliationSnapshot)
}
@discardableResult
func persistResolvedCodexActiveSourceCorrectionIfNeeded() -> Bool {
let resolution = self.codexResolvedActiveSourceState
guard resolution.requiresPersistenceCorrection else { return false }
self.codexActiveSource = resolution.resolvedSource
return true
}
@discardableResult
func refreshCodexAccountReconciliationAfterManagedAccountsDidChange() -> Bool {
self.invalidateCodexAccountReconciliationSnapshotCache()
self.cachedCodexAccountMenuProjection = nil
return self.persistResolvedCodexActiveSourceCorrectionIfNeeded()
}
var codexCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .codex)?.sanitizedCookieHeader ?? "" }
set {
// This is intentionally provider-scoped today. A per-managed-account manual cookie override would need
// its own storage and UI semantics so editing one account's header does not silently rewrite another's.
self.updateProviderConfig(provider: .codex) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .codex, field: "cookieHeader", value: newValue)
}
}
var codexCookieSource: ProviderCookieSource {
get {
let resolved = self.resolvedCookieSource(provider: .codex, fallback: .auto)
return self.openAIWebAccessEnabled ? resolved : .off
}
set {
self.updateProviderConfig(provider: .codex) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .codex, field: "cookieSource", value: newValue.rawValue)
self.openAIWebAccessEnabled = newValue.isEnabled
}
}
func ensureCodexCookieLoaded() {}
}
extension SettingsStore {
private static var codexAccountReconciliationSnapshotCacheInterval: TimeInterval {
#if DEBUG
if let codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting {
return codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting
}
#endif
return self.isRunningTests ? 0 : self.productionCodexAccountReconciliationSnapshotCacheInterval
}
func invalidateCodexAccountReconciliationSnapshotCache() {
self.cachedCodexAccountReconciliationSnapshot = nil
self.codexAccountReconciliationGeneration &+= 1
}
var codexAccountReconciliationSnapshot: CodexAccountReconciliationSnapshot {
self.codexAccountReconciliationSnapshot(activeSourceOverride: nil)
}
func codexAccountReconciliationSnapshot(
activeSourceOverride: CodexActiveSource?) -> CodexAccountReconciliationSnapshot
{
let activeSource = activeSourceOverride ?? self.codexPersistedActiveSource
let cacheInterval = Self.codexAccountReconciliationSnapshotCacheInterval
let now = Date()
if cacheInterval > 0,
let cached = self.cachedCodexAccountReconciliationSnapshot,
cached.activeSource == activeSource,
now.timeIntervalSince(cached.loadedAt) < cacheInterval
{
return cached.snapshot
}
let snapshot = self.codexAccountSnapshotLoader(activeSource: activeSource)()
let loadedAt = Date()
if cacheInterval > 0 {
self.cachedCodexAccountReconciliationSnapshot = CachedCodexAccountReconciliationSnapshot(
activeSource: activeSource,
loadedAt: loadedAt,
snapshot: snapshot)
}
if activeSource == self.codexPersistedActiveSource {
self.cachedCodexAccountMenuProjection = CachedCodexAccountMenuProjection(
activeSource: activeSource,
loadedAt: loadedAt,
projection: CodexVisibleAccountProjection.make(from: snapshot))
}
return snapshot
}
/// Menu rendering must stay side-effect free: no `auth.json` reads, JWT parsing, or fingerprint hashing.
var codexVisibleAccountProjectionForMenuDisplay: CodexVisibleAccountProjection? {
let activeSource = self.codexPersistedActiveSource
guard let cached = self.cachedCodexAccountMenuProjection,
cached.activeSource == activeSource
else {
return nil
}
return cached.projection
}
var codexAccountMenuProjectionNeedsRevalidation: Bool {
let activeSource = self.codexPersistedActiveSource
guard let cached = self.cachedCodexAccountMenuProjection,
cached.activeSource == activeSource
else {
return true
}
return Date().timeIntervalSince(cached.loadedAt) >= Self.codexAccountReconciliationSnapshotCacheInterval
}
func revalidateCodexAccountMenuProjection() async -> CodexAccountMenuProjectionRevalidationResult {
guard self.codexAccountMenuProjectionNeedsRevalidation else { return .skipped }
let activeSource = self.codexPersistedActiveSource
let generation = self.codexAccountReconciliationGeneration
let loader = self.codexAccountSnapshotLoader(activeSource: activeSource)
let snapshot = await Self.loadCodexAccountSnapshot(loader)
guard generation == self.codexAccountReconciliationGeneration,
activeSource == self.codexPersistedActiveSource
else {
return .discarded
}
let now = Date()
let projection = CodexVisibleAccountProjection.make(from: snapshot)
let previousProjection = self.cachedCodexAccountMenuProjection.flatMap { cached in
cached.activeSource == activeSource ? cached.projection : nil
}
self.cachedCodexAccountMenuProjection = CachedCodexAccountMenuProjection(
activeSource: activeSource,
loadedAt: now,
projection: projection)
if Self.codexAccountReconciliationSnapshotCacheInterval > 0 {
self.cachedCodexAccountReconciliationSnapshot = CachedCodexAccountReconciliationSnapshot(
activeSource: activeSource,
loadedAt: now,
snapshot: snapshot)
}
return previousProjection == projection ? .unchanged : .updated
}
@concurrent
private nonisolated static func loadCodexAccountSnapshot(
_ loader: @escaping @Sendable () -> CodexAccountReconciliationSnapshot)
async -> CodexAccountReconciliationSnapshot
{
loader()
}
var codexVisibleAccountProjection: CodexVisibleAccountProjection {
CodexVisibleAccountProjection.make(from: self.codexAccountReconciliationSnapshot)
}
var codexVisibleAccounts: [CodexVisibleAccount] {
self.codexVisibleAccountProjection.visibleAccounts
}
@discardableResult
func selectCodexVisibleAccount(id: String) -> Bool {
guard let source = self.codexSource(forVisibleAccountID: id) else { return false }
self.invalidateCodexAccountReconciliationSnapshotCache()
self.codexActiveSource = source
return true
}
func selectDisplayedCodexVisibleAccount(_ account: CodexVisibleAccount) {
// The row already carries the exact source it represented. Re-resolving its ID would synchronously
// reload auth state from the menu click callback and can also fail after a stale snapshot is rendered.
self.codexActiveSource = account.selectionSource
}
func selectAuthenticatedManagedCodexAccount(_ account: ManagedCodexAccount) {
if let visibleAccountID = self.codexVisibleAccountProjection.visibleAccounts
.first(where: { $0.storedAccountID == account.id })?
.id,
self.selectCodexVisibleAccount(id: visibleAccountID)
{
return
}
self.invalidateCodexAccountReconciliationSnapshotCache()
self.codexActiveSource = .managedAccount(id: account.id)
_ = self.persistResolvedCodexActiveSourceCorrectionIfNeeded()
}
func codexSource(forVisibleAccountID id: String) -> CodexActiveSource? {
self.codexVisibleAccountProjection.source(forVisibleAccountID: id)
}
private func codexAccountSnapshotLoader(
activeSource: CodexActiveSource) -> @Sendable () -> CodexAccountReconciliationSnapshot
{
#if DEBUG
if let loader = self._test_codexAccountSnapshotLoader {
return { loader(activeSource) }
}
#endif
let reconciler = self.codexAccountReconciler(activeSource: activeSource)
return { reconciler.loadSnapshot() }
}
private func codexAccountReconciler(activeSource: CodexActiveSource) -> DefaultCodexAccountReconciler {
let baseEnvironment = self.codexReconciliationEnvironment()
#if DEBUG
let liveSystemAccountOverride = CodexManagedRemoteHomeTestingOverride.liveSystemAccount(for: self)
let reconciliationEnvironmentOverride = CodexManagedRemoteHomeTestingOverride
.reconciliationEnvironment(for: self)
let managedAccountOverride = CodexManagedRemoteHomeTestingOverride.account(for: self)
let managedStoreURLOverride = CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self)
let unreadableStoreOverride = CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self)
guard CodexManagedRemoteHomeTestingOverride.hasAnyOverride(for: self) else {
return DefaultCodexAccountReconciler(
activeSource: activeSource,
baseEnvironment: baseEnvironment,
profileHomePaths: self.codexProfileHomePaths,
managedEnvironmentBuilder: { environment, account in
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
})
}
let storeLoader: @Sendable () throws -> ManagedCodexAccountSet
if unreadableStoreOverride {
storeLoader = { throw CodexManagedRemoteHomeTestingOverrideError.unreadableManagedStore }
} else if let managedAccountOverride {
let accounts = ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [managedAccountOverride])
storeLoader = { accounts }
} else if let managedStoreURLOverride {
let store = FileManagedCodexAccountStore(fileURL: managedStoreURLOverride)
storeLoader = { try store.loadAccounts() }
} else {
let accounts = ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [])
storeLoader = { accounts }
}
return DefaultCodexAccountReconciler(
storeLoader: storeLoader,
systemObserver: CodexManagedRemoteHomeTestingSystemObserver(
overrideAccount: liveSystemAccountOverride,
usesInjectedEnvironment: reconciliationEnvironmentOverride != nil),
activeSource: activeSource,
baseEnvironment: baseEnvironment,
profileHomePaths: self.codexProfileHomePaths,
managedEnvironmentBuilder: { environment, account in
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
})
#else
return DefaultCodexAccountReconciler(
activeSource: activeSource,
baseEnvironment: baseEnvironment,
profileHomePaths: self.codexProfileHomePaths,
managedEnvironmentBuilder: { environment, account in
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
})
#endif
}
private func codexReconciliationEnvironment() -> [String: String] {
#if DEBUG
if let override = CodexManagedRemoteHomeTestingOverride.reconciliationEnvironment(for: self) {
return override
}
#endif
return ProcessInfo.processInfo.environment
}
}
#if DEBUG
private enum CodexManagedRemoteHomeTestingOverride {
private struct Override {
var account: ManagedCodexAccount?
var homePath: String?
var unreadableStore: Bool = false
var managedStoreURL: URL?
var liveSystemAccount: ObservedSystemCodexAccount?
var reconciliationEnvironment: [String: String]?
var isEmpty: Bool {
self.account == nil && self.homePath == nil && self.unreadableStore == false && self
.managedStoreURL == nil && self.liveSystemAccount == nil && self
.reconciliationEnvironment == nil
}
}
private final class Entry {
weak var settings: SettingsStore?
var overrideValue: Override
init(settings: SettingsStore, overrideValue: Override) {
self.settings = settings
self.overrideValue = overrideValue
}
}
@MainActor
private static var values: [ObjectIdentifier: Entry] = [:]
@MainActor
private static func entry(for settings: SettingsStore) -> Entry? {
let key = ObjectIdentifier(settings)
guard let entry = self.values[key] else { return nil }
guard let storedSettings = entry.settings, storedSettings === settings else {
self.values.removeValue(forKey: key)
return nil
}
return entry
}
@MainActor
static func account(for settings: SettingsStore) -> ManagedCodexAccount? {
self.entry(for: settings)?.overrideValue.account
}
@MainActor
static func setAccount(_ account: ManagedCodexAccount?, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.account = account
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func homePath(for settings: SettingsStore) -> String? {
self.entry(for: settings)?.overrideValue.homePath
}
@MainActor
static func setHomePath(_ value: String?, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.homePath = value
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func isUnreadable(for settings: SettingsStore) -> Bool {
self.entry(for: settings)?.overrideValue.unreadableStore == true
}
@MainActor
static func setUnreadable(_ value: Bool, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.unreadableStore = value
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func liveSystemAccount(for settings: SettingsStore) -> ObservedSystemCodexAccount? {
self.entry(for: settings)?.overrideValue.liveSystemAccount
}
@MainActor
static func managedStoreURL(for settings: SettingsStore) -> URL? {
self.entry(for: settings)?.overrideValue.managedStoreURL
}
@MainActor
static func setManagedStoreURL(_ value: URL?, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.managedStoreURL = value
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func setLiveSystemAccount(_ account: ObservedSystemCodexAccount?, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.liveSystemAccount = account
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func reconciliationEnvironment(for settings: SettingsStore) -> [String: String]? {
self.entry(for: settings)?.overrideValue.reconciliationEnvironment
}
@MainActor
static func setReconciliationEnvironment(_ environment: [String: String]?, for settings: SettingsStore) {
let key = ObjectIdentifier(settings)
var override = self.entry(for: settings)?.overrideValue ?? Override()
override.reconciliationEnvironment = environment
if override.isEmpty {
self.values.removeValue(forKey: key)
} else {
self.values[key] = Entry(settings: settings, overrideValue: override)
}
}
@MainActor
static func hasAnyOverride(for settings: SettingsStore) -> Bool {
self.entry(for: settings)?.overrideValue.isEmpty == false
}
}
private enum CodexManagedRemoteHomeTestingOverrideError: Error {
case unreadableManagedStore
}
private struct CodexManagedRemoteHomeTestingSystemObserver: CodexSystemAccountObserving {
let overrideAccount: ObservedSystemCodexAccount?
let usesInjectedEnvironment: Bool
func loadSystemAccount(environment: [String: String]) throws -> ObservedSystemCodexAccount? {
if let overrideAccount {
return overrideAccount
}
guard self.usesInjectedEnvironment else {
return nil
}
return try DefaultCodexSystemAccountObserver().loadSystemAccount(environment: environment)
}
}
extension SettingsStore {
private func invalidateCodexAccountReconciliationCachesForTesting() {
self.invalidateCodexAccountReconciliationSnapshotCache()
self.cachedCodexAccountMenuProjection = nil
}
var _test_activeManagedCodexRemoteHomePath: String? {
get { CodexManagedRemoteHomeTestingOverride.homePath(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setHomePath(newValue, for: self)
}
}
var _test_activeManagedCodexAccount: ManagedCodexAccount? {
get { CodexManagedRemoteHomeTestingOverride.account(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setAccount(newValue, for: self)
}
}
var _test_unreadableManagedCodexAccountStore: Bool {
get { CodexManagedRemoteHomeTestingOverride.isUnreadable(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setUnreadable(newValue, for: self)
}
}
var _test_managedCodexAccountStoreURL: URL? {
get { CodexManagedRemoteHomeTestingOverride.managedStoreURL(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setManagedStoreURL(newValue, for: self)
}
}
var _test_liveSystemCodexAccount: ObservedSystemCodexAccount? {
get { CodexManagedRemoteHomeTestingOverride.liveSystemAccount(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setLiveSystemAccount(newValue, for: self)
}
}
var _test_codexReconciliationEnvironment: [String: String]? {
get { CodexManagedRemoteHomeTestingOverride.reconciliationEnvironment(for: self) }
set {
self.invalidateCodexAccountReconciliationCachesForTesting()
CodexManagedRemoteHomeTestingOverride.setReconciliationEnvironment(newValue, for: self)
}
}
}
#endif
extension SettingsStore {
func codexSettingsSnapshot(
tokenOverride: TokenAccountOverride?,
activeSourceOverride: CodexActiveSource? = nil) -> ProviderSettingsSnapshot.CodexProviderSettings
{
let reconciliationSnapshot = self.codexAccountReconciliationSnapshot(
activeSourceOverride: activeSourceOverride)
let resolvedActiveSource = CodexActiveSourceResolver.resolve(from: reconciliationSnapshot)
return CodexProviderSettingsBuilder.make(input: CodexProviderSettingsBuilderInput(
usageDataSource: self.codexUsageDataSource,
cookieSource: self.codexSnapshotCookieSource(tokenOverride: tokenOverride),
manualCookieHeader: self.codexSnapshotCookieHeader(tokenOverride: tokenOverride),
reconciliationSnapshot: reconciliationSnapshot,
resolvedActiveSource: resolvedActiveSource))
}
private static func codexUsageDataSource(from source: ProviderSourceMode?) -> CodexUsageDataSource {
guard let source else { return .auto }
switch source {
case .auto, .web, .api:
return .auto
case .cli:
return .cli
case .oauth:
return .oauth
}
}
private func codexSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String {
let fallback = self.codexCookieHeader
guard let support = TokenAccountSupportCatalog.support(for: .codex),
case .cookieHeader = support.injection
else {
return fallback
}
guard let account = ProviderTokenAccountSelection.selectedAccount(
provider: .codex,
settings: self,
override: tokenOverride)
else {
return fallback
}
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
}
private func codexSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
let fallback = self.codexCookieSource
guard let support = TokenAccountSupportCatalog.support(for: .codex),
support.requiresManualCookieSource
else {
return fallback
}
if self.tokenAccounts(for: .codex).isEmpty { return fallback }
return .manual
}
}