169 lines · 6.8 KB · Swift
RawDownload
import Cryptoimport Foundation public struct ManagedCodexAccount: Codable, Identifiable, Sendable {    public let id: UUID    public let email: String    public let providerAccountID: String?    public let workspaceLabel: String?    public let workspaceAccountID: String?    public let authFingerprint: String?    public let managedHomePath: String    public let createdAt: TimeInterval    public let updatedAt: TimeInterval    public let lastAuthenticatedAt: TimeInterval?     public init(        id: UUID,        email: String,        providerAccountID: String? = nil,        workspaceLabel: String? = nil,        workspaceAccountID: String? = nil,        authFingerprint: String? = nil,        managedHomePath: String,        createdAt: TimeInterval,        updatedAt: TimeInterval,        lastAuthenticatedAt: TimeInterval?)    {        self.id = id        self.email = Self.normalizeEmail(email)        self.providerAccountID = Self.normalizeProviderAccountID(providerAccountID)        self.workspaceLabel = Self.normalizeWorkspaceLabel(workspaceLabel)        self.workspaceAccountID = Self.normalizeWorkspaceAccountID(workspaceAccountID)        self.authFingerprint = CodexAuthFingerprint.normalize(authFingerprint)        self.managedHomePath = managedHomePath        self.createdAt = createdAt        self.updatedAt = updatedAt        self.lastAuthenticatedAt = lastAuthenticatedAt    }     static func normalizeEmail(_ email: String) -> String {        email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()    }     public static func normalizeProviderAccountID(_ providerAccountID: String?) -> String? {        CodexIdentityResolver.normalizeAccountID(providerAccountID)    }     public static func normalizeWorkspaceLabel(_ workspaceLabel: String?) -> String? {        guard let trimmed = workspaceLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {            return nil        }        return trimmed    }     public static func normalizeWorkspaceAccountID(_ workspaceAccountID: String?) -> String? {        guard let trimmed = workspaceAccountID?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {            return nil        }        return trimmed.lowercased()    }     public init(from decoder: any Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        try self.init(            id: container.decode(UUID.self, forKey: .id),            email: container.decode(String.self, forKey: .email),            providerAccountID: container.decodeIfPresent(String.self, forKey: .providerAccountID),            workspaceLabel: container.decodeIfPresent(String.self, forKey: .workspaceLabel),            workspaceAccountID: container.decodeIfPresent(String.self, forKey: .workspaceAccountID),            authFingerprint: container.decodeIfPresent(String.self, forKey: .authFingerprint),            managedHomePath: container.decode(String.self, forKey: .managedHomePath),            createdAt: container.decode(TimeInterval.self, forKey: .createdAt),            updatedAt: container.decode(TimeInterval.self, forKey: .updatedAt),            lastAuthenticatedAt: container.decodeIfPresent(TimeInterval.self, forKey: .lastAuthenticatedAt))    }} public enum CodexAuthFingerprint {    public static func authFileURL(homePath: String) -> URL {        URL(fileURLWithPath: homePath, isDirectory: true)            .appendingPathComponent("auth.json", isDirectory: false)    }     public static func fingerprint(homePath: String, fileManager: FileManager = .default) -> String? {        let url = self.authFileURL(homePath: homePath)        guard fileManager.fileExists(atPath: url.path),              let data = try? Data(contentsOf: url)        else {            return nil        }        return self.fingerprint(data: data)    }     public static func fingerprint(env: [String: String], fileManager: FileManager = .default) -> String? {        self.fingerprint(            homePath: CodexHomeScope.ambientHomeURL(env: env, fileManager: fileManager).path,            fileManager: fileManager)    }     public static func fingerprint(data: Data) -> String {        SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()    }     public static func normalize(_ fingerprint: String?) -> String? {        guard let trimmed = fingerprint?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),              !trimmed.isEmpty        else {            return nil        }        return trimmed    }} public struct ManagedCodexAccountSet: Codable, Sendable {    public let version: Int    public let accounts: [ManagedCodexAccount]     public init(version: Int, accounts: [ManagedCodexAccount]) {        self.version = version        self.accounts = Self.sanitizedAccounts(accounts)    }     public func account(id: UUID) -> ManagedCodexAccount? {        self.accounts.first { $0.id == id }    }     public func account(email: String, providerAccountID: String? = nil) -> ManagedCodexAccount? {        let normalizedEmail = ManagedCodexAccount.normalizeEmail(email)        if let normalizedProviderAccountID = ManagedCodexAccount.normalizeProviderAccountID(providerAccountID),           let exactMatch = self.accounts.first(where: {               $0.email == normalizedEmail && $0.providerAccountID == normalizedProviderAccountID           })        {            return exactMatch        }        if providerAccountID != nil {            return self.accounts.first { $0.email == normalizedEmail && $0.providerAccountID == nil }        }        return self.accounts.first { $0.email == normalizedEmail }    }     public init(from decoder: any Decoder) throws {        let container = try decoder.container(keyedBy: CodingKeys.self)        self.version = try container.decode(Int.self, forKey: .version)        self.accounts = try container.decode([ManagedCodexAccount].self, forKey: .accounts)    }     private static func sanitizedAccounts(_ accounts: [ManagedCodexAccount]) -> [ManagedCodexAccount] {        var seenIDs: Set<UUID> = []        var seenProviderAccountKeys: Set<String> = []        var seenLegacyEmails: Set<String> = []        var sanitized: [ManagedCodexAccount] = []        sanitized.reserveCapacity(accounts.count)         for account in accounts {            guard seenIDs.insert(account.id).inserted else { continue }            if let providerAccountID = account.providerAccountID {                guard seenProviderAccountKeys.insert("\(account.email)\u{0}\(providerAccountID)").inserted else {                    continue                }            } else {                guard seenLegacyEmails.insert(account.email).inserted else { continue }            }            sanitized.append(account)        }         return sanitized    }}