// // APIKeyRecord.swift // Zyquo Router // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // A local router API key (`zyquo-sk-…`). The plaintext token is shown once at // creation and never stored — only its SHA-256 hash persists. // import CryptoKit import Foundation struct APIKeyRecord: Codable, Identifiable, Hashable, Sendable { let id: UUID var name: String /// Hex SHA-256 of the full token. var tokenHash: String /// First 12 characters of the token, for display ("zyquo-sk-a1b…"). var tokenPrefix: String var enabled: Bool var createdAt: Date /// Requests per minute; nil = unlimited. var rateLimitPerMinute: Int? /// Namespaced model IDs this key may use; nil = all models. var allowedModels: Set? init( id: UUID = UUID(), name: String, tokenHash: String, tokenPrefix: String, enabled: Bool = true, createdAt: Date = Date(), rateLimitPerMinute: Int? = nil, allowedModels: Set? = nil ) { self.id = id self.name = name self.tokenHash = tokenHash self.tokenPrefix = tokenPrefix self.enabled = enabled self.createdAt = createdAt self.rateLimitPerMinute = rateLimitPerMinute self.allowedModels = allowedModels } /// Generates a fresh token and its record. The token is returned exactly once. static func generate(name: String) -> (record: APIKeyRecord, token: String) { let random = Data((0..<24).map { _ in UInt8.random(in: 0...255) }) let token = "zyquo-sk-" + random.base64EncodedString() .replacingOccurrences(of: "+", with: "a") .replacingOccurrences(of: "/", with: "b") .replacingOccurrences(of: "=", with: "") let record = APIKeyRecord( name: name, tokenHash: Self.hash(token), tokenPrefix: String(token.prefix(12)) ) return (record, token) } static func hash(_ token: String) -> String { SHA256.hash(data: Data(token.utf8)).map { String(format: "%02x", $0) }.joined() } }