chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user