974 lines · 38.5 KB · Swift
RawDownload
#if os(macOS)import Foundationimport SweetCookieKitimport WebKit @MainActor// swiftlint:disable:next type_body_lengthpublic struct OpenAIDashboardBrowserCookieImporter {    public struct FoundAccount: Sendable, Hashable {        public let sourceLabel: String        public let email: String         public init(sourceLabel: String, email: String) {            self.sourceLabel = sourceLabel            self.email = email        }    }     public enum ImportError: LocalizedError {        case noCookiesFound        case browserAccessDenied(details: String)        case dashboardStillRequiresLogin        case noMatchingAccount(found: [FoundAccount])        case manualCookieHeaderInvalid         public var errorDescription: String? {            switch self {            case .noCookiesFound:                return "No browser cookies found."            case let .browserAccessDenied(details):                return "Browser cookie access denied. \(details)"            case .dashboardStillRequiresLogin:                return "Browser cookies imported, but dashboard still requires login."            case let .noMatchingAccount(found):                if found.isEmpty { return "No matching OpenAI web session found in browsers." }                let display = found                    .sorted { lhs, rhs in                        if lhs.sourceLabel == rhs.sourceLabel { return lhs.email < rhs.email }                        return lhs.sourceLabel < rhs.sourceLabel                    }                    .map { "\($0.sourceLabel)=\($0.email)" }                    .joined(separator: ", ")                return "OpenAI web session does not match Codex account. Found: \(display)."            case .manualCookieHeaderInvalid:                return "Manual cookie header is missing a valid OpenAI session cookie."            }        }    }     public struct ImportResult: Sendable {        public let sourceLabel: String        public let cookieCount: Int        public let signedInEmail: String?        public let matchesCodexEmail: Bool         public init(            sourceLabel: String,            cookieCount: Int,            signedInEmail: String?,            matchesCodexEmail: Bool)        {            self.sourceLabel = sourceLabel            self.cookieCount = cookieCount            self.signedInEmail = signedInEmail            self.matchesCodexEmail = matchesCodexEmail        }    }     public init(browserDetection: BrowserDetection) {        self.browserDetection = browserDetection    }     private let browserDetection: BrowserDetection     struct PersistentValidationTimeout: Error {}     nonisolated static func shouldTrustVerifiedSession(afterPersistFailure error: Error) -> Bool {        error is PersistentValidationTimeout    }     nonisolated static func persistentValidationFailure(_ error: Error) -> Error {        let nsError = error as NSError        guard nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorTimedOut else { return error }        return PersistentValidationTimeout()    }     private struct ImportDiagnostics {        var mismatches: [FoundAccount] = []        var foundAnyCookies: Bool = false        var foundUnknownEmail: Bool = false        var accessDeniedHints: [String] = []    }     private struct ImportContext {        let targetEmail: String?        let allowAnyAccount: Bool        let cacheScope: CookieHeaderCache.Scope?        let deadline: Date?    }     private static let cookieDomains = ["chatgpt.com", "openai.com"]    private nonisolated static let cookieClient = BrowserCookieClient()    private static let cookieImportOrder: BrowserCookieImportOrder =        ProviderDefaults.metadata[.codex]?.browserCookieOrder ?? Browser.defaultImportOrder     private enum CandidateEvaluation {        case match(candidate: Candidate, signedInEmail: String)        case mismatch(candidate: Candidate, signedInEmail: String)        case loggedIn(candidate: Candidate, signedInEmail: String)        case unknown(candidate: Candidate)        case loginRequired(candidate: Candidate)    }     public func importBestCookies(        intoAccountEmail targetEmail: String?,        allowAnyAccount: Bool = false,        preferCachedCookieHeader: Bool = true,        cacheScope: CookieHeaderCache.Scope? = nil,        deadline: Date? = nil,        logger: ((String) -> Void)? = nil) async throws -> ImportResult    {        let log: (String) -> Void = { message in            logger?("[web] \(message)")        }         let targetEmail = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)        let normalizedTarget = targetEmail?.isEmpty == false ? targetEmail : nil        let context = ImportContext(            targetEmail: normalizedTarget,            allowAnyAccount: allowAnyAccount,            cacheScope: cacheScope,            deadline: deadline)         if normalizedTarget != nil {            log("Codex email known; matching required.")        } else {            guard context.allowAnyAccount else {                throw ImportError.noCookiesFound            }            log("Codex email unknown; importing any signed-in session.")        }         var diagnostics = ImportDiagnostics()         if preferCachedCookieHeader {            let cached = try await Self.runBoundedCookieCacheOperation(deadline: deadline) {                CookieHeaderCache.loadSerialized(provider: .codex, scope: cacheScope)            }            if let cached,               !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty            {                log("Using cached cookie header from \(cached.sourceLabel)")                do {                    return try await self.importManualCookies(                        cookieHeader: cached.cookieHeader,                        intoAccountEmail: context.targetEmail,                        allowAnyAccount: context.allowAnyAccount,                        cacheScope: cacheScope,                        deadline: deadline,                        logger: log)                } catch let error as ImportError {                    switch error {                    case .manualCookieHeaderInvalid, .noMatchingAccount, .dashboardStillRequiresLogin:                        do {                            _ = try await Self.runBoundedCookieCacheOperation(deadline: deadline) {                                CookieHeaderCache.clear(provider: .codex, scope: cacheScope)                            }                        } catch {                            log("Stale cookie cache cleanup did not finish before the web deadline.")                        }                    default:                        throw error                    }                } catch {                    throw error                }            }        } else {            log("Skipping cached cookie header; forcing fresh browser import")        }         // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts        let installedBrowsers = Self.cookieImportOrder.cookieImportCandidates(using: self.browserDetection)        for browserSource in installedBrowsers {            _ = try Self.remainingTimeout(until: deadline)            if let match = try await self.trySource(                browserSource,                context: context,                deadline: deadline,                log: log,                diagnostics: &diagnostics)            {                return match            }        }        _ = try Self.remainingTimeout(until: deadline)         if !diagnostics.mismatches.isEmpty {            let found = Array(Set(diagnostics.mismatches)).sorted { lhs, rhs in                if lhs.sourceLabel == rhs.sourceLabel { return lhs.email < rhs.email }                return lhs.sourceLabel < rhs.sourceLabel            }            log("No matching browser session found. Candidate count: \(found.count)")            throw ImportError.noMatchingAccount(found: found)        }         if diagnostics.foundUnknownEmail || diagnostics.foundAnyCookies {            log("No matching browser session found (email unknown).")            throw ImportError.noMatchingAccount(found: [])        }         if !diagnostics.accessDeniedHints.isEmpty {            let details = diagnostics.accessDeniedHints.joined(separator: " ")            log("Cookie access denied: \(details)")            throw ImportError.browserAccessDenied(details: details)        }         throw ImportError.noCookiesFound    }     public func importManualCookies(        cookieHeader: String,        intoAccountEmail targetEmail: String?,        allowAnyAccount: Bool = false,        cacheScope: CookieHeaderCache.Scope? = nil,        deadline: Date? = nil,        logger: ((String) -> Void)? = nil) async throws -> ImportResult    {        let log: (String) -> Void = { message in            logger?("[web] \(message)")        }        let normalizedTarget = targetEmail?.trimmingCharacters(in: .whitespacesAndNewlines)        let allowAnyAccount = allowAnyAccount || normalizedTarget == nil || normalizedTarget?.isEmpty == true         guard let normalized = CookieHeaderNormalizer.normalize(cookieHeader) else {            throw ImportError.manualCookieHeaderInvalid        }        let pairs = CookieHeaderNormalizer.pairs(from: normalized)        guard !pairs.isEmpty else { throw ImportError.manualCookieHeaderInvalid }        let cookies = self.cookies(from: pairs)        guard !cookies.isEmpty else { throw ImportError.manualCookieHeaderInvalid }         let candidate = Candidate(label: "Manual", cookies: cookies)        switch try await self.evaluateCandidate(            candidate,            targetEmail: normalizedTarget,            allowAnyAccount: allowAnyAccount,            deadline: deadline,            log: log)        {        case let .match(_, signedInEmail):            return try await self.persistVerifiedCandidate(                candidate: candidate,                targetEmail: signedInEmail,                cacheScope: cacheScope,                deadline: deadline,                logger: log)        case let .loggedIn(_, signedInEmail):            return try await self.persistVerifiedCandidate(                candidate: candidate,                targetEmail: signedInEmail,                cacheScope: cacheScope,                deadline: deadline,                logger: log)        case let .mismatch(_, signedInEmail):            throw ImportError.noMatchingAccount(found: [FoundAccount(sourceLabel: "Manual", email: signedInEmail)])        case .unknown:            if allowAnyAccount {                return try await self.persistToDefaultStore(                    candidate: candidate,                    deadline: deadline,                    logger: log)            }            throw ImportError.noMatchingAccount(found: [])        case .loginRequired:            throw ImportError.manualCookieHeaderInvalid        }    }     private func trySafari(        context: ImportContext,        deadline: Date?,        log: @escaping (String) -> Void,        diagnostics: inout ImportDiagnostics) async throws -> ImportResult?    {        // Safari first: avoids touching Keychain ("Chrome Safe Storage") when Safari already matches.        do {            let query = BrowserCookieQuery(domains: Self.cookieDomains)            let sources = try await Self.runBoundedCookieLoad(deadline: deadline) {                try Self.cookieClient.codexBarRecords(matching: query, in: .safari)            }            _ = try Self.remainingTimeout(until: deadline)            guard !sources.isEmpty else {                log("Safari contained 0 matching records.")                return nil            }            for source in sources {                _ = try Self.remainingTimeout(until: deadline)                let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)                guard !cookies.isEmpty else {                    log("\(source.label) produced 0 HTTPCookies.")                    continue                }                 diagnostics.foundAnyCookies = true                log("Loaded \(cookies.count) cookies from \(source.label) (\(self.cookieSummary(cookies)))")                let candidate = Candidate(label: source.label, cookies: cookies)                if let match = try await self.applyCandidate(                    candidate,                    context: context,                    deadline: deadline,                    log: log,                    diagnostics: &diagnostics)                {                    return match                }            }            return nil        } catch let error as URLError where error.code == .timedOut {            throw error        } catch let error as BrowserCookieError {            BrowserCookieAccessGate.recordIfNeeded(error)            if let hint = error.accessDeniedHint {                diagnostics.accessDeniedHints.append(hint)            }            log("Safari cookie load failed: \(error.localizedDescription)")            return nil        } catch {            log("Safari cookie load failed: \(error.localizedDescription)")            return nil        }    }     /// Generic cookie loader for any non-Safari browser (Chrome, Edge, Firefox, Brave, Arc, etc.).    /// SweetCookieKit handles engine-specific decryption internally.    private func tryBrowser(        _ browser: Browser,        context: ImportContext,        deadline: Date?,        log: @escaping (String) -> Void,        diagnostics: inout ImportDiagnostics) async throws -> ImportResult?    {        do {            let query = BrowserCookieQuery(domains: Self.cookieDomains)            let sources = try await Self.runBoundedCookieLoad(deadline: deadline) {                try Self.cookieClient.codexBarRecords(matching: query, in: browser)            }            _ = try Self.remainingTimeout(until: deadline)            guard !sources.isEmpty else {                log("\(browser.displayName) contained 0 matching records.")                return nil            }            for source in sources {                _ = try Self.remainingTimeout(until: deadline)                let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)                guard !cookies.isEmpty else {                    log("\(source.label) produced 0 HTTPCookies.")                    continue                }                diagnostics.foundAnyCookies = true                log("Loaded \(cookies.count) cookies from \(source.label) (\(self.cookieSummary(cookies)))")                let candidate = Candidate(label: source.label, cookies: cookies)                if let match = try await self.applyCandidate(                    candidate,                    context: context,                    deadline: deadline,                    log: log,                    diagnostics: &diagnostics)                {                    return match                }            }            return nil        } catch let error as URLError where error.code == .timedOut {            throw error        } catch let error as BrowserCookieError {            BrowserCookieAccessGate.recordIfNeeded(error)            if let hint = error.accessDeniedHint {                diagnostics.accessDeniedHints.append(hint)            }            log("\(browser.displayName) cookie load failed: \(error.localizedDescription)")            return nil        } catch {            log("\(browser.displayName) cookie load failed: \(error.localizedDescription)")            return nil        }    }     private func trySource(        _ source: Browser,        context: ImportContext,        deadline: Date?,        log: @escaping (String) -> Void,        diagnostics: inout ImportDiagnostics) async throws -> ImportResult?    {        switch source {        case .safari:            try await self.trySafari(                context: context,                deadline: deadline,                log: log,                diagnostics: &diagnostics)        default:            // All non-Safari browsers (Chrome, Edge, Firefox, Brave, Arc, etc.)            // share the same cookie loading path via SweetCookieKit.            try await self.tryBrowser(                source,                context: context,                deadline: deadline,                log: log,                diagnostics: &diagnostics)        }    }     private func applyCandidate(        _ candidate: Candidate,        context: ImportContext,        deadline: Date?,        log: @escaping (String) -> Void,        diagnostics: inout ImportDiagnostics) async throws -> ImportResult?    {        switch try await self.evaluateCandidate(            candidate,            targetEmail: context.targetEmail,            allowAnyAccount: context.allowAnyAccount,            deadline: deadline,            log: log)        {        case let .match(candidate, signedInEmail):            log("Selected \(candidate.label) (matches Codex: \(signedInEmail))")            guard let targetEmail = context.targetEmail else { return nil }            if let result = try? await self.persistVerifiedCandidate(                candidate: candidate,                targetEmail: targetEmail,                cacheScope: context.cacheScope,                deadline: deadline,                logger: log)            {                await self.cacheCookies(                    candidate: candidate,                    scope: context.cacheScope,                    deadline: deadline,                    logger: log)                return result            }            _ = try Self.remainingTimeout(until: deadline)            return nil        case let .mismatch(candidate, signedInEmail):            try await self.handleMismatch(                candidate: candidate,                signedInEmail: signedInEmail,                context: context,                log: log,                diagnostics: &diagnostics)            _ = try Self.remainingTimeout(until: deadline)            return nil        case let .loggedIn(candidate, signedInEmail):            log("Selected \(candidate.label) (signed in: \(signedInEmail))")            if let result = try? await self.persistVerifiedCandidate(                candidate: candidate,                targetEmail: signedInEmail,                cacheScope: context.cacheScope,                deadline: deadline,                logger: log)            {                await self.cacheCookies(                    candidate: candidate,                    scope: context.cacheScope,                    deadline: deadline,                    logger: log)                return result            }            _ = try Self.remainingTimeout(until: deadline)            return nil        case .unknown:            if context.allowAnyAccount {                log("Selected \(candidate.label) (signed in: unknown)")                if let result = try? await self.persistToDefaultStore(                    candidate: candidate,                    deadline: deadline,                    logger: log)                {                    await self.cacheCookies(                        candidate: candidate,                        scope: context.cacheScope,                        deadline: deadline,                        logger: log)                    return result                }                _ = try Self.remainingTimeout(until: deadline)                return nil            }            diagnostics.foundUnknownEmail = true            return nil        case .loginRequired:            return nil        }    }     private func evaluateCandidate(

Lines 1–500 of 974.