// // KeyVaultStore.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Observable wrapper around SecureKeyStore for the Keys screen: per-provider // key presence, redacted display, and "Test" with latency. Decrypted keys are // fetched on demand and never retained beyond the call. // import Foundation @MainActor final class KeyVaultStore: ObservableObject { enum KeyStatus: Equatable { case unset case saved // present, not yet verified this session case testing case verified(latency: TimeInterval) case failed(message: String) } @Published private(set) var statuses: [ProviderID: KeyStatus] = [:] /// Redacted display strings (••••1234) for providers with saved keys. @Published private(set) var redactedKeys: [ProviderID: String] = [:] private let store: SecureKeyStore init(store: SecureKeyStore = SecureKeyStore()) { self.store = store refresh() } func refresh() { let keys = (try? store.loadKeys()) ?? [:] for provider in ProviderID.builtIn { if let key = keys[provider.rawValue], !key.isEmpty { redactedKeys[provider] = SecureKeyStore.redacted(key) if case .verified = statuses[provider] ?? .unset {} else { statuses[provider] = .saved } } else { redactedKeys[provider] = nil statuses[provider] = .unset } } } func hasKey(for provider: ProviderID) -> Bool { redactedKeys[provider] != nil } /// Decrypts and returns the key — call sites use it immediately and drop it. func apiKey(for provider: ProviderID) throws -> String { guard let key = try store.key(for: provider), !key.isEmpty else { throw ProviderError.missingAPIKey(provider) } return key } func setKey(_ key: String, for provider: ProviderID) { let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } try? store.setKey(trimmed, for: provider) statuses[provider] = .saved refresh() } func deleteKey(for provider: ProviderID) { try? store.deleteKey(for: provider) statuses[provider] = .unset refresh() } /// Runs the cheapest authenticated call and records latency or failure. func testKey(for provider: ProviderID, catalog: ModelCatalog) async { guard let key = try? apiKey(for: provider) else { statuses[provider] = .failed(message: "No key saved") return } statuses[provider] = .testing let client = ProviderRegistry.client(for: provider) let fallback = catalog.cheapestModel(for: provider) do { let latency = try await client.testKey(key, fallbackModel: fallback) statuses[provider] = .verified(latency: latency) } catch { statuses[provider] = .failed(message: error.localizedDescription) } } }