import Foundation#if os(macOS)import SweetCookieKit// MARK: - Abacus Cookie Importerpublic enum AbacusCookieImporter { private static let log = CodexBarLog.logger(LogCategories.abacusCookie) private static let cookieClient = BrowserCookieClient() private static let cookieDomains = ["abacus.ai", "apps.abacus.ai"] private static let cookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.abacus]?.browserCookieOrder ?? Browser.defaultImportOrder /// Exact cookie names known to carry Abacus session state. /// CSRF tokens are deliberately excluded — they are present in anonymous /// jars and do not indicate an authenticated session. private static let knownSessionCookieNames: Set<String> = [ "sessionid", "session_id", "session_token", "auth_token", "access_token", ] /// Substrings that indicate a session or auth cookie (applied only when /// no exact-name match is found). Deliberately excludes overly broad /// patterns like "id" and "token" that match analytics/CSRF cookies. private static let sessionCookieSubstrings = ["session", "auth", "sid", "jwt"] /// Cookie name prefixes that indicate a non-session cookie even when a /// substring match would otherwise accept it (e.g. "csrftoken"). private static let excludedCookiePrefixes = ["csrf", "_ga", "_gid", "tracking", "analytics"] public struct SessionInfo: Sendable { public let cookies: [HTTPCookie] public let sourceLabel: String public init(cookies: [HTTPCookie], sourceLabel: String) { self.cookies = cookies self.sourceLabel = sourceLabel } public var cookieHeader: String { self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") } } /// Returns all candidate sessions across browsers/profiles, ordered by /// import priority. Callers should try each in turn so that a stale /// session in the first source doesn't block a valid one further down. /// /// Defaults to Chrome-only per AGENTS.md guideline. Pass an empty /// `preferredBrowsers` list to fall back to the full descriptor-defined /// import order (Safari, Firefox, etc.) when Chrome has no cookies. public static func importSessions( browserDetection: BrowserDetection = BrowserDetection(), preferredBrowsers: [Browser] = [.chrome], logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { var candidates: [SessionInfo] = [] let installedBrowsers = preferredBrowsers.isEmpty ? self.cookieImportOrder.cookieImportCandidates(using: browserDetection) : preferredBrowsers.cookieImportCandidates(using: browserDetection) for browserSource in installedBrowsers { do { let query = BrowserCookieQuery(domains: self.cookieDomains) let sources = try Self.cookieClient.codexBarRecords( matching: query, in: browserSource, logger: { msg in self.emit(msg, logger: logger) }) for source in sources where !source.records.isEmpty { let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin) guard !httpCookies.isEmpty else { continue } guard Self.containsSessionCookie(httpCookies) else { self.emit( "Skipping \(source.label): no session cookie found", logger: logger) continue } self.emit( "Found \(httpCookies.count) session cookies in \(source.label)", logger: logger) candidates.append(SessionInfo(cookies: httpCookies, sourceLabel: source.label)) } } catch { BrowserCookieAccessGate.recordIfNeeded(error) self.emit( "\(browserSource.displayName) cookie import failed: \(error.localizedDescription)", logger: logger) } } guard !candidates.isEmpty else { throw AbacusUsageError.noSessionCookie } return candidates } /// Cheap check for whether any browser has an Abacus session cookie, /// used by the fetch strategy's `isAvailable()`. public static func hasSession( browserDetection: BrowserDetection = BrowserDetection(), preferredBrowsers: [Browser] = [.chrome], logger: ((String) -> Void)? = nil) -> Bool { do { return try !self.importSessions( browserDetection: browserDetection, preferredBrowsers: preferredBrowsers, logger: logger).isEmpty } catch { return false } } /// Returns `true` if the cookie set contains at least one cookie whose name /// indicates session or authentication state. Checks exact known names /// first, then falls back to conservative substring matching. private static func containsSessionCookie(_ cookies: [HTTPCookie]) -> Bool { cookies.contains { cookie in let lower = cookie.name.lowercased() if self.knownSessionCookieNames.contains(lower) { return true } if self.excludedCookiePrefixes.contains(where: { lower.hasPrefix($0) }) { return false } return self.sessionCookieSubstrings.contains { lower.contains($0) } } } private static func emit(_ message: String, logger: ((String) -> Void)?) { logger?("[abacus-cookie] \(message)") self.log.debug(message) }}#endif
import Foundation
#if os(macOS)
import SweetCookieKit
// MARK: - Abacus Cookie Importer
public enum AbacusCookieImporter {
private static let log = CodexBarLog.logger(LogCategories.abacusCookie)
private static let cookieClient = BrowserCookieClient()
private static let cookieDomains = ["abacus.ai", "apps.abacus.ai"]
private static let cookieImportOrder: BrowserCookieImportOrder =
ProviderDefaults.metadata[.abacus]?.browserCookieOrder ?? Browser.defaultImportOrder
/// Exact cookie names known to carry Abacus session state.
/// CSRF tokens are deliberately excluded — they are present in anonymous
/// jars and do not indicate an authenticated session.
private static let knownSessionCookieNames: Set<String> = [
"sessionid", "session_id", "session_token",
"auth_token", "access_token",
]
/// Substrings that indicate a session or auth cookie (applied only when
/// no exact-name match is found). Deliberately excludes overly broad
/// patterns like "id" and "token" that match analytics/CSRF cookies.
private static let sessionCookieSubstrings = ["session", "auth", "sid", "jwt"]
/// Cookie name prefixes that indicate a non-session cookie even when a
/// substring match would otherwise accept it (e.g. "csrftoken").
private static let excludedCookiePrefixes = ["csrf", "_ga", "_gid", "tracking", "analytics"]
public struct SessionInfo: Sendable {
public let cookies: [HTTPCookie]
public let sourceLabel: String
public init(cookies: [HTTPCookie], sourceLabel: String) {
self.cookies = cookies
self.sourceLabel = sourceLabel
}
public var cookieHeader: String {
self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ")
}
}
/// Returns all candidate sessions across browsers/profiles, ordered by
/// import priority. Callers should try each in turn so that a stale
/// session in the first source doesn't block a valid one further down.
///
/// Defaults to Chrome-only per AGENTS.md guideline. Pass an empty
/// `preferredBrowsers` list to fall back to the full descriptor-defined
/// import order (Safari, Firefox, etc.) when Chrome has no cookies.
public static func importSessions(
browserDetection: BrowserDetection = BrowserDetection(),
preferredBrowsers: [Browser] = [.chrome],
logger: ((String) -> Void)? = nil) throws -> [SessionInfo]
{
var candidates: [SessionInfo] = []
let installedBrowsers = preferredBrowsers.isEmpty
? self.cookieImportOrder.cookieImportCandidates(using: browserDetection)
: preferredBrowsers.cookieImportCandidates(using: browserDetection)
for browserSource in installedBrowsers {
do {
let query = BrowserCookieQuery(domains: self.cookieDomains)
let sources = try Self.cookieClient.codexBarRecords(
matching: query,
in: browserSource,
logger: { msg in self.emit(msg, logger: logger) })
for source in sources where !source.records.isEmpty {
let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
guard !httpCookies.isEmpty else { continue }
guard Self.containsSessionCookie(httpCookies) else {
self.emit(
"Skipping \(source.label): no session cookie found",
logger: logger)
continue
}
self.emit(
"Found \(httpCookies.count) session cookies in \(source.label)",
logger: logger)
candidates.append(SessionInfo(cookies: httpCookies, sourceLabel: source.label))
}
} catch {
BrowserCookieAccessGate.recordIfNeeded(error)
self.emit(
"\(browserSource.displayName) cookie import failed: \(error.localizedDescription)",
logger: logger)
}
}
guard !candidates.isEmpty else {
throw AbacusUsageError.noSessionCookie
}
return candidates
}
/// Cheap check for whether any browser has an Abacus session cookie,
/// used by the fetch strategy's `isAvailable()`.
public static func hasSession(
browserDetection: BrowserDetection = BrowserDetection(),
preferredBrowsers: [Browser] = [.chrome],
logger: ((String) -> Void)? = nil) -> Bool
{
do {
return try !self.importSessions(
browserDetection: browserDetection,
preferredBrowsers: preferredBrowsers,
logger: logger).isEmpty
} catch {
return false
}
}
/// Returns `true` if the cookie set contains at least one cookie whose name
/// indicates session or authentication state. Checks exact known names
/// first, then falls back to conservative substring matching.
private static func containsSessionCookie(_ cookies: [HTTPCookie]) -> Bool {
cookies.contains { cookie in
let lower = cookie.name.lowercased()
if self.knownSessionCookieNames.contains(lower) { return true }
if self.excludedCookiePrefixes.contains(where: { lower.hasPrefix($0) }) { return false }
return self.sessionCookieSubstrings.contains { lower.contains($0) }
}
}
private static func emit(_ message: String, logger: ((String) -> Void)?) {
logger?("[abacus-cookie] \(message)")
self.log.debug(message)
}
}
#endif