Peter Steinbergerfix: cache provider cookies680ce0910 months agoHistory
767 lines · 28.0 KB · Swift
RawDownload
import Foundationimport SweetCookieKit #if os(macOS) private let cursorCookieImportOrder: BrowserCookieImportOrder =    ProviderDefaults.metadata[.cursor]?.browserCookieOrder ?? Browser.defaultImportOrder // MARK: - Cursor Cookie Importer /// Imports Cursor session cookies from browser cookies.public enum CursorCookieImporter {    private static let cookieClient = BrowserCookieClient()    private static let sessionCookieNames: Set<String> = [        "WorkosCursorSessionToken",        "__Secure-next-auth.session-token",        "next-auth.session-token",    ]     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: "; ")        }    }     /// Attempts to import Cursor cookies using the standard browser import order.    public static func importSession(        browserDetection: BrowserDetection,        logger: ((String) -> Void)? = nil) throws -> SessionInfo    {        let log: (String) -> Void = { msg in logger?("[cursor-cookie] \(msg)") }         // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts        let installedBrowsers = cursorCookieImportOrder.cookieImportCandidates(using: browserDetection)        let cookieDomains = ["cursor.com", "cursor.sh"]        for browserSource in installedBrowsers {            do {                let query = BrowserCookieQuery(domains: cookieDomains)                let sources = try Self.cookieClient.records(                    matching: query,                    in: browserSource,                    logger: log)                for source in sources where !source.records.isEmpty {                    let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)                    if httpCookies.contains(where: { Self.sessionCookieNames.contains($0.name) }) {                        log("Found \(httpCookies.count) Cursor cookies in \(source.label)")                        return SessionInfo(cookies: httpCookies, sourceLabel: source.label)                    } else {                        log("\(source.label) cookies found, but no Cursor session cookie present")                    }                }            } catch {                log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")            }        }         throw CursorStatusProbeError.noSessionCookie    }     /// Check if Cursor session cookies are available    public static func hasSession(browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) -> Bool {        do {            let session = try self.importSession(browserDetection: browserDetection, logger: logger)            return !session.cookies.isEmpty        } catch {            return false        }    }} // MARK: - Cursor API Models public struct CursorUsageSummary: Codable, Sendable {    public let billingCycleStart: String?    public let billingCycleEnd: String?    public let membershipType: String?    public let limitType: String?    public let isUnlimited: Bool?    public let autoModelSelectedDisplayMessage: String?    public let namedModelSelectedDisplayMessage: String?    public let individualUsage: CursorIndividualUsage?    public let teamUsage: CursorTeamUsage?} public struct CursorIndividualUsage: Codable, Sendable {    public let plan: CursorPlanUsage?    public let onDemand: CursorOnDemandUsage?} public struct CursorPlanUsage: Codable, Sendable {    public let enabled: Bool?    /// Usage in cents (e.g., 2000 = $20.00)    public let used: Int?    /// Limit in cents (e.g., 2000 = $20.00)    public let limit: Int?    /// Remaining in cents    public let remaining: Int?    public let breakdown: CursorPlanBreakdown?    public let autoPercentUsed: Double?    public let apiPercentUsed: Double?    public let totalPercentUsed: Double?} public struct CursorPlanBreakdown: Codable, Sendable {    public let included: Int?    public let bonus: Int?    public let total: Int?} public struct CursorOnDemandUsage: Codable, Sendable {    public let enabled: Bool?    /// Usage in cents    public let used: Int?    /// Limit in cents (nil if unlimited)    public let limit: Int?    /// Remaining in cents (nil if unlimited)    public let remaining: Int?} public struct CursorTeamUsage: Codable, Sendable {    public let onDemand: CursorOnDemandUsage?} // MARK: - Cursor Usage API Models (Legacy Request-Based Plans) /// Response from `/api/usage?user=ID` endpoint for legacy request-based plans.public struct CursorUsageResponse: Codable, Sendable {    public let gpt4: CursorModelUsage?    public let startOfMonth: String?     enum CodingKeys: String, CodingKey {        case gpt4 = "gpt-4"        case startOfMonth    }} public struct CursorModelUsage: Codable, Sendable {    public let numRequests: Int?    public let numRequestsTotal: Int?    public let numTokens: Int?    public let maxRequestUsage: Int?    public let maxTokenUsage: Int?} public struct CursorUserInfo: Codable, Sendable {    public let email: String?    public let emailVerified: Bool?    public let name: String?    public let sub: String?    public let createdAt: String?    public let updatedAt: String?    public let picture: String?     enum CodingKeys: String, CodingKey {        case email        case emailVerified = "email_verified"        case name        case sub        case createdAt = "created_at"        case updatedAt = "updated_at"        case picture    }} // MARK: - Cursor Status Snapshot public struct CursorStatusSnapshot: Sendable {    /// Percentage of included plan usage (0-100)    public let planPercentUsed: Double    /// Included plan usage in USD    public let planUsedUSD: Double    /// Included plan limit in USD    public let planLimitUSD: Double    /// On-demand usage in USD    public let onDemandUsedUSD: Double    /// On-demand limit in USD (nil if unlimited)    public let onDemandLimitUSD: Double?    /// Team on-demand usage in USD (for team plans)    public let teamOnDemandUsedUSD: Double?    /// Team on-demand limit in USD    public let teamOnDemandLimitUSD: Double?    /// Billing cycle reset date    public let billingCycleEnd: Date?    /// Membership type (e.g., "enterprise", "pro", "hobby")    public let membershipType: String?    /// User email    public let accountEmail: String?    /// User name    public let accountName: String?    /// Raw API response for debugging    public let rawJSON: String?     // MARK: - Legacy Plan (Request-Based) Fields     /// Requests used this billing cycle (legacy plans only)    public let requestsUsed: Int?    /// Request limit (non-nil indicates legacy request-based plan)    public let requestsLimit: Int?     /// Whether this is a legacy request-based plan (vs token-based)    public var isLegacyRequestPlan: Bool {        self.requestsLimit != nil    }     public init(        planPercentUsed: Double,        planUsedUSD: Double,        planLimitUSD: Double,        onDemandUsedUSD: Double,        onDemandLimitUSD: Double?,        teamOnDemandUsedUSD: Double?,        teamOnDemandLimitUSD: Double?,        billingCycleEnd: Date?,        membershipType: String?,        accountEmail: String?,        accountName: String?,        rawJSON: String?,        requestsUsed: Int? = nil,        requestsLimit: Int? = nil)    {        self.planPercentUsed = planPercentUsed        self.planUsedUSD = planUsedUSD        self.planLimitUSD = planLimitUSD        self.onDemandUsedUSD = onDemandUsedUSD        self.onDemandLimitUSD = onDemandLimitUSD        self.teamOnDemandUsedUSD = teamOnDemandUsedUSD        self.teamOnDemandLimitUSD = teamOnDemandLimitUSD        self.billingCycleEnd = billingCycleEnd        self.membershipType = membershipType        self.accountEmail = accountEmail        self.accountName = accountName        self.rawJSON = rawJSON        self.requestsUsed = requestsUsed        self.requestsLimit = requestsLimit    }     /// Convert to UsageSnapshot for the common provider interface    public func toUsageSnapshot() -> UsageSnapshot {        // Primary: For legacy request-based plans, use request usage; otherwise use plan percentage        let primaryUsedPercent: Double = if self.isLegacyRequestPlan,                                            let used = self.requestsUsed,                                            let limit = self.requestsLimit,                                            limit > 0        {            (Double(used) / Double(limit)) * 100        } else {            self.planPercentUsed        }         let primary = RateWindow(            usedPercent: primaryUsedPercent,            windowMinutes: nil,            resetsAt: self.billingCycleEnd,            resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) })         // Always use individual on-demand values (what users see in their Cursor dashboard).        // Team values are aggregates across all members, not useful for individual tracking.        let resolvedOnDemandUsed = self.onDemandUsedUSD        let resolvedOnDemandLimit = self.onDemandLimitUSD         // Secondary: On-demand usage as percentage of individual limit        let secondary: RateWindow? = if let limit = resolvedOnDemandLimit,                                        limit > 0        {            RateWindow(                usedPercent: (resolvedOnDemandUsed / limit) * 100,                windowMinutes: nil,                resetsAt: self.billingCycleEnd,                resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) })        } else {            nil        }         // Provider cost snapshot for on-demand usage        let providerCost: ProviderCostSnapshot? = if resolvedOnDemandUsed > 0 {            ProviderCostSnapshot(                used: resolvedOnDemandUsed,                limit: resolvedOnDemandLimit ?? 0,                currencyCode: "USD",                period: "monthly",                resetsAt: self.billingCycleEnd,                updatedAt: Date())        } else {            nil        }         // Legacy plan request usage (when maxRequestUsage is set)        let cursorRequests: CursorRequestUsage? = if let used = self.requestsUsed,                                                     let limit = self.requestsLimit        {            CursorRequestUsage(used: used, limit: limit)        } else {            nil        }         let identity = ProviderIdentitySnapshot(            providerID: .cursor,            accountEmail: self.accountEmail,            accountOrganization: nil,            loginMethod: self.membershipType.map { Self.formatMembershipType($0) })        return UsageSnapshot(            primary: primary,            secondary: secondary,            tertiary: nil,            providerCost: providerCost,            cursorRequests: cursorRequests,            updatedAt: Date(),            identity: identity)    }     private static func formatResetDate(_ date: Date) -> String {        let formatter = DateFormatter()        formatter.dateFormat = "MMM d 'at' h:mma"        formatter.locale = Locale(identifier: "en_US_POSIX")        return "Resets " + formatter.string(from: date)    }     private static func formatMembershipType(_ type: String) -> String {        switch type.lowercased() {        case "enterprise":            "Cursor Enterprise"        case "pro":            "Cursor Pro"        case "hobby":            "Cursor Hobby"        case "team":            "Cursor Team"        default:            "Cursor \(type.capitalized)"        }    }} // MARK: - Cursor Status Probe Error public enum CursorStatusProbeError: LocalizedError, Sendable {    case notLoggedIn    case networkError(String)    case parseFailed(String)    case noSessionCookie     public var errorDescription: String? {        switch self {        case .notLoggedIn:            "Not logged in to Cursor. Please log in via the CodexBar menu."        case let .networkError(msg):            "Cursor API error: \(msg)"        case let .parseFailed(msg):            "Could not parse Cursor usage: \(msg)"        case .noSessionCookie:            "No Cursor session found. Please log in to cursor.com in \(cursorCookieImportOrder.loginHint)."        }    }} // MARK: - Cursor Session Store public actor CursorSessionStore {    public static let shared = CursorSessionStore()     private var sessionCookies: [HTTPCookie] = []    private var hasLoadedFromDisk = false    private let fileURL: URL     private init() {        let fm = FileManager.default        let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first            ?? fm.temporaryDirectory        let dir = appSupport.appendingPathComponent("CodexBar", isDirectory: true)        try? fm.createDirectory(at: dir, withIntermediateDirectories: true)        self.fileURL = dir.appendingPathComponent("cursor-session.json")         // Load saved cookies on init        Task { await self.loadFromDiskIfNeeded() }    }     public func setCookies(_ cookies: [HTTPCookie]) {        self.hasLoadedFromDisk = true        self.sessionCookies = cookies        self.saveToDisk()    }     public func getCookies() -> [HTTPCookie] {        self.loadFromDiskIfNeeded()        return self.sessionCookies    }     public func clearCookies() {        self.hasLoadedFromDisk = true        self.sessionCookies = []        try? FileManager.default.removeItem(at: self.fileURL)    }     public func hasValidSession() -> Bool {        self.loadFromDiskIfNeeded()        return !self.sessionCookies.isEmpty    }     #if DEBUG    func resetForTesting(clearDisk: Bool = true) {        self.hasLoadedFromDisk = false        self.sessionCookies = []        if clearDisk {            try? FileManager.default.removeItem(at: self.fileURL)        }    }    #endif     private func loadFromDiskIfNeeded() {        guard !self.hasLoadedFromDisk else { return }        self.hasLoadedFromDisk = true        self.loadFromDisk()    }     private func saveToDisk() {        // Convert cookie properties to JSON-serializable format        // Date values must be converted to TimeInterval (Double)        let cookieData = self.sessionCookies.compactMap { cookie -> [String: Any]? in            guard let props = cookie.properties else { return nil }            var serializable: [String: Any] = [:]            for (key, value) in props {                let keyString = key.rawValue                if let date = value as? Date {                    // Convert Date to TimeInterval for JSON compatibility                    serializable[keyString] = date.timeIntervalSince1970                    serializable[keyString + "_isDate"] = true                } else if let url = value as? URL {                    serializable[keyString] = url.absoluteString                    serializable[keyString + "_isURL"] = true                } else if JSONSerialization.isValidJSONObject([value]) ||                    value is String ||                    value is Bool ||                    value is NSNumber                {                    serializable[keyString] = value                }            }            return serializable        }        guard !cookieData.isEmpty,              let data = try? JSONSerialization.data(withJSONObject: cookieData, options: [.prettyPrinted])        else {            return        }        try? data.write(to: self.fileURL)    }     private func loadFromDisk() {        guard let data = try? Data(contentsOf: self.fileURL),              let cookieArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]        else { return }         self.sessionCookies = cookieArray.compactMap { props in            // Convert back to HTTPCookiePropertyKey dictionary            var cookieProps: [HTTPCookiePropertyKey: Any] = [:]            for (key, value) in props {                // Skip marker keys                if key.hasSuffix("_isDate") || key.hasSuffix("_isURL") { continue }                 let propKey = HTTPCookiePropertyKey(key)                 // Check if this was a Date                if props[key + "_isDate"] as? Bool == true, let interval = value as? TimeInterval {                    cookieProps[propKey] = Date(timeIntervalSince1970: interval)                }                // Check if this was a URL                else if props[key + "_isURL"] as? Bool == true, let urlString = value as? String {                    cookieProps[propKey] = URL(string: urlString)                } else {                    cookieProps[propKey] = value                }            }            return HTTPCookie(properties: cookieProps)        }    }} // MARK: - Cursor Status Probe public struct CursorStatusProbe: Sendable {    public let baseURL: URL    public var timeout: TimeInterval = 15.0    private let browserDetection: BrowserDetection     public init(        baseURL: URL = URL(string: "https://cursor.com")!,        timeout: TimeInterval = 15.0,        browserDetection: BrowserDetection)    {        self.baseURL = baseURL        self.timeout = timeout        self.browserDetection = browserDetection

Lines 1–500 of 767.