SPB Git

spb/zyquo-cloud Public MIT

Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.

Swift 97.4% Shell 1.7% Makefile 1%
3.1 KB · 94 lines swift
Raw Blame History
1//2//  KeyVaultStore.swift3//  Zyquo Cloud4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Observable wrapper around SecureKeyStore for the Settings UI: per-provider9//  key presence, redacted display, and "Test" with latency. Decrypted keys are10//  fetched on demand and never retained beyond the call.11//1213import Foundation1415@MainActor16final class KeyVaultStore: ObservableObject {17    enum KeyStatus: Equatable {18        case unset19        case saved                    // present, not yet verified this session20        case testing21        case verified(latency: TimeInterval)22        case failed(message: String)23    }2425    @Published private(set) var statuses: [ProviderID: KeyStatus] = [:]26    /// Redacted display strings (••••1234) for providers with saved keys.27    @Published private(set) var redactedKeys: [ProviderID: String] = [:]2829    private let store: SecureKeyStore3031    init(store: SecureKeyStore = SecureKeyStore()) {32        self.store = store33        refresh()34    }3536    func refresh() {37        let keys = (try? store.loadKeys()) ?? [:]38        for provider in ProviderID.builtIn {39            if let key = keys[provider.rawValue], !key.isEmpty {40                redactedKeys[provider] = SecureKeyStore.redacted(key)41                if case .verified = statuses[provider] ?? .unset {} else {42                    statuses[provider] = .saved43                }44            } else {45                redactedKeys[provider] = nil46                statuses[provider] = .unset47            }48        }49    }5051    func hasKey(for provider: ProviderID) -> Bool {52        redactedKeys[provider] != nil53    }5455    /// Decrypts and returns the key — call sites use it immediately and drop it.56    func apiKey(for provider: ProviderID) throws -> String {57        guard let key = try store.key(for: provider), !key.isEmpty else {58            throw ProviderError.missingAPIKey(provider)59        }60        return key61    }6263    func setKey(_ key: String, for provider: ProviderID) {64        let trimmed = key.trimmingCharacters(in: .whitespacesAndNewlines)65        guard !trimmed.isEmpty else { return }66        try? store.setKey(trimmed, for: provider)67        statuses[provider] = .saved68        refresh()69    }7071    func deleteKey(for provider: ProviderID) {72        try? store.deleteKey(for: provider)73        statuses[provider] = .unset74        refresh()75    }7677    /// Runs the cheapest authenticated call and records latency or failure.78    func testKey(for provider: ProviderID, catalog: ModelCatalog) async {79        guard let key = try? apiKey(for: provider) else {80            statuses[provider] = .failed(message: "No key saved")81            return82        }83        statuses[provider] = .testing84        let client = ProviderRegistry.client(for: provider)85        let fallback = catalog.cheapestModel(for: provider)86        do {87            let latency = try await client.testKey(key, fallbackModel: fallback)88            statuses[provider] = .verified(latency: latency)89        } catch {90            statuses[provider] = .failed(message: error.localizedDescription)91        }92    }93}94