#if os(macOS) import Foundation import SweetCookieKit import WebKit @MainActor // swiftlint:disable:next type_body_length public 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 browserCookieLoadTimedOut(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 let .browserCookieLoadTimedOut(details): return "Browser cookie loading timed out. \(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") } for browser in Self.cookieImportOrder { guard let issue = self.browserDetection.cookieSourceProfileAccessIssue(browser) else { continue } let hint = Self.browserProfileAccessHint(for: browser, issue: issue) diagnostics.accessDeniedHints.append(hint) log(hint) } // 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 = Array(Set(diagnostics.accessDeniedHints)).sorted().joined(separator: " ") log("Cookie access denied: \(details)") throw ImportError.browserAccessDenied(details: details) } throw ImportError.noCookiesFound } nonisolated static func browserProfileAccessHint( for browser: Browser, issue: BrowserProfileAccessIssue, processName: String = ProcessInfo.processInfo.processName, executablePath: String? = Bundle.main.executablePath) -> String { let failure = switch issue { case .accessDenied: "macOS denied" case .unreadable: "macOS could not read" } let executable = executablePath?.trimmingCharacters(in: .whitespacesAndNewlines) let target: String = if let executable, !executable.isEmpty { Self.fullDiskAccessTarget(processName: processName, executablePath: executable) } else { processName } return "\(failure) \(browser.displayName) profile access for \(target). " + "Grant that exact component Full Disk Access, then quit and reopen it." } private nonisolated static func fullDiskAccessTarget(processName: String, executablePath: String) -> String { guard processName != "CodexBarCLI", let appSuffix = executablePath.range(of: ".app/") else { return "\(processName) (\(executablePath))" } let appPath = executablePath[.. ImportError { let executable = executablePath?.trimmingCharacters(in: .whitespacesAndNewlines) let target: String = if let executable, !executable.isEmpty { Self.fullDiskAccessTarget(processName: processName, executablePath: executable) } else { processName } let keychainHint = browser.usesKeychainForCookieDecryption ? " If a macOS Keychain prompt is waiting, approve it." : "" let details = "\(browser.displayName) did not finish before the web timeout for \(target)." + keychainHint + " If the timeout repeats, grant that exact component Full Disk Access, quit and reopen it, then retry." return .browserCookieLoadTimedOut(details: details) } 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: [BrowserCookieStoreRecords] do { sources = try await Self.runBoundedCookieLoad(deadline: deadline) { try Self.cookieClient.codexBarRecords(matching: query, in: .safari) } } catch let error as URLError where error.code == .timedOut { let timeoutError = Self.browserCookieLoadTimeoutError(for: .safari) log(timeoutError.localizedDescription) throw timeoutError } _ = 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 ImportError { throw error } 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: [BrowserCookieStoreRecords] do { sources = try await Self.runBoundedCookieLoad(deadline: deadline) { try Self.cookieClient.codexBarRecords(matching: query, in: browser) } } catch let error as URLError where error.code == .timedOut { let timeoutError = Self.browserCookieLoadTimeoutError(for: browser) log(timeoutError.localizedDescription) throw timeoutError } _ = 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 ImportError { throw error } 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( _ candidate: Candidate, targetEmail: String?, allowAnyAccount: Bool, deadline: Date?, log: @escaping (String) -> Void) async throws -> CandidateEvaluation { log("Trying candidate \(candidate.label) (\(candidate.cookies.count) cookies)") let apiEmail = try await self.fetchSignedInEmailFromAPI( cookies: candidate.cookies, deadline: deadline, logger: log) if let apiEmail { log("Candidate \(candidate.label) API email: \(apiEmail)") } // Prefer the API email when available (fast; avoids WebKit hydration/timeout risks). if let apiEmail, !apiEmail.isEmpty { if let targetEmail { if apiEmail.lowercased() == targetEmail.lowercased() { return .match(candidate: candidate, signedInEmail: apiEmail) } return .mismatch(candidate: candidate, signedInEmail: apiEmail) } if allowAnyAccount { return .loggedIn(candidate: candidate, signedInEmail: apiEmail) } } if !self.hasSessionCookies(candidate.cookies) { log("Candidate \(candidate.label) missing session cookies; skipping") return .loginRequired(candidate: candidate) } let scratch = WKWebsiteDataStore.nonPersistent() try await self.setCookies(candidate.cookies, into: scratch, deadline: deadline) do { let probe = try await OpenAIDashboardFetcher().probeUsagePage( websiteDataStore: scratch, logger: log, timeout: Self.remainingTimeout(until: deadline, cappedAt: 25)) let signedInEmail = probe.signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines) log("Candidate \(candidate.label) DOM email: \(signedInEmail ?? "unknown")") let resolvedEmail = signedInEmail?.trimmingCharacters(in: .whitespacesAndNewlines) if let resolvedEmail, !resolvedEmail.isEmpty { if let targetEmail { if resolvedEmail.lowercased() == targetEmail.lowercased() { return .match(candidate: candidate, signedInEmail: resolvedEmail) } return .mismatch(candidate: candidate, signedInEmail: resolvedEmail) } if allowAnyAccount { return .loggedIn(candidate: candidate, signedInEmail: resolvedEmail) } } return .unknown(candidate: candidate) } catch OpenAIDashboardFetcher.FetchError.loginRequired { log("Candidate \(candidate.label) requires login.") return .loginRequired(candidate: candidate) } catch { _ = try Self.remainingTimeout(until: deadline) log("Candidate \(candidate.label) probe error: \(error.localizedDescription)") return .unknown(candidate: candidate) } } private func hasSessionCookies(_ cookies: [HTTPCookie]) -> Bool { for cookie in cookies { let name = cookie.name.lowercased() if name.contains("session-token") || name.contains("authjs") || name.contains("next-auth") { return true } if name == "_account" { return true } } return false } private func handleMismatch( candidate: Candidate, signedInEmail: String, context: ImportContext, log: @escaping (String) -> Void, diagnostics: inout ImportDiagnostics) async throws { log("Candidate \(candidate.label) mismatch (\(signedInEmail)); continuing browser search") diagnostics.mismatches.append(FoundAccount(sourceLabel: candidate.label, email: signedInEmail)) // Mismatch still means we found a valid signed-in session. Keep it inside the active source scope; // a profile import must never populate the live account or another profile's WebKit store. do { try await self.persistCookies( candidate: candidate, accountEmail: signedInEmail, cacheScope: context.cacheScope, deadline: context.deadline, logger: log) } catch { log("Could not cache mismatched session: \(error.localizedDescription)") _ = try Self.remainingTimeout(until: context.deadline) } } func fetchSignedInEmailFromAPI( cookies: [HTTPCookie], deadline: Date?, logger: (String) -> Void) async throws -> String? { let chatgptCookies = cookies.filter { $0.domain.lowercased().contains("chatgpt.com") } guard !chatgptCookies.isEmpty else { return nil } let cookieHeader = chatgptCookies .map { "\($0.name)=\($0.value)" } .joined(separator: "; ") let endpoints = [ "https://chatgpt.com/backend-api/me", "https://chatgpt.com/api/auth/session", ] for urlString in endpoints { let requestTimeout = try Self.remainingTimeout(until: deadline, cappedAt: 10) guard let url = URL(string: urlString) else { continue } let request = OpenAIDashboardFetcher.dashboardIdentityAPIRequest( url: url, cookieHeader: cookieHeader, timeout: requestTimeout) do { let (data, response) = try await CodexAuthenticatedHTTPTransport.current.data(for: request) let status = (response as? HTTPURLResponse)?.statusCode ?? -1 logger("API \(url.host ?? "chatgpt.com") \(url.path) status=\(status)") guard status >= 200, status < 300 else { continue } if let email = Self.findFirstEmail(inJSONData: data) { return email.trimmingCharacters(in: .whitespacesAndNewlines) } } catch { _ = try Self.remainingTimeout(until: deadline) logger("API request failed: \(error.localizedDescription)") } } return nil } private static func findFirstEmail(inJSONData data: Data) -> String? { guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil } var queue: [Any] = [json] var seen = 0 while !queue.isEmpty, seen < 2000 { let cur = queue.removeFirst() seen += 1 if let str = cur as? String, str.contains("@") { return str } if let dict = cur as? [String: Any] { for (k, v) in dict { if k.lowercased() == "email", let s = v as? String, s.contains("@") { return s } queue.append(v) } } else if let arr = cur as? [Any] { queue.append(contentsOf: arr) } } return nil } private func persistVerifiedCandidate( candidate: Candidate, targetEmail: String, cacheScope: CookieHeaderCache.Scope?, deadline: Date?, logger: @escaping (String) -> Void) async throws -> ImportResult { do { return try await self.persist( candidate: candidate, targetEmail: targetEmail, cacheScope: cacheScope, deadline: deadline, logger: logger) } catch { guard Self.shouldTrustVerifiedSession(afterPersistFailure: error) else { throw error } let signedInEmail = targetEmail.trimmingCharacters(in: .whitespacesAndNewlines) logger( "Persistent validation timed out after session verification; " + "keeping \(candidate.label) cookies for \(signedInEmail).") return ImportResult( sourceLabel: candidate.label, cookieCount: candidate.cookies.count, signedInEmail: signedInEmail, matchesCodexEmail: signedInEmail.lowercased() == targetEmail.lowercased()) } } private func persist( candidate: Candidate, targetEmail: String, cacheScope: CookieHeaderCache.Scope?, deadline: Date?, logger: @escaping (String) -> Void) async throws -> ImportResult { let persistent = OpenAIDashboardWebsiteDataStore.store( forAccountEmail: targetEmail, scope: cacheScope) try await self.clearChatGPTCookies(in: persistent, deadline: deadline) try await self.setCookies(candidate.cookies, into: persistent, deadline: deadline) // Validate against the persistent store (login + email sync). do { let probe = try await OpenAIDashboardFetcher().probeUsagePage( websiteDataStore: persistent, logger: logger, timeout: Self.remainingTimeout(until: deadline, cappedAt: 20), preserveLoadedPageForReuse: true) let signed = probe.signedInEmail?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let matches = signed?.lowercased() == targetEmail.lowercased() logger("Persistent session signed in as: \(signed ?? "unknown")") if signed != nil, matches == false { let found = signed?.isEmpty == false ? [FoundAccount(sourceLabel: candidate.label, email: signed!)] : [] throw ImportError.noMatchingAccount(found: found) } return ImportResult( sourceLabel: candidate.label, cookieCount: candidate.cookies.count, signedInEmail: signed, matchesCodexEmail: matches) } catch OpenAIDashboardFetcher.FetchError.loginRequired { OpenAIDashboardWebViewCache.shared.evict(websiteDataStore: persistent) logger("Selected \(candidate.label) but dashboard still requires login.") throw ImportError.dashboardStillRequiresLogin } catch { OpenAIDashboardWebViewCache.shared.evict(websiteDataStore: persistent) throw Self.persistentValidationFailure(error) } } private func persistToDefaultStore( candidate: Candidate, deadline: Date?, logger: @escaping (String) -> Void) async throws -> ImportResult { let persistent = OpenAIDashboardWebsiteDataStore.store(forAccountEmail: nil) try await self.clearChatGPTCookies(in: persistent, deadline: deadline) try await self.setCookies(candidate.cookies, into: persistent, deadline: deadline) do { let probe = try await OpenAIDashboardFetcher().probeUsagePage( websiteDataStore: persistent, logger: logger, timeout: Self.remainingTimeout(until: deadline, cappedAt: 20), preserveLoadedPageForReuse: true) let signed = probe.signedInEmail?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) logger("Persistent session signed in as: \(signed ?? "unknown")") return ImportResult( sourceLabel: candidate.label, cookieCount: candidate.cookies.count, signedInEmail: signed, matchesCodexEmail: false) } catch OpenAIDashboardFetcher.FetchError.loginRequired { OpenAIDashboardWebViewCache.shared.evict(websiteDataStore: persistent) logger("Selected \(candidate.label) but dashboard still requires login.") throw ImportError.dashboardStillRequiresLogin } catch { OpenAIDashboardWebViewCache.shared.evict(websiteDataStore: persistent) throw error } } // MARK: - Candidates private func cookies(from pairs: [(name: String, value: String)]) -> [HTTPCookie] { var cookies: [HTTPCookie] = [] for domain in Self.cookieDomains { for pair in pairs { let props: [HTTPCookiePropertyKey: Any] = [ .name: pair.name, .value: pair.value, .domain: domain, .path: "/", .secure: true, ] if let cookie = HTTPCookie(properties: props) { cookies.append(cookie) } } } return cookies } private func cacheCookies( candidate: Candidate, scope: CookieHeaderCache.Scope?, deadline: Date?, logger: @escaping (String) -> Void) async { let header = self.cookieHeader(from: candidate.cookies) guard !header.isEmpty else { return } do { _ = try await Self.runBoundedCookieCacheOperation(deadline: deadline) { CookieHeaderCache.store( provider: .codex, scope: scope, cookieHeader: header, sourceLabel: candidate.label) return true } } catch { logger("Cookie cache write did not finish before the web deadline.") } } private func cookieHeader(from cookies: [HTTPCookie]) -> String { cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") } private struct Candidate { let label: String let cookies: [HTTPCookie] } // MARK: - WebKit cookie store private func persistCookies( candidate: Candidate, accountEmail: String, cacheScope: CookieHeaderCache.Scope?, deadline: Date?, logger: (String) -> Void) async throws { let store = OpenAIDashboardWebsiteDataStore.store(forAccountEmail: accountEmail, scope: cacheScope) try await self.clearChatGPTCookies(in: store, deadline: deadline) try await self.setCookies(candidate.cookies, into: store, deadline: deadline) logger("Persisted cookies for \(accountEmail) (source=\(candidate.label))") } private func clearChatGPTCookies(in store: WKWebsiteDataStore, deadline: Date?) async throws { try await Self.runSerializedCallback( key: ObjectIdentifier(store), deadline: deadline) { completion in store.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in let filtered = records.filter { record in let name = record.displayName.lowercased() return name.contains("chatgpt.com") || name.contains("openai.com") } store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: filtered) { completion() } } } } private func setCookies( _ cookies: [HTTPCookie], into store: WKWebsiteDataStore, deadline: Date?) async throws { for cookie in cookies { try await Self.runSerializedCallback( key: ObjectIdentifier(store), deadline: deadline) { completion in store.httpCookieStore.setCookie(cookie, completionHandler: completion) } } } private func cookieSummary(_ cookies: [HTTPCookie]) -> String { let nameCounts = Dictionary(grouping: cookies, by: \.name).mapValues { $0.count } let important = [ "__Secure-next-auth.session-token", "__Secure-next-auth.session-token.0", "__Secure-next-auth.session-token.1", "_account", "oai-did", "cf_clearance", ] let parts: [String] = important.compactMap { name -> String? in guard let c = nameCounts[name], c > 0 else { return nil } return "\(name)=\(c)" } if parts.isEmpty { return "no key cookies detected" } return parts.joined(separator: ", ") } } #else import Foundation @MainActor public 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 browserCookieLoadTimedOut(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 let .browserCookieLoadTimedOut(details): return "Browser cookie loading timed out. \(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() {} public func importBestCookies( intoAccountEmail _: String?, allowAnyAccount _: Bool = false, preferCachedCookieHeader _: Bool = true, cacheScope _: CookieHeaderCache.Scope? = nil, deadline _: Date? = nil, logger _: ((String) -> Void)? = nil) async throws -> ImportResult { throw ImportError.browserAccessDenied(details: "OpenAI web cookie import is only supported on macOS.") } public func importManualCookies( cookieHeader _: String, intoAccountEmail _: String?, allowAnyAccount _: Bool = false, cacheScope _: CookieHeaderCache.Scope? = nil, deadline _: Date? = nil, logger _: ((String) -> Void)? = nil) async throws -> ImportResult { throw ImportError.browserAccessDenied(details: "OpenAI web cookie import is only supported on macOS.") } } #endif