fancy new browser
1import Foundation
2import MereKit
3
4/// Bridges the cookie stores between the two engines when switching a tab.
5///
6/// The core problem: WKWebView and CEF maintain completely separate HTTP cookie
7/// stores. A user logged into GitHub in a WebKit tab will not be logged in when
8/// the same URL is opened in a Chromium tab.
9///
10/// This controller extracts cookies for a given URL from the source engine's
11/// store and injects them into the destination engine's store before navigation.
12///
13/// Limitations:
14/// - HttpOnly cookies set by servers are readable from WKHTTPCookieStore but
15/// may not be extractable from CEF's cookie manager depending on CEF version.
16/// - Secure cookies are transferred in-process (no network exposure), which is safe.
17/// - Session cookies are transferred but may expire immediately if the destination
18/// engine's session handling differs.
19@MainActor
20public final class CookieSyncController {
21
22 private let webkit: any BrowserContext
23 private let chromium: (any BrowserContext)?
24
25 public init(webkit: any BrowserContext, chromium: (any BrowserContext)?) {
26 self.webkit = webkit
27 self.chromium = chromium
28 }
29
30 /// Copy cookies for `url` from `sourceEngine` into the other engine's store.
31 public func sync(from sourceEngine: EngineType, url: URL) async {
32 switch sourceEngine {
33 case .webkit:
34 guard let chromium else { return }
35 let cookies = await webkit.cookies(for: url)
36 await chromium.setCookies(cookies, for: url)
37
38 case .chromium:
39 guard let chromium else { return }
40 let cookies = await chromium.cookies(for: url)
41 await webkit.setCookies(cookies, for: url)
42 }
43 }
44
45 /// Full bidirectional sync for all cookies on a domain.
46 /// Call this periodically if keeping both engines logged in simultaneously.
47 public func fullSync(url: URL) async {
48 guard let chromium else { return }
49 let webkitCookies = await webkit.cookies(for: url)
50 let chromiumCookies = await chromium.cookies(for: url)
51
52 // Merge: newest cookie wins on conflict
53 let merged = merge(webkitCookies, chromiumCookies)
54 await webkit.setCookies(merged, for: url)
55 await chromium.setCookies(merged, for: url)
56 }
57
58 private func merge(_ a: [HTTPCookie], _ b: [HTTPCookie]) -> [HTTPCookie] {
59 var result: [String: HTTPCookie] = [:]
60 for cookie in a + b {
61 let key = "\(cookie.domain)|\(cookie.path)|\(cookie.name)"
62 if let existing = result[key] {
63 // Keep the one with a later expiry, or b if equal
64 if let expA = existing.expiresDate, let expB = cookie.expiresDate, expB > expA {
65 result[key] = cookie
66 } else if existing.expiresDate == nil {
67 result[key] = cookie
68 }
69 } else {
70 result[key] = cookie
71 }
72 }
73 return Array(result.values)
74 }
75}