118 lines · 4.7 KB · Swift
RawDownload
#if os(macOS)import CryptoKitimport Foundationimport WebKit /// Per-account persistent `WKWebsiteDataStore` for the OpenAI dashboard scrape.////// Why: `WKWebsiteDataStore.default()` is a single shared cookie jar. If the user switches Codex accounts,/// we want to keep multiple signed-in dashboard sessions around (one per email) without clearing cookies.////// Implementation detail: macOS 14+ supports `WKWebsiteDataStore.dataStore(forIdentifier:)`, which creates/// persistent isolated stores keyed by an identifier. We derive a stable UUID from the email and optional/// Codex source scope so distinct profiles with the same email never share a cookie store.////// Important: We cache the `WKWebsiteDataStore` instances so the same object is returned for the same/// account email. This ensures `OpenAIDashboardWebViewCache` can use object identity for cache lookups.@MainActorpublic enum OpenAIDashboardWebsiteDataStore {    /// Cached data store instances keyed by normalized email and optional Codex source scope.    /// Using the same instance ensures stable object identity for WebView cache lookups.    private static var cachedStores: [String: WKWebsiteDataStore] = [:]     public static func store(        forAccountEmail email: String?,        scope: CookieHeaderCache.Scope? = nil) -> WKWebsiteDataStore    {        guard let normalized = normalizeEmail(email) else { return .default() }        let storageKey = self.storageKey(normalizedEmail: normalized, scope: scope)         // Return cached instance if available to maintain stable object identity        if let cached = cachedStores[storageKey] {            return cached        }         let id = Self.identifier(forStorageKey: storageKey)        let store = WKWebsiteDataStore(forIdentifier: id)        self.cachedStores[storageKey] = store        return store    }     /// Clears the persistent cookie store for a single account email.    ///    /// Note: this does *not* impact other accounts, and is safe to use when the stored session is "stuck"    /// or signed in to a different account than expected.    public static func clearStore(        forAccountEmail email: String?,        scope: CookieHeaderCache.Scope? = nil) async    {        // Clear only ChatGPT/OpenAI domain data for the per-account store.        // Avoid deleting the entire persistent store (WebKit requires all WKWebViews using it to be released).        let store = self.store(forAccountEmail: email, scope: scope)        await withCheckedContinuation { cont 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) {                    cont.resume()                }            }        }         // Remove from cache so a fresh instance is created on next access        if let normalized = normalizeEmail(email) {            self.cachedStores.removeValue(forKey: self.storageKey(normalizedEmail: normalized, scope: scope))        }    }     #if DEBUG    /// Clear all cached store instances (for test isolation).    public static func clearCacheForTesting() {        self.cachedStores.removeAll()    }     #endif     // MARK: - Private     private static func normalizeEmail(_ email: String?) -> String? {        guard let raw = email?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil }        return raw.lowercased()    }     private static func storageKey(normalizedEmail: String, scope: CookieHeaderCache.Scope?) -> String {        guard let scope else { return normalizedEmail }        return "\(normalizedEmail)|\(scope.isolationIdentifier)"    }     private static func identifier(forStorageKey storageKey: String) -> UUID {        let digest = SHA256.hash(data: Data(storageKey.utf8))        var bytes = Array(digest.prefix(16))         // Make it a well-formed UUID (v4 + RFC4122 variant) while staying deterministic.        bytes[6] = (bytes[6] & 0x0F) | 0x40        bytes[8] = (bytes[8] & 0x3F) | 0x80         let uuidBytes: uuid_t = (            bytes[0],            bytes[1],            bytes[2],            bytes[3],            bytes[4],            bytes[5],            bytes[6],            bytes[7],            bytes[8],            bytes[9],            bytes[10],            bytes[11],            bytes[12],            bytes[13],            bytes[14],            bytes[15])        return UUID(uuid: uuidBytes)    }}#endif