374 lines
14 KiB
Swift
374 lines
14 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
|
|
struct CodexBarConfigMigrator {
|
|
struct LegacyStores {
|
|
let zaiTokenStore: any ZaiTokenStoring
|
|
let syntheticTokenStore: any SyntheticTokenStoring
|
|
let codexCookieStore: any CookieHeaderStoring
|
|
let claudeCookieStore: any CookieHeaderStoring
|
|
let cursorCookieStore: any CookieHeaderStoring
|
|
let opencodeCookieStore: any CookieHeaderStoring
|
|
let factoryCookieStore: any CookieHeaderStoring
|
|
let minimaxCookieStore: any MiniMaxCookieStoring
|
|
let minimaxAPITokenStore: any MiniMaxAPITokenStoring
|
|
let kimiTokenStore: any KimiTokenStoring
|
|
let kimiK2TokenStore: any KimiK2TokenStoring
|
|
let augmentCookieStore: any CookieHeaderStoring
|
|
let ampCookieStore: any CookieHeaderStoring
|
|
let copilotTokenStore: any CopilotTokenStoring
|
|
let tokenAccountStore: any ProviderTokenAccountStoring
|
|
}
|
|
|
|
private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted"
|
|
|
|
private struct MigrationState {
|
|
var didUpdate = false
|
|
var sawLegacySecrets = false
|
|
var sawLegacyAccounts = false
|
|
}
|
|
|
|
static func loadOrMigrate(
|
|
configStore: CodexBarConfigStore,
|
|
userDefaults: UserDefaults,
|
|
stores: LegacyStores) -> CodexBarConfig
|
|
{
|
|
let log = CodexBarLog.logger(LogCategories.configMigration)
|
|
let existing = try? configStore.load()
|
|
var config = (existing ?? CodexBarConfig.makeDefault()).normalized()
|
|
var state = MigrationState()
|
|
|
|
// applyLegacyCookieSources reads only UserDefaults — cheap, runs unconditionally so
|
|
// newly-added cookie-source keys are picked up on every launch.
|
|
self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state)
|
|
|
|
let migrationCompleted = userDefaults.bool(forKey: Self.legacyMigrationCompletedKey)
|
|
if !migrationCompleted {
|
|
// Run once: migrate Keychain/file secrets then clear them. Using a completion flag rather
|
|
// than `existing == nil` ensures a crash between config-save and clearLegacyStores can
|
|
// finish cleanup on the next launch without re-doing the (already-saved) data migration.
|
|
if existing == nil {
|
|
self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state)
|
|
}
|
|
self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
|
|
self.migrateLegacyAccounts(stores: stores, config: &config, state: &state)
|
|
}
|
|
|
|
var didPersistUpdates = true
|
|
if state.didUpdate {
|
|
do {
|
|
try configStore.save(config)
|
|
} catch {
|
|
didPersistUpdates = false
|
|
log.error("Failed to persist config: \(error)")
|
|
}
|
|
}
|
|
|
|
guard didPersistUpdates else {
|
|
return config.normalized()
|
|
}
|
|
|
|
if state.sawLegacySecrets || state.sawLegacyAccounts {
|
|
let cleared = self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log)
|
|
if cleared {
|
|
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
|
|
}
|
|
} else if !migrationCompleted {
|
|
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
|
|
}
|
|
|
|
return config.normalized()
|
|
}
|
|
|
|
private static func applyLegacyOrderAndToggles(
|
|
userDefaults: UserDefaults,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
if let order = userDefaults.stringArray(forKey: "providerOrder"), !order.isEmpty {
|
|
config = self.applyProviderOrder(order, config: config)
|
|
state.didUpdate = true
|
|
}
|
|
let toggles = userDefaults.dictionary(forKey: "providerToggles") as? [String: Bool] ?? [:]
|
|
if !toggles.isEmpty {
|
|
config = self.applyProviderToggles(toggles, config: config)
|
|
state.didUpdate = true
|
|
}
|
|
}
|
|
|
|
private static func migrateLegacySecrets(
|
|
userDefaults: UserDefaults,
|
|
stores: LegacyStores,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
self.migrateTokenProviders(
|
|
[
|
|
(.zai, stores.zaiTokenStore.loadToken),
|
|
(.synthetic, stores.syntheticTokenStore.loadToken),
|
|
(.copilot, stores.copilotTokenStore.loadToken),
|
|
(.kimik2, stores.kimiK2TokenStore.loadToken),
|
|
],
|
|
config: &config,
|
|
state: &state)
|
|
|
|
self.migrateCookieProviders(
|
|
[
|
|
(.codex, stores.codexCookieStore.loadCookieHeader),
|
|
(.claude, stores.claudeCookieStore.loadCookieHeader),
|
|
(.cursor, stores.cursorCookieStore.loadCookieHeader),
|
|
(.factory, stores.factoryCookieStore.loadCookieHeader),
|
|
(.augment, stores.augmentCookieStore.loadCookieHeader),
|
|
(.amp, stores.ampCookieStore.loadCookieHeader),
|
|
],
|
|
config: &config,
|
|
state: &state)
|
|
|
|
self.migrateMiniMax(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
|
|
self.migrateKimi(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
|
|
self.migrateOpenCode(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
|
|
}
|
|
|
|
private static func applyLegacyCookieSources(
|
|
userDefaults: UserDefaults,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
let sources: [(UsageProvider, String)] = [
|
|
(.codex, "codexCookieSource"),
|
|
(.claude, "claudeCookieSource"),
|
|
(.cursor, "cursorCookieSource"),
|
|
(.opencode, "opencodeCookieSource"),
|
|
(.factory, "factoryCookieSource"),
|
|
(.minimax, "minimaxCookieSource"),
|
|
(.kimi, "kimiCookieSource"),
|
|
(.augment, "augmentCookieSource"),
|
|
(.amp, "ampCookieSource"),
|
|
]
|
|
|
|
for (provider, key) in sources {
|
|
guard let raw = userDefaults.string(forKey: key),
|
|
let source = ProviderCookieSource(rawValue: raw)
|
|
else { continue }
|
|
self.updateProvider(provider, config: &config, state: &state) { entry in
|
|
guard entry.cookieSource == nil else { return false }
|
|
entry.cookieSource = source
|
|
return true
|
|
}
|
|
}
|
|
|
|
if userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool == false {
|
|
self.updateProvider(.codex, config: &config, state: &state) { entry in
|
|
guard entry.cookieSource == nil else { return false }
|
|
entry.cookieSource = .off
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func migrateTokenProviders(
|
|
_ providers: [(UsageProvider, () throws -> String?)],
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
for (provider, loader) in providers {
|
|
let token = try? loader()
|
|
if token != nil { state.sawLegacySecrets = true }
|
|
self.updateProvider(provider, config: &config, state: &state) { entry in
|
|
self.setIfEmpty(&entry.apiKey, token)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func migrateCookieProviders(
|
|
_ providers: [(UsageProvider, () throws -> String?)],
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
for (provider, loader) in providers {
|
|
let header = try? loader()
|
|
if header != nil { state.sawLegacySecrets = true }
|
|
self.updateProvider(provider, config: &config, state: &state) { entry in
|
|
self.setIfEmpty(&entry.cookieHeader, header)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func migrateMiniMax(
|
|
userDefaults: UserDefaults,
|
|
stores: LegacyStores,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
let token = try? stores.minimaxAPITokenStore.loadToken()
|
|
let header = try? stores.minimaxCookieStore.loadCookieHeader()
|
|
if token != nil || header != nil {
|
|
state.sawLegacySecrets = true
|
|
}
|
|
let regionRaw = userDefaults.string(forKey: "minimaxAPIRegion")
|
|
self.updateProvider(.minimax, config: &config, state: &state) { entry in
|
|
var changed = false
|
|
changed = self.setIfEmpty(&entry.apiKey, token) || changed
|
|
if let regionRaw, !regionRaw.isEmpty, entry.region == nil {
|
|
entry.region = regionRaw
|
|
changed = true
|
|
}
|
|
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
|
|
return changed
|
|
}
|
|
}
|
|
|
|
private static func migrateKimi(
|
|
userDefaults: UserDefaults,
|
|
stores: LegacyStores,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
var token = try? stores.kimiTokenStore.loadToken()
|
|
if token?.isEmpty ?? true {
|
|
token = userDefaults.string(forKey: "kimiManualCookieHeader")
|
|
}
|
|
if token != nil { state.sawLegacySecrets = true }
|
|
self.updateProvider(.kimi, config: &config, state: &state) { entry in
|
|
self.setIfEmpty(&entry.cookieHeader, token)
|
|
}
|
|
}
|
|
|
|
private static func migrateOpenCode(
|
|
userDefaults: UserDefaults,
|
|
stores: LegacyStores,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
let header = try? stores.opencodeCookieStore.loadCookieHeader()
|
|
if header != nil { state.sawLegacySecrets = true }
|
|
let workspaceID = userDefaults.string(forKey: "opencodeWorkspaceID")
|
|
self.updateProvider(.opencode, config: &config, state: &state) { entry in
|
|
var changed = false
|
|
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
|
|
if let workspaceID, !workspaceID.isEmpty, entry.workspaceID == nil {
|
|
entry.workspaceID = workspaceID
|
|
changed = true
|
|
}
|
|
return changed
|
|
}
|
|
}
|
|
|
|
private static func migrateLegacyAccounts(
|
|
stores: LegacyStores,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState)
|
|
{
|
|
guard let accounts = try? stores.tokenAccountStore.loadAccounts(), !accounts.isEmpty else { return }
|
|
state.sawLegacyAccounts = true
|
|
for (provider, data) in accounts where !data.accounts.isEmpty {
|
|
self.updateProvider(provider, config: &config, state: &state) { entry in
|
|
guard entry.tokenAccounts == nil else { return false }
|
|
entry.tokenAccounts = data
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func updateProvider(
|
|
_ provider: UsageProvider,
|
|
config: inout CodexBarConfig,
|
|
state: inout MigrationState,
|
|
mutate: (inout ProviderConfig) -> Bool)
|
|
{
|
|
guard let index = config.providers.firstIndex(where: { $0.id == provider }) else { return }
|
|
var entry = config.providers[index]
|
|
let changed = mutate(&entry)
|
|
if changed {
|
|
config.providers[index] = entry
|
|
state.didUpdate = true
|
|
}
|
|
}
|
|
|
|
private static func setIfEmpty(_ value: inout String?, _ replacement: String?) -> Bool {
|
|
let cleaned = replacement?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let cleaned, !cleaned.isEmpty else { return false }
|
|
if value == nil || value?.isEmpty == true {
|
|
value = cleaned
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@discardableResult
|
|
private static func clearLegacyStores(
|
|
stores: LegacyStores,
|
|
sawAccounts: Bool,
|
|
log: CodexBarLogger) -> Bool
|
|
{
|
|
var success = true
|
|
do {
|
|
try stores.zaiTokenStore.storeToken(nil)
|
|
try stores.syntheticTokenStore.storeToken(nil)
|
|
try stores.copilotTokenStore.storeToken(nil)
|
|
try stores.minimaxAPITokenStore.storeToken(nil)
|
|
try stores.kimiTokenStore.storeToken(nil)
|
|
try stores.kimiK2TokenStore.storeToken(nil)
|
|
try stores.codexCookieStore.storeCookieHeader(nil)
|
|
try stores.claudeCookieStore.storeCookieHeader(nil)
|
|
try stores.cursorCookieStore.storeCookieHeader(nil)
|
|
try stores.opencodeCookieStore.storeCookieHeader(nil)
|
|
try stores.factoryCookieStore.storeCookieHeader(nil)
|
|
try stores.minimaxCookieStore.storeCookieHeader(nil)
|
|
try stores.augmentCookieStore.storeCookieHeader(nil)
|
|
try stores.ampCookieStore.storeCookieHeader(nil)
|
|
} catch {
|
|
log.error("Failed to clear legacy secrets: \(error)")
|
|
success = false
|
|
}
|
|
|
|
if sawAccounts {
|
|
let legacyURL = FileTokenAccountStore.defaultURL()
|
|
if FileManager.default.fileExists(atPath: legacyURL.path) {
|
|
try? FileManager.default.removeItem(at: legacyURL)
|
|
}
|
|
}
|
|
|
|
return success
|
|
}
|
|
|
|
private static func applyProviderOrder(_ raw: [String], config: CodexBarConfig) -> CodexBarConfig {
|
|
let configsByID = Dictionary(uniqueKeysWithValues: config.providers.map { ($0.id, $0) })
|
|
var seen: Set<UsageProvider> = []
|
|
var ordered: [ProviderConfig] = []
|
|
ordered.reserveCapacity(config.providers.count)
|
|
|
|
for rawValue in raw {
|
|
guard let provider = UsageProvider(rawValue: rawValue),
|
|
let entry = configsByID[provider],
|
|
!seen.contains(provider)
|
|
else { continue }
|
|
seen.insert(provider)
|
|
ordered.append(entry)
|
|
}
|
|
|
|
for provider in UsageProvider.allCases where !seen.contains(provider) {
|
|
ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))
|
|
}
|
|
|
|
var updated = config
|
|
updated.providers = ordered
|
|
return updated
|
|
}
|
|
|
|
private static func applyProviderToggles(
|
|
_ toggles: [String: Bool],
|
|
config: CodexBarConfig) -> CodexBarConfig
|
|
{
|
|
var updated = config
|
|
for index in updated.providers.indices {
|
|
let provider = updated.providers[index].id
|
|
let meta = ProviderDescriptorRegistry.descriptor(for: provider).metadata
|
|
if let value = toggles[meta.cliName] {
|
|
updated.providers[index].enabled = value
|
|
}
|
|
}
|
|
return updated
|
|
}
|
|
}
|