import Foundationimport SweetCookieKit#if canImport(FoundationNetworking)import FoundationNetworking#endif /// Fetches Claude usage data directly from the claude.ai API using browser session cookies.////// This approach mirrors what Claude Usage Tracker does, but automatically extracts the session key/// from browser cookies instead of requiring manual setup.////// API endpoints used:/// - `GET https://claude.ai/api/organizations` → get org UUID/// - `GET https://claude.ai/api/organizations/{org_id}/usage` → usage percentages + reset timespublic enum ClaudeWebAPIFetcher { private static let baseURL = "https://claude.ai/api" private static let maxProbeBytes = 200_000 #if os(macOS) private static let cookieClient = BrowserCookieClient() private static let cookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.claude]?.browserCookieOrder ?? Browser.defaultImportOrder #else private static let cookieImportOrder: BrowserCookieImportOrder = [] #endif public struct OrganizationInfo: Sendable { public let id: String public let name: String? public init(id: String, name: String?) { self.id = id self.name = name } } public struct SessionKeyInfo: Sendable { public let key: String public let sourceLabel: String public let cookieCount: Int public init(key: String, sourceLabel: String, cookieCount: Int) { self.key = key self.sourceLabel = sourceLabel self.cookieCount = cookieCount } } public enum FetchError: LocalizedError, Sendable { case noSessionKeyFound case invalidSessionKey case notSupportedOnThisPlatform case networkError(Error) case invalidResponse case unauthorized case serverError(statusCode: Int) case noOrganization case organizationNotFound(String) public var errorDescription: String? { switch self { case .noSessionKeyFound: "No Claude session key found in browser cookies." case .invalidSessionKey: "Invalid Claude session key format." case .notSupportedOnThisPlatform: "Claude web fetching is only supported on macOS." case let .networkError(error): "Network error: \(error.localizedDescription)" case .invalidResponse: "Invalid response from Claude API." case .unauthorized: "Sign in to claude.ai (or refresh Claude cookies) to load usage data." case let .serverError(code): "Claude API error: HTTP \(code)" case .noOrganization: "No Claude organization found for this account." case let .organizationNotFound(id): "Claude organization '\(id)' was not found for this session." } } } /// Claude usage data from the API public struct WebUsageData: Sendable { public let sessionPercentUsed: Double public let sessionResetsAt: Date? public let weeklyPercentUsed: Double? public let weeklyResetsAt: Date? public let opusPercentUsed: Double? public let extraRateWindows: [NamedRateWindow] public let extraUsageCost: ProviderCostSnapshot? public let accountOrganization: String? public let accountEmail: String? public let loginMethod: String? public init( sessionPercentUsed: Double, sessionResetsAt: Date?, weeklyPercentUsed: Double?, weeklyResetsAt: Date?, opusPercentUsed: Double?, extraRateWindows: [NamedRateWindow], extraUsageCost: ProviderCostSnapshot?, accountOrganization: String?, accountEmail: String?, loginMethod: String?) { self.sessionPercentUsed = sessionPercentUsed self.sessionResetsAt = sessionResetsAt self.weeklyPercentUsed = weeklyPercentUsed self.weeklyResetsAt = weeklyResetsAt self.opusPercentUsed = opusPercentUsed self.extraRateWindows = extraRateWindows self.extraUsageCost = extraUsageCost self.accountOrganization = accountOrganization self.accountEmail = accountEmail self.loginMethod = loginMethod } } public struct ProbeResult: Sendable { public let url: String public let statusCode: Int? public let contentType: String? public let topLevelKeys: [String] public let emails: [String] public let planHints: [String] public let notableFields: [String] public let bodyPreview: String? } // MARK: - Public API #if os(macOS) /// Attempts to fetch Claude usage data using cookies extracted from browsers. /// Tries browser cookies using the standard import order. public static func fetchUsage( browserDetection: BrowserDetection, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?("[claude-web] \(msg)") } if let cached = CookieHeaderCache.load(provider: .claude), !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { log("Using cached cookie header from \(cached.sourceLabel)") do { return try await self.fetchUsage( cookieHeader: cached.cookieHeader, targetOrganizationID: targetOrganizationID, logger: log) } catch let error as FetchError { switch error { case .unauthorized, .noSessionKeyFound, .invalidSessionKey: CookieHeaderCache.clear(provider: .claude) default: throw error } } catch { throw error } } let sessionInfo = try extractSessionKeyInfo(browserDetection: browserDetection, logger: log) log("Found session key (\(sessionInfo.cookieCount) cookies)") let usage = try await self.fetchUsage( using: sessionInfo, targetOrganizationID: targetOrganizationID, logger: log) CookieHeaderCache.store( provider: .claude, cookieHeader: "sessionKey=\(sessionInfo.key)", sourceLabel: sessionInfo.sourceLabel) return usage } public static func fetchUsage( cookieHeader: String, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?("[claude-web] \(msg)") } let sessionInfo = try self.sessionKeyInfo(cookieHeader: cookieHeader) log("Using manual session key (\(sessionInfo.cookieCount) cookies)") return try await self.fetchUsage( using: sessionInfo, targetOrganizationID: targetOrganizationID, logger: log) } public static func fetchUsage( using sessionKeyInfo: SessionKeyInfo, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?(msg) } let sessionKey = sessionKeyInfo.key // Fetch organization info let organization = try await fetchOrganizationInfo( sessionKey: sessionKey, targetOrganizationID: targetOrganizationID, logger: log) log("Organization resolved") var usage = try await fetchUsageData(orgId: organization.id, sessionKey: sessionKey, logger: log) if usage.extraUsageCost == nil, let extra = await ClaudeWebExtraUsageCost.fetch( baseURL: Self.baseURL, orgId: organization.id, sessionKey: sessionKey, logger: log) { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: extra, accountOrganization: usage.accountOrganization, accountEmail: usage.accountEmail, loginMethod: usage.loginMethod) } if let account = await fetchAccountInfo(sessionKey: sessionKey, orgId: organization.id, logger: log) { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: usage.extraUsageCost, accountOrganization: usage.accountOrganization, accountEmail: account.email, loginMethod: account.loginMethod) } if usage.accountOrganization == nil, let name = organization.name { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: usage.extraUsageCost, accountOrganization: name, accountEmail: usage.accountEmail, loginMethod: usage.loginMethod) } return usage } /// Probes a list of endpoints using the current claude.ai session cookies. /// - Parameters: /// - endpoints: Absolute URLs or "/api/..." paths. Supports "{orgId}" placeholder. /// - includePreview: When true, includes a truncated response preview in results. public static func probeEndpoints( _ endpoints: [String], browserDetection: BrowserDetection, includePreview: Bool = false, logger: ((String) -> Void)? = nil) async throws -> [ProbeResult] { let log: (String) -> Void = { msg in logger?("[claude-probe] \(msg)") } let sessionInfo = try extractSessionKeyInfo(browserDetection: browserDetection, logger: log) let sessionKey = sessionInfo.key let organization = try? await fetchOrganizationInfo(sessionKey: sessionKey, logger: log) let expanded = endpoints.map { endpoint -> String in var url = endpoint if let orgId = organization?.id { url = url.replacingOccurrences(of: "{orgId}", with: orgId) } if url.hasPrefix("/") { url = "https://claude.ai\(url)" } return url } var results: [ProbeResult] = [] results.reserveCapacity(expanded.count) for endpoint in expanded { guard let url = URL(string: endpoint) else { continue } var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json, text/html;q=0.9, */*;q=0.8", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 20 do { let (data, response) = try await ProviderHTTPClient.shared.data(for: request) let http = response as? HTTPURLResponse let contentType = http?.allHeaderFields["Content-Type"] as? String let truncated = data.prefix(Self.maxProbeBytes) let body = String(data: truncated, encoding: .utf8) ?? "" let parsed = Self.parseProbeBody(data: data, fallbackText: body, contentType: contentType) let preview = includePreview ? parsed.preview : nil results.append(ProbeResult( url: endpoint, statusCode: http?.statusCode, contentType: contentType, topLevelKeys: parsed.keys, emails: parsed.emails, planHints: parsed.planHints, notableFields: parsed.notableFields, bodyPreview: preview)) } catch { results.append(ProbeResult( url: endpoint, statusCode: nil, contentType: nil, topLevelKeys: [], emails: [], planHints: [], notableFields: [], bodyPreview: "Error: \(error.localizedDescription)")) } } return results } /// Checks if we can find a Claude session key in browser cookies without making API calls. public static func hasSessionKey(browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) -> Bool { if let cached = CookieHeaderCache.load(provider: .claude), self.hasSessionKey(cookieHeader: cached.cookieHeader) { return true } do { _ = try self.sessionKeyInfo(browserDetection: browserDetection, logger: logger) return true } catch { return false } } public static func hasSessionKey(cookieHeader: String?) -> Bool { guard let cookieHeader else { return false } return (try? self.sessionKeyInfo(cookieHeader: cookieHeader)) != nil } public static func sessionKeyInfo( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo { try self.extractSessionKeyInfo(browserDetection: browserDetection, logger: logger) } public static func sessionKeyInfo(cookieHeader: String) throws -> SessionKeyInfo { let pairs = CookieHeaderNormalizer.pairs(from: cookieHeader) if let sessionKey = self.findSessionKey(in: pairs) { return SessionKeyInfo( key: sessionKey, sourceLabel: "Manual", cookieCount: pairs.count) } throw FetchError.noSessionKeyFound } // MARK: - Session Key Extraction private static func extractSessionKeyInfo( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo { let log: (String) -> Void = { msg in logger?(msg) } let cookieDomains = ["claude.ai"] // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts let installedBrowsers = Self.cookieImportOrder.cookieImportCandidates(using: browserDetection) for browserSource in installedBrowsers { do { let query = BrowserCookieQuery(domains: cookieDomains) let sources = try Self.cookieClient.codexBarRecords( matching: query, in: browserSource, logger: log) for source in sources { if let sessionKey = findSessionKey(in: source.records.map { record in (name: record.name, value: record.value) }) { log("Found sessionKey in \(source.label)") return SessionKeyInfo( key: sessionKey, sourceLabel: source.label, cookieCount: source.records.count) } } } catch { BrowserCookieAccessGate.recordIfNeeded(error) log("\(browserSource.displayName) cookie load failed: \(error.localizedDescription)") } } throw FetchError.noSessionKeyFound } private static func findSessionKey(in cookies: [(name: String, value: String)]) -> String? { for cookie in cookies where cookie.name == "sessionKey" { let value = cookie.value.trimmingCharacters(in: .whitespacesAndNewlines) // Validate it looks like a Claude session key if value.hasPrefix("sk-ant-") { return value } } return nil } // MARK: - API Calls private static func fetchOrganizationInfo( sessionKey: String, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> OrganizationInfo { let url = URL(string: "\(baseURL)/organizations")! var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 15 let (data, response) = try await ProviderHTTPClient.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw FetchError.invalidResponse } logger?("Organizations API status: \(httpResponse.statusCode)") switch httpResponse.statusCode { case 200: return try self.parseOrganizationResponse(data, targetOrganizationID: targetOrganizationID) case 401, 403: throw FetchError.unauthorized default: throw FetchError.serverError(statusCode: httpResponse.statusCode) } } private static func fetchUsageData( orgId: String, sessionKey: String, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let url = URL(string: "\(baseURL)/organizations/\(orgId)/usage")! var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 15 let (data, response) = try await ProviderHTTPClient.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw FetchError.invalidResponse } logger?("Usage API status: \(httpResponse.statusCode)") switch httpResponse.statusCode { case 200: return try self.parseUsageResponse(data, logger: logger) case 401, 403: throw FetchError.unauthorized default: throw FetchError.serverError(statusCode: httpResponse.statusCode) } } private static func parseUsageResponse(_ data: Data, logger: ((String) -> Void)? = nil) throws -> WebUsageData { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw FetchError.invalidResponse } // Parse five_hour (session) usage var sessionPercent: Double? var sessionResets: Date? if let fiveHour = json["five_hour"] as? [String: Any] { if let utilization = fiveHour["utilization"] as? Int { sessionPercent = Double(utilization) } if let resetsAt = fiveHour["resets_at"] as? String { sessionResets = self.parseISO8601Date(resetsAt) } } // Enterprise/credit-based accounts return null for five_hour; treat as 0% rather than an error. let resolvedSessionPercent = sessionPercent ?? 0.0 // Parse seven_day (weekly) usage var weeklyPercent: Double? var weeklyResets: Date?Lines 1–500 of 993. Load more
import Foundation import SweetCookieKit #if canImport(FoundationNetworking) import FoundationNetworking #endif /// Fetches Claude usage data directly from the claude.ai API using browser session cookies. /// /// This approach mirrors what Claude Usage Tracker does, but automatically extracts the session key /// from browser cookies instead of requiring manual setup. /// /// API endpoints used: /// - `GET https://claude.ai/api/organizations` → get org UUID /// - `GET https://claude.ai/api/organizations/{org_id}/usage` → usage percentages + reset times public enum ClaudeWebAPIFetcher { private static let baseURL = "https://claude.ai/api" private static let maxProbeBytes = 200_000 #if os(macOS) private static let cookieClient = BrowserCookieClient() private static let cookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.claude]?.browserCookieOrder ?? Browser.defaultImportOrder #else private static let cookieImportOrder: BrowserCookieImportOrder = [] #endif public struct OrganizationInfo: Sendable { public let id: String public let name: String? public init(id: String, name: String?) { self.id = id self.name = name } } public struct SessionKeyInfo: Sendable { public let key: String public let sourceLabel: String public let cookieCount: Int public init(key: String, sourceLabel: String, cookieCount: Int) { self.key = key self.sourceLabel = sourceLabel self.cookieCount = cookieCount } } public enum FetchError: LocalizedError, Sendable { case noSessionKeyFound case invalidSessionKey case notSupportedOnThisPlatform case networkError(Error) case invalidResponse case unauthorized case serverError(statusCode: Int) case noOrganization case organizationNotFound(String) public var errorDescription: String? { switch self { case .noSessionKeyFound: "No Claude session key found in browser cookies." case .invalidSessionKey: "Invalid Claude session key format." case .notSupportedOnThisPlatform: "Claude web fetching is only supported on macOS." case let .networkError(error): "Network error: \(error.localizedDescription)" case .invalidResponse: "Invalid response from Claude API." case .unauthorized: "Sign in to claude.ai (or refresh Claude cookies) to load usage data." case let .serverError(code): "Claude API error: HTTP \(code)" case .noOrganization: "No Claude organization found for this account." case let .organizationNotFound(id): "Claude organization '\(id)' was not found for this session." } } } /// Claude usage data from the API public struct WebUsageData: Sendable { public let sessionPercentUsed: Double public let sessionResetsAt: Date? public let weeklyPercentUsed: Double? public let weeklyResetsAt: Date? public let opusPercentUsed: Double? public let extraRateWindows: [NamedRateWindow] public let extraUsageCost: ProviderCostSnapshot? public let accountOrganization: String? public let accountEmail: String? public let loginMethod: String? public init( sessionPercentUsed: Double, sessionResetsAt: Date?, weeklyPercentUsed: Double?, weeklyResetsAt: Date?, opusPercentUsed: Double?, extraRateWindows: [NamedRateWindow], extraUsageCost: ProviderCostSnapshot?, accountOrganization: String?, accountEmail: String?, loginMethod: String?) { self.sessionPercentUsed = sessionPercentUsed self.sessionResetsAt = sessionResetsAt self.weeklyPercentUsed = weeklyPercentUsed self.weeklyResetsAt = weeklyResetsAt self.opusPercentUsed = opusPercentUsed self.extraRateWindows = extraRateWindows self.extraUsageCost = extraUsageCost self.accountOrganization = accountOrganization self.accountEmail = accountEmail self.loginMethod = loginMethod } } public struct ProbeResult: Sendable { public let url: String public let statusCode: Int? public let contentType: String? public let topLevelKeys: [String] public let emails: [String] public let planHints: [String] public let notableFields: [String] public let bodyPreview: String? } // MARK: - Public API #if os(macOS) /// Attempts to fetch Claude usage data using cookies extracted from browsers. /// Tries browser cookies using the standard import order. public static func fetchUsage( browserDetection: BrowserDetection, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?("[claude-web] \(msg)") } if let cached = CookieHeaderCache.load(provider: .claude), !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { log("Using cached cookie header from \(cached.sourceLabel)") do { return try await self.fetchUsage( cookieHeader: cached.cookieHeader, targetOrganizationID: targetOrganizationID, logger: log) } catch let error as FetchError { switch error { case .unauthorized, .noSessionKeyFound, .invalidSessionKey: CookieHeaderCache.clear(provider: .claude) default: throw error } } catch { throw error } } let sessionInfo = try extractSessionKeyInfo(browserDetection: browserDetection, logger: log) log("Found session key (\(sessionInfo.cookieCount) cookies)") let usage = try await self.fetchUsage( using: sessionInfo, targetOrganizationID: targetOrganizationID, logger: log) CookieHeaderCache.store( provider: .claude, cookieHeader: "sessionKey=\(sessionInfo.key)", sourceLabel: sessionInfo.sourceLabel) return usage } public static func fetchUsage( cookieHeader: String, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?("[claude-web] \(msg)") } let sessionInfo = try self.sessionKeyInfo(cookieHeader: cookieHeader) log("Using manual session key (\(sessionInfo.cookieCount) cookies)") return try await self.fetchUsage( using: sessionInfo, targetOrganizationID: targetOrganizationID, logger: log) } public static func fetchUsage( using sessionKeyInfo: SessionKeyInfo, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let log: (String) -> Void = { msg in logger?(msg) } let sessionKey = sessionKeyInfo.key // Fetch organization info let organization = try await fetchOrganizationInfo( sessionKey: sessionKey, targetOrganizationID: targetOrganizationID, logger: log) log("Organization resolved") var usage = try await fetchUsageData(orgId: organization.id, sessionKey: sessionKey, logger: log) if usage.extraUsageCost == nil, let extra = await ClaudeWebExtraUsageCost.fetch( baseURL: Self.baseURL, orgId: organization.id, sessionKey: sessionKey, logger: log) { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: extra, accountOrganization: usage.accountOrganization, accountEmail: usage.accountEmail, loginMethod: usage.loginMethod) } if let account = await fetchAccountInfo(sessionKey: sessionKey, orgId: organization.id, logger: log) { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: usage.extraUsageCost, accountOrganization: usage.accountOrganization, accountEmail: account.email, loginMethod: account.loginMethod) } if usage.accountOrganization == nil, let name = organization.name { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, extraRateWindows: usage.extraRateWindows, extraUsageCost: usage.extraUsageCost, accountOrganization: name, accountEmail: usage.accountEmail, loginMethod: usage.loginMethod) } return usage } /// Probes a list of endpoints using the current claude.ai session cookies. /// - Parameters: /// - endpoints: Absolute URLs or "/api/..." paths. Supports "{orgId}" placeholder. /// - includePreview: When true, includes a truncated response preview in results. public static func probeEndpoints( _ endpoints: [String], browserDetection: BrowserDetection, includePreview: Bool = false, logger: ((String) -> Void)? = nil) async throws -> [ProbeResult] { let log: (String) -> Void = { msg in logger?("[claude-probe] \(msg)") } let sessionInfo = try extractSessionKeyInfo(browserDetection: browserDetection, logger: log) let sessionKey = sessionInfo.key let organization = try? await fetchOrganizationInfo(sessionKey: sessionKey, logger: log) let expanded = endpoints.map { endpoint -> String in var url = endpoint if let orgId = organization?.id { url = url.replacingOccurrences(of: "{orgId}", with: orgId) } if url.hasPrefix("/") { url = "https://claude.ai\(url)" } return url } var results: [ProbeResult] = [] results.reserveCapacity(expanded.count) for endpoint in expanded { guard let url = URL(string: endpoint) else { continue } var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json, text/html;q=0.9, */*;q=0.8", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 20 do { let (data, response) = try await ProviderHTTPClient.shared.data(for: request) let http = response as? HTTPURLResponse let contentType = http?.allHeaderFields["Content-Type"] as? String let truncated = data.prefix(Self.maxProbeBytes) let body = String(data: truncated, encoding: .utf8) ?? "" let parsed = Self.parseProbeBody(data: data, fallbackText: body, contentType: contentType) let preview = includePreview ? parsed.preview : nil results.append(ProbeResult( url: endpoint, statusCode: http?.statusCode, contentType: contentType, topLevelKeys: parsed.keys, emails: parsed.emails, planHints: parsed.planHints, notableFields: parsed.notableFields, bodyPreview: preview)) } catch { results.append(ProbeResult( url: endpoint, statusCode: nil, contentType: nil, topLevelKeys: [], emails: [], planHints: [], notableFields: [], bodyPreview: "Error: \(error.localizedDescription)")) } } return results } /// Checks if we can find a Claude session key in browser cookies without making API calls. public static func hasSessionKey(browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) -> Bool { if let cached = CookieHeaderCache.load(provider: .claude), self.hasSessionKey(cookieHeader: cached.cookieHeader) { return true } do { _ = try self.sessionKeyInfo(browserDetection: browserDetection, logger: logger) return true } catch { return false } } public static func hasSessionKey(cookieHeader: String?) -> Bool { guard let cookieHeader else { return false } return (try? self.sessionKeyInfo(cookieHeader: cookieHeader)) != nil } public static func sessionKeyInfo( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo { try self.extractSessionKeyInfo(browserDetection: browserDetection, logger: logger) } public static func sessionKeyInfo(cookieHeader: String) throws -> SessionKeyInfo { let pairs = CookieHeaderNormalizer.pairs(from: cookieHeader) if let sessionKey = self.findSessionKey(in: pairs) { return SessionKeyInfo( key: sessionKey, sourceLabel: "Manual", cookieCount: pairs.count) } throw FetchError.noSessionKeyFound } // MARK: - Session Key Extraction private static func extractSessionKeyInfo( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionKeyInfo { let log: (String) -> Void = { msg in logger?(msg) } let cookieDomains = ["claude.ai"] // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts let installedBrowsers = Self.cookieImportOrder.cookieImportCandidates(using: browserDetection) for browserSource in installedBrowsers { do { let query = BrowserCookieQuery(domains: cookieDomains) let sources = try Self.cookieClient.codexBarRecords( matching: query, in: browserSource, logger: log) for source in sources { if let sessionKey = findSessionKey(in: source.records.map { record in (name: record.name, value: record.value) }) { log("Found sessionKey in \(source.label)") return SessionKeyInfo( key: sessionKey, sourceLabel: source.label, cookieCount: source.records.count) } } } catch { BrowserCookieAccessGate.recordIfNeeded(error) log("\(browserSource.displayName) cookie load failed: \(error.localizedDescription)") } } throw FetchError.noSessionKeyFound } private static func findSessionKey(in cookies: [(name: String, value: String)]) -> String? { for cookie in cookies where cookie.name == "sessionKey" { let value = cookie.value.trimmingCharacters(in: .whitespacesAndNewlines) // Validate it looks like a Claude session key if value.hasPrefix("sk-ant-") { return value } } return nil } // MARK: - API Calls private static func fetchOrganizationInfo( sessionKey: String, targetOrganizationID: String? = nil, logger: ((String) -> Void)? = nil) async throws -> OrganizationInfo { let url = URL(string: "\(baseURL)/organizations")! var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 15 let (data, response) = try await ProviderHTTPClient.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw FetchError.invalidResponse } logger?("Organizations API status: \(httpResponse.statusCode)") switch httpResponse.statusCode { case 200: return try self.parseOrganizationResponse(data, targetOrganizationID: targetOrganizationID) case 401, 403: throw FetchError.unauthorized default: throw FetchError.serverError(statusCode: httpResponse.statusCode) } } private static func fetchUsageData( orgId: String, sessionKey: String, logger: ((String) -> Void)? = nil) async throws -> WebUsageData { let url = URL(string: "\(baseURL)/organizations/\(orgId)/usage")! var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") request.httpMethod = "GET" request.timeoutInterval = 15 let (data, response) = try await ProviderHTTPClient.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw FetchError.invalidResponse } logger?("Usage API status: \(httpResponse.statusCode)") switch httpResponse.statusCode { case 200: return try self.parseUsageResponse(data, logger: logger) case 401, 403: throw FetchError.unauthorized default: throw FetchError.serverError(statusCode: httpResponse.statusCode) } } private static func parseUsageResponse(_ data: Data, logger: ((String) -> Void)? = nil) throws -> WebUsageData { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw FetchError.invalidResponse } // Parse five_hour (session) usage var sessionPercent: Double? var sessionResets: Date? if let fiveHour = json["five_hour"] as? [String: Any] { if let utilization = fiveHour["utilization"] as? Int { sessionPercent = Double(utilization) } if let resetsAt = fiveHour["resets_at"] as? String { sessionResets = self.parseISO8601Date(resetsAt) } } // Enterprise/credit-based accounts return null for five_hour; treat as 0% rather than an error. let resolvedSessionPercent = sessionPercent ?? 0.0 // Parse seven_day (weekly) usage var weeklyPercent: Double? var weeklyResets: Date?