chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// A resolved CommandCode session cookie ready to be sent on `Cookie:` headers.
|
||||
///
|
||||
/// CommandCode's API (api.commandcode.ai) authenticates with the better-auth session
|
||||
/// cookie set by commandcode.ai. better-auth emits either `better-auth.session_token`
|
||||
/// or `__Secure-better-auth.session_token` depending on whether `useSecureCookies` is
|
||||
/// enabled (the `__Secure-` variant is required by browsers for HTTPS production).
|
||||
public struct CommandCodeCookieOverride: Sendable, Equatable {
|
||||
public let name: String
|
||||
public let token: String
|
||||
|
||||
public init(name: String, token: String) {
|
||||
self.name = name
|
||||
self.token = token
|
||||
}
|
||||
|
||||
/// `Cookie: name=value` header value.
|
||||
public var headerValue: String {
|
||||
"\(self.name)=\(self.token)"
|
||||
}
|
||||
}
|
||||
|
||||
public enum CommandCodeCookieHeader {
|
||||
/// Cookie names used by better-auth in production (HTTPS) and dev (HTTP).
|
||||
/// The `__Secure-` variant is the standard production deployment.
|
||||
public static let supportedSessionCookieNames = [
|
||||
"__Host-better-auth.session_token",
|
||||
"__Secure-better-auth.session_token",
|
||||
"better-auth.session_token",
|
||||
]
|
||||
|
||||
/// Extract a session cookie from a list of `HTTPCookie` records.
|
||||
public static func sessionCookie(from cookies: [HTTPCookie]) -> CommandCodeCookieOverride? {
|
||||
let pairs = cookies.map { (name: $0.name, value: $0.value) }
|
||||
return self.extractSessionCookie(from: pairs)
|
||||
}
|
||||
|
||||
/// Parse a raw `Cookie:` header (or bare token) and extract the session value.
|
||||
public static func override(from raw: String?) -> CommandCodeCookieOverride? {
|
||||
guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bare token — assume the production cookie name.
|
||||
if !raw.contains("="), !raw.contains(";") {
|
||||
return CommandCodeCookieOverride(
|
||||
name: "__Secure-better-auth.session_token",
|
||||
token: raw)
|
||||
}
|
||||
|
||||
return self.extractSessionCookie(fromHeader: raw)
|
||||
}
|
||||
|
||||
private static func extractSessionCookie(fromHeader header: String) -> CommandCodeCookieOverride? {
|
||||
var pairs: [(name: String, value: String)] = []
|
||||
for chunk in header.split(separator: ";") {
|
||||
let trimmed = chunk.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let separator = trimmed.firstIndex(of: "=") else { continue }
|
||||
let key = String(trimmed[..<separator]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let value = String(trimmed[trimmed.index(after: separator)...])
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !key.isEmpty, !value.isEmpty else { continue }
|
||||
pairs.append((name: key, value: value))
|
||||
}
|
||||
return self.extractSessionCookie(from: pairs)
|
||||
}
|
||||
|
||||
private static func extractSessionCookie(from pairs: [(name: String, value: String)])
|
||||
-> CommandCodeCookieOverride? {
|
||||
var byLowerName: [String: (name: String, value: String)] = [:]
|
||||
for pair in pairs {
|
||||
byLowerName[pair.name.lowercased()] = pair
|
||||
}
|
||||
for expected in self.supportedSessionCookieNames {
|
||||
if let match = byLowerName[expected.lowercased()] {
|
||||
return CommandCodeCookieOverride(name: match.name, token: match.value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
import SweetCookieKit
|
||||
|
||||
/// Imports CommandCode session cookies from installed browsers (Chrome by default).
|
||||
public enum CommandCodeCookieImporter {
|
||||
private static let importSessionCacheTTL: TimeInterval = 5
|
||||
private static let importSessionCache = ImportSessionCache(ttl: importSessionCacheTTL)
|
||||
private static let log = CodexBarLog.logger(LogCategories.commandcodeCookie)
|
||||
private static let cookieClient = BrowserCookieClient()
|
||||
private static let cookieDomains = ["commandcode.ai", "www.commandcode.ai"]
|
||||
private static let cookieImportOrder: BrowserCookieImportOrder =
|
||||
ProviderDefaults.metadata[.commandcode]?.browserCookieOrder ?? Browser.defaultImportOrder
|
||||
|
||||
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 sessionCookie: CommandCodeCookieOverride? {
|
||||
CommandCodeCookieHeader.sessionCookie(from: self.cookies)
|
||||
}
|
||||
|
||||
public var cookieHeader: String {
|
||||
self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ")
|
||||
}
|
||||
}
|
||||
|
||||
public static func importSessions(
|
||||
browserDetection: BrowserDetection = BrowserDetection(),
|
||||
logger: ((String) -> Void)? = nil) throws -> [SessionInfo]
|
||||
{
|
||||
if let cached = self.cachedImportSessions() {
|
||||
return cached
|
||||
}
|
||||
|
||||
var sessions: [SessionInfo] = []
|
||||
let candidates = self.cookieImportOrder.cookieImportCandidates(using: browserDetection)
|
||||
for browserSource in candidates {
|
||||
do {
|
||||
let perSource = try self.importSessions(from: browserSource, logger: logger)
|
||||
sessions.append(contentsOf: perSource)
|
||||
} catch {
|
||||
BrowserCookieAccessGate.recordIfNeeded(error)
|
||||
self.emit(
|
||||
"\(browserSource.displayName) cookie import failed: \(error.localizedDescription)",
|
||||
logger: logger)
|
||||
}
|
||||
}
|
||||
|
||||
guard !sessions.isEmpty else {
|
||||
throw CommandCodeCookieImportError.noCookies
|
||||
}
|
||||
self.storeImportSessions(sessions)
|
||||
return sessions
|
||||
}
|
||||
|
||||
public static func importSessions(
|
||||
from browserSource: Browser,
|
||||
logger: ((String) -> Void)? = nil) throws -> [SessionInfo]
|
||||
{
|
||||
let query = BrowserCookieQuery(domains: self.cookieDomains)
|
||||
let log: (String) -> Void = { msg in self.emit(msg, logger: logger) }
|
||||
let sources = try Self.cookieClient.codexBarRecords(
|
||||
matching: query,
|
||||
in: browserSource,
|
||||
logger: log)
|
||||
|
||||
var sessions: [SessionInfo] = []
|
||||
let grouped = Dictionary(grouping: sources, by: { $0.store.profile.id })
|
||||
let sortedGroups = grouped.values.sorted { lhs, rhs in
|
||||
self.mergedLabel(for: lhs) < self.mergedLabel(for: rhs)
|
||||
}
|
||||
|
||||
for group in sortedGroups where !group.isEmpty {
|
||||
let label = self.mergedLabel(for: group)
|
||||
let mergedRecords = self.mergeRecords(group)
|
||||
guard !mergedRecords.isEmpty else { continue }
|
||||
let httpCookies = BrowserCookieClient.makeHTTPCookies(mergedRecords, origin: query.origin)
|
||||
guard !httpCookies.isEmpty else { continue }
|
||||
|
||||
let session = SessionInfo(cookies: httpCookies, sourceLabel: label)
|
||||
if let sessionCookie = session.sessionCookie {
|
||||
log("Found \(sessionCookie.name) cookie in \(label)")
|
||||
} else {
|
||||
let names = httpCookies.map(\.name).joined(separator: ", ")
|
||||
log("No known session name in \(label); sending all domain cookies (\(names))")
|
||||
}
|
||||
sessions.append(session)
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
||||
public static func importSession(
|
||||
browserDetection: BrowserDetection = BrowserDetection(),
|
||||
logger: ((String) -> Void)? = nil) throws -> SessionInfo
|
||||
{
|
||||
let sessions = try self.importSessions(browserDetection: browserDetection, logger: logger)
|
||||
guard let first = sessions.first else { throw CommandCodeCookieImportError.noCookies }
|
||||
return first
|
||||
}
|
||||
|
||||
public static func hasSession(
|
||||
browserDetection: BrowserDetection = BrowserDetection(),
|
||||
logger: ((String) -> Void)? = nil) -> Bool
|
||||
{
|
||||
do {
|
||||
let session = try self.importSession(browserDetection: browserDetection, logger: logger)
|
||||
return !session.cookies.isEmpty
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func invalidateImportSessionCache() {
|
||||
self.importSessionCache.invalidate()
|
||||
}
|
||||
|
||||
private static func emit(_ message: String, logger: ((String) -> Void)?) {
|
||||
logger?("[commandcode-cookie] \(message)")
|
||||
self.log.debug(message)
|
||||
}
|
||||
|
||||
private static func cachedImportSessions(now: Date = Date()) -> [SessionInfo]? {
|
||||
self.importSessionCache.load(now: now)
|
||||
}
|
||||
|
||||
private static func storeImportSessions(_ sessions: [SessionInfo], now: Date = Date()) {
|
||||
self.importSessionCache.store(sessions, now: now)
|
||||
}
|
||||
|
||||
private static func mergedLabel(for sources: [BrowserCookieStoreRecords]) -> String {
|
||||
guard let base = sources.map(\.label).min() else { return "Unknown" }
|
||||
if base.hasSuffix(" (Network)") {
|
||||
return String(base.dropLast(" (Network)".count))
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
private static func mergeRecords(_ sources: [BrowserCookieStoreRecords]) -> [BrowserCookieRecord] {
|
||||
let sortedSources = sources.sorted { lhs, rhs in
|
||||
self.storePriority(lhs.store.kind) < self.storePriority(rhs.store.kind)
|
||||
}
|
||||
var mergedByKey: [String: BrowserCookieRecord] = [:]
|
||||
for source in sortedSources {
|
||||
for record in source.records {
|
||||
let key = self.recordKey(record)
|
||||
if let existing = mergedByKey[key] {
|
||||
if self.shouldReplace(existing: existing, candidate: record) {
|
||||
mergedByKey[key] = record
|
||||
}
|
||||
} else {
|
||||
mergedByKey[key] = record
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array(mergedByKey.values)
|
||||
}
|
||||
|
||||
private static func storePriority(_ kind: BrowserCookieStoreKind) -> Int {
|
||||
switch kind {
|
||||
case .network: 0
|
||||
case .primary: 1
|
||||
case .safari: 2
|
||||
}
|
||||
}
|
||||
|
||||
private static func recordKey(_ record: BrowserCookieRecord) -> String {
|
||||
"\(record.name)|\(record.domain)|\(record.path)"
|
||||
}
|
||||
|
||||
private static func shouldReplace(existing: BrowserCookieRecord, candidate: BrowserCookieRecord) -> Bool {
|
||||
switch (existing.expires, candidate.expires) {
|
||||
case let (lhs?, rhs?): rhs > lhs
|
||||
case (nil, .some): true
|
||||
case (.some, nil): false
|
||||
case (nil, nil): false
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImportSessionCache: @unchecked Sendable {
|
||||
private let ttl: TimeInterval
|
||||
private let lock = NSLock()
|
||||
private var entry: (sessions: [SessionInfo], expiresAt: Date)?
|
||||
|
||||
init(ttl: TimeInterval) {
|
||||
self.ttl = ttl
|
||||
}
|
||||
|
||||
func load(now: Date) -> [SessionInfo]? {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
guard let entry = self.entry else { return nil }
|
||||
guard entry.expiresAt > now else {
|
||||
self.entry = nil
|
||||
return nil
|
||||
}
|
||||
return entry.sessions
|
||||
}
|
||||
|
||||
func store(_ sessions: [SessionInfo], now: Date) {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.entry = (sessions: sessions, expiresAt: now.addingTimeInterval(self.ttl))
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.entry = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum CommandCodeCookieImportError: LocalizedError {
|
||||
case noCookies
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noCookies:
|
||||
"No Command Code session cookies found in browsers. Sign in to commandcode.ai."
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
/// Static catalog of CommandCode subscription plans → monthly credit allowance (in USD).
|
||||
///
|
||||
/// The `/internal/billing/credits` endpoint exposes the *remaining* `monthlyCredits`,
|
||||
/// not the plan total. The plan total is published on the public pricing page
|
||||
/// (https://commandcode.ai/pricing) and is keyed by `planId` returned from
|
||||
/// `/internal/billing/subscriptions`.
|
||||
public enum CommandCodePlanCatalog {
|
||||
public struct Plan: Sendable, Equatable {
|
||||
public let id: String
|
||||
public let displayName: String
|
||||
/// Monthly credit allowance in USD.
|
||||
public let monthlyCreditsUSD: Double
|
||||
|
||||
public init(id: String, displayName: String, monthlyCreditsUSD: Double) {
|
||||
self.id = id
|
||||
self.displayName = displayName
|
||||
self.monthlyCreditsUSD = monthlyCreditsUSD
|
||||
}
|
||||
}
|
||||
|
||||
public static let plans: [Plan] = [
|
||||
Plan(id: "individual-go", displayName: "Go", monthlyCreditsUSD: 10),
|
||||
Plan(id: "individual-pro", displayName: "Pro", monthlyCreditsUSD: 30),
|
||||
Plan(id: "individual-max", displayName: "Max", monthlyCreditsUSD: 150),
|
||||
Plan(id: "individual-ultra", displayName: "Ultra", monthlyCreditsUSD: 300),
|
||||
]
|
||||
|
||||
public static func plan(forID planID: String) -> Plan? {
|
||||
let normalized = planID.lowercased()
|
||||
return self.plans.first(where: { $0.id == normalized })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import Foundation
|
||||
|
||||
public enum CommandCodeProviderDescriptor {
|
||||
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
|
||||
|
||||
static func makeDescriptor() -> ProviderDescriptor {
|
||||
ProviderDescriptor(
|
||||
id: .commandcode,
|
||||
metadata: ProviderMetadata(
|
||||
id: .commandcode,
|
||||
displayName: "Command Code",
|
||||
sessionLabel: "Monthly credits",
|
||||
weeklyLabel: "Monthly",
|
||||
opusLabel: nil,
|
||||
supportsOpus: false,
|
||||
supportsCredits: true,
|
||||
creditsHint: "Monthly USD credits from Command Code billing.",
|
||||
toggleTitle: "Show Command Code usage",
|
||||
cliName: "commandcode",
|
||||
defaultEnabled: false,
|
||||
isPrimaryProvider: false,
|
||||
usesAccountFallback: false,
|
||||
browserCookieOrder: ProviderBrowserCookieDefaults.defaultImportOrder,
|
||||
dashboardURL: "https://commandcode.ai/studio",
|
||||
subscriptionDashboardURL: "https://commandcode.ai/sixhobbits/settings/billing",
|
||||
statusPageURL: nil,
|
||||
statusLinkURL: nil),
|
||||
branding: ProviderBranding(
|
||||
iconStyle: .commandcode,
|
||||
iconResourceName: "ProviderIcon-commandcode",
|
||||
color: ProviderColor(red: 0 / 255, green: 0 / 255, blue: 0 / 255)),
|
||||
tokenCost: ProviderTokenCostConfig(
|
||||
supportsTokenCost: false,
|
||||
noDataMessage: { "Command Code cost summary is not yet supported." }),
|
||||
fetchPlan: ProviderFetchPlan(
|
||||
sourceModes: [.auto, .web],
|
||||
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [CommandCodeWebFetchStrategy()] })),
|
||||
cli: ProviderCLIConfig(
|
||||
name: "commandcode",
|
||||
aliases: ["command-code"],
|
||||
versionDetector: nil))
|
||||
}
|
||||
}
|
||||
|
||||
struct CommandCodeWebFetchStrategy: ProviderFetchStrategy {
|
||||
let id: String = "commandcode.web"
|
||||
let kind: ProviderFetchKind = .web
|
||||
|
||||
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
|
||||
guard context.settings?.commandcode?.cookieSource != .off else { return false }
|
||||
if Self.manualCookieHeader(from: context) != nil {
|
||||
return true
|
||||
}
|
||||
#if os(macOS)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
|
||||
let cookieHeader: String
|
||||
let sourceLabel: String
|
||||
if let manual = Self.manualCookieHeader(from: context) {
|
||||
cookieHeader = manual
|
||||
sourceLabel = "manual"
|
||||
} else {
|
||||
#if os(macOS)
|
||||
let session: CommandCodeCookieImporter.SessionInfo
|
||||
do {
|
||||
session = try CommandCodeCookieImporter.importSession()
|
||||
} catch {
|
||||
throw CommandCodeUsageError.missingCredentials
|
||||
}
|
||||
guard !session.cookies.isEmpty else {
|
||||
throw CommandCodeUsageError.missingCredentials
|
||||
}
|
||||
cookieHeader = session.cookieHeader
|
||||
sourceLabel = session.sourceLabel
|
||||
#else
|
||||
throw CommandCodeUsageError.missingCredentials
|
||||
#endif
|
||||
}
|
||||
let snapshot = try await CommandCodeUsageFetcher.fetchUsage(cookieHeader: cookieHeader)
|
||||
return self.makeResult(
|
||||
usage: snapshot.toUsageSnapshot(),
|
||||
sourceLabel: sourceLabel)
|
||||
}
|
||||
|
||||
private static func manualCookieHeader(from context: ProviderFetchContext) -> String? {
|
||||
guard context.settings?.commandcode?.cookieSource == .manual else { return nil }
|
||||
return CookieHeaderNormalizer.normalize(context.settings?.commandcode?.manualCookieHeader)
|
||||
}
|
||||
|
||||
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
public enum CommandCodeUsageError: LocalizedError, Sendable, Equatable {
|
||||
case missingCredentials
|
||||
case invalidCredentials
|
||||
case networkError(String)
|
||||
case apiError(Int)
|
||||
case parseFailed(String)
|
||||
case unknownPlan(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .missingCredentials:
|
||||
"Command Code session cookie not found. Sign in to commandcode.ai in Chrome."
|
||||
case .invalidCredentials:
|
||||
"Command Code session is invalid or expired. Sign in to commandcode.ai again."
|
||||
case let .networkError(message):
|
||||
"Command Code network error: \(message)"
|
||||
case let .apiError(status):
|
||||
"Command Code API returned status \(status)."
|
||||
case let .parseFailed(message):
|
||||
"Could not parse Command Code response: \(message)"
|
||||
case let .unknownPlan(planID):
|
||||
"Unknown Command Code plan: \(planID). Add it to CommandCodePlanCatalog."
|
||||
}
|
||||
}
|
||||
|
||||
public var isAuthRelated: Bool {
|
||||
switch self {
|
||||
case .missingCredentials, .invalidCredentials: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// Fetches live billing data from `api.commandcode.ai` using a better-auth session
|
||||
/// cookie scraped from the user's browser.
|
||||
public enum CommandCodeUsageFetcher {
|
||||
private static let log = CodexBarLog.logger(LogCategories.commandcodeUsage)
|
||||
private static let requestTimeoutSeconds: TimeInterval = 15
|
||||
private static let subscriptionGraceSeconds: TimeInterval = 2
|
||||
private static let apiBase = URL(string: "https://api.commandcode.ai")!
|
||||
private static let creditsPath = "/internal/billing/credits"
|
||||
private static let subscriptionsPath = "/internal/billing/subscriptions"
|
||||
private static let webOrigin = "https://commandcode.ai"
|
||||
private static let userAgent =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
||||
|
||||
public static func fetchUsage(
|
||||
cookieHeader: String,
|
||||
session transport: any ProviderHTTPTransport = ProviderHTTPClient.shared,
|
||||
now: Date = Date()) async throws -> CommandCodeUsageSnapshot
|
||||
{
|
||||
try await self.fetchUsage(
|
||||
cookieHeader: cookieHeader,
|
||||
transport: transport,
|
||||
now: now,
|
||||
subscriptionGrace: .seconds(self.subscriptionGraceSeconds))
|
||||
}
|
||||
|
||||
static func _fetchUsageForTesting(
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport,
|
||||
now: Date = Date(),
|
||||
subscriptionGrace: Duration) async throws -> CommandCodeUsageSnapshot
|
||||
{
|
||||
try await self.fetchUsage(
|
||||
cookieHeader: cookieHeader,
|
||||
transport: transport,
|
||||
now: now,
|
||||
subscriptionGrace: subscriptionGrace)
|
||||
}
|
||||
|
||||
private static func fetchUsage(
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport,
|
||||
now: Date,
|
||||
subscriptionGrace: Duration) async throws -> CommandCodeUsageSnapshot
|
||||
{
|
||||
let (credits, subscription, subscriptionEnrichmentUnavailable) = try await self.fetchPayloads(
|
||||
cookieHeader: cookieHeader,
|
||||
transport: transport,
|
||||
subscriptionGrace: subscriptionGrace)
|
||||
|
||||
let plan: CommandCodePlanCatalog.Plan? = subscription.flatMap { sub in
|
||||
CommandCodePlanCatalog.plan(forID: sub.planID)
|
||||
}
|
||||
|
||||
// If we got an active subscription with an unrecognised plan ID, surface that
|
||||
// explicitly rather than silently dropping the totals row.
|
||||
if let sub = subscription, sub.status.lowercased() == "active", plan == nil {
|
||||
Self.log.error("Unknown CommandCode planId: \(sub.planID)")
|
||||
throw CommandCodeUsageError.unknownPlan(sub.planID)
|
||||
}
|
||||
|
||||
return CommandCodeUsageSnapshot(
|
||||
monthlyCreditsRemaining: credits.monthlyCredits,
|
||||
purchasedCredits: credits.purchasedCredits,
|
||||
premiumMonthlyCredits: credits.premiumMonthlyCredits,
|
||||
opensourceMonthlyCredits: credits.opensourceMonthlyCredits,
|
||||
plan: plan,
|
||||
billingPeriodEnd: subscription?.currentPeriodEnd,
|
||||
subscriptionStatus: subscription?.status,
|
||||
subscriptionEnrichmentUnavailable: subscriptionEnrichmentUnavailable,
|
||||
updatedAt: now)
|
||||
}
|
||||
|
||||
private static func fetchPayloads(
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport,
|
||||
subscriptionGrace: Duration) async throws -> (CreditsPayload, SubscriptionPayload?, Bool)
|
||||
{
|
||||
let subscriptionTask = Task<SubscriptionPayload?, Error> {
|
||||
try await self.fetchSubscription(cookieHeader: cookieHeader, transport: transport)
|
||||
}
|
||||
let credits: CreditsPayload
|
||||
do {
|
||||
credits = try await withTaskCancellationHandler {
|
||||
try await self.fetchCredits(cookieHeader: cookieHeader, transport: transport)
|
||||
} onCancel: {
|
||||
subscriptionTask.cancel()
|
||||
}
|
||||
} catch {
|
||||
subscriptionTask.cancel()
|
||||
throw error
|
||||
}
|
||||
|
||||
do {
|
||||
try Task.checkCancellation()
|
||||
} catch {
|
||||
subscriptionTask.cancel()
|
||||
throw error
|
||||
}
|
||||
let race = BoundedTaskJoin(sourceTask: subscriptionTask)
|
||||
switch await race.value(joinGrace: subscriptionGrace) {
|
||||
case let .value(subscription):
|
||||
try Task.checkCancellation()
|
||||
return (credits, subscription, false)
|
||||
case .timedOut:
|
||||
try Task.checkCancellation()
|
||||
Self.log.warning("Command Code subscription enrichment timed out")
|
||||
return (credits, nil, true)
|
||||
case let .failure(error):
|
||||
subscriptionTask.cancel()
|
||||
try Task.checkCancellation()
|
||||
Self.log.warning("Command Code subscription enrichment failed: \(error.localizedDescription)")
|
||||
return (credits, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Endpoints
|
||||
|
||||
struct CreditsPayload {
|
||||
let monthlyCredits: Double
|
||||
let purchasedCredits: Double
|
||||
let premiumMonthlyCredits: Double
|
||||
let opensourceMonthlyCredits: Double
|
||||
}
|
||||
|
||||
struct SubscriptionPayload {
|
||||
let planID: String
|
||||
let status: String
|
||||
let currentPeriodEnd: Date?
|
||||
}
|
||||
|
||||
private static func fetchCredits(
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport) async throws -> CreditsPayload
|
||||
{
|
||||
let url = self.apiBase.appendingPathComponent(self.creditsPath)
|
||||
let data = try await self.send(url: url, cookieHeader: cookieHeader, transport: transport)
|
||||
return try self.parseCredits(data: data)
|
||||
}
|
||||
|
||||
private static func fetchSubscription(
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport) async throws -> SubscriptionPayload?
|
||||
{
|
||||
let url = self.apiBase.appendingPathComponent(self.subscriptionsPath)
|
||||
let data = try await self.send(url: url, cookieHeader: cookieHeader, transport: transport)
|
||||
return try self.parseSubscription(data: data)
|
||||
}
|
||||
|
||||
private static func send(
|
||||
url: URL,
|
||||
cookieHeader: String,
|
||||
transport: any ProviderHTTPTransport) async throws -> Data
|
||||
{
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = self.requestTimeoutSeconds
|
||||
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
|
||||
request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
|
||||
request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language")
|
||||
request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
||||
request.setValue(self.webOrigin, forHTTPHeaderField: "Origin")
|
||||
request.setValue("\(self.webOrigin)/", forHTTPHeaderField: "Referer")
|
||||
|
||||
let response: ProviderHTTPResponse
|
||||
do {
|
||||
response = try await transport.response(for: request)
|
||||
} catch {
|
||||
if error is CancellationError || (error as? URLError)?.code == .cancelled || Task.isCancelled {
|
||||
throw CancellationError()
|
||||
}
|
||||
throw CommandCodeUsageError.networkError(error.localizedDescription)
|
||||
}
|
||||
if response.statusCode == 401 || response.statusCode == 403 {
|
||||
throw CommandCodeUsageError.invalidCredentials
|
||||
}
|
||||
guard (200..<300).contains(response.statusCode) else {
|
||||
let body = String(data: response.data, encoding: .utf8) ?? ""
|
||||
Self.log.error("CommandCode \(url.path) → \(response.statusCode): \(body)")
|
||||
throw CommandCodeUsageError.apiError(response.statusCode)
|
||||
}
|
||||
return response.data
|
||||
}
|
||||
|
||||
// MARK: - Parsing
|
||||
|
||||
static func parseCredits(data: Data) throws -> CreditsPayload {
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw CommandCodeUsageError.parseFailed("Credits: invalid JSON")
|
||||
}
|
||||
guard let credits = root["credits"] as? [String: Any] else {
|
||||
throw CommandCodeUsageError.parseFailed("Credits: missing 'credits' object")
|
||||
}
|
||||
guard let monthly = self.double(from: credits["monthlyCredits"]) else {
|
||||
throw CommandCodeUsageError.parseFailed("Credits: missing monthlyCredits")
|
||||
}
|
||||
return CreditsPayload(
|
||||
monthlyCredits: monthly,
|
||||
purchasedCredits: self.double(from: credits["purchasedCredits"]) ?? 0,
|
||||
premiumMonthlyCredits: self.double(from: credits["premiumMonthlyCredits"]) ?? 0,
|
||||
opensourceMonthlyCredits: self.double(from: credits["opensourceMonthlyCredits"]) ?? 0)
|
||||
}
|
||||
|
||||
static func parseSubscription(data: Data) throws -> SubscriptionPayload? {
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: invalid JSON")
|
||||
}
|
||||
// Only an explicit successful null response identifies the free tier. Failure envelopes are transient.
|
||||
guard let success = root["success"] as? Bool else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: missing success flag")
|
||||
}
|
||||
guard success else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: unsuccessful response")
|
||||
}
|
||||
guard let dataValue = root["data"] else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: missing data")
|
||||
}
|
||||
if dataValue is NSNull {
|
||||
return nil
|
||||
}
|
||||
guard let data = dataValue as? [String: Any] else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: invalid data")
|
||||
}
|
||||
guard let planID = data["planId"] as? String, !planID.isEmpty else {
|
||||
throw CommandCodeUsageError.parseFailed("Subscriptions: missing planId")
|
||||
}
|
||||
let status = (data["status"] as? String) ?? "unknown"
|
||||
let periodEnd = self.date(from: data["currentPeriodEnd"])
|
||||
return SubscriptionPayload(planID: planID, status: status, currentPeriodEnd: periodEnd)
|
||||
}
|
||||
|
||||
// MARK: - Value coercion
|
||||
|
||||
private static func double(from value: Any?) -> Double? {
|
||||
switch value {
|
||||
case let n as NSNumber:
|
||||
let d = n.doubleValue
|
||||
return d.isFinite ? d : nil
|
||||
case let s as String:
|
||||
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Double(trimmed)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func date(from value: Any?) -> Date? {
|
||||
guard let s = value as? String else { return nil }
|
||||
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let fractional = ISO8601DateFormatter()
|
||||
fractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
if let date = fractional.date(from: trimmed) { return date }
|
||||
let plain = ISO8601DateFormatter()
|
||||
plain.formatOptions = [.withInternetDateTime]
|
||||
return plain.date(from: trimmed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
|
||||
/// Parsed view of CommandCode `/internal/billing/credits` + `/internal/billing/subscriptions`.
|
||||
public struct CommandCodeUsageSnapshot: Sendable {
|
||||
/// USD remaining in the current monthly grant (`credits.monthlyCredits`).
|
||||
public let monthlyCreditsRemaining: Double
|
||||
/// USD top-up balance carried over (`credits.purchasedCredits`).
|
||||
public let purchasedCredits: Double
|
||||
/// USD remaining in the premium monthly grant (`credits.premiumMonthlyCredits`).
|
||||
public let premiumMonthlyCredits: Double
|
||||
/// USD remaining in the open-source monthly grant (`credits.opensourceMonthlyCredits`).
|
||||
public let opensourceMonthlyCredits: Double
|
||||
/// Subscription plan, or nil when the user is on the free tier.
|
||||
public let plan: CommandCodePlanCatalog.Plan?
|
||||
/// `currentPeriodEnd` from the active subscription.
|
||||
public let billingPeriodEnd: Date?
|
||||
/// Subscription status (e.g. `active`, `canceled`).
|
||||
public let subscriptionStatus: String?
|
||||
/// The optional subscription request timed out or failed for this refresh.
|
||||
public let subscriptionEnrichmentUnavailable: Bool
|
||||
public let updatedAt: Date
|
||||
|
||||
public init(
|
||||
monthlyCreditsRemaining: Double,
|
||||
purchasedCredits: Double,
|
||||
premiumMonthlyCredits: Double,
|
||||
opensourceMonthlyCredits: Double,
|
||||
plan: CommandCodePlanCatalog.Plan?,
|
||||
billingPeriodEnd: Date?,
|
||||
subscriptionStatus: String?,
|
||||
subscriptionEnrichmentUnavailable: Bool = false,
|
||||
updatedAt: Date = Date())
|
||||
{
|
||||
self.monthlyCreditsRemaining = monthlyCreditsRemaining
|
||||
self.purchasedCredits = purchasedCredits
|
||||
self.premiumMonthlyCredits = premiumMonthlyCredits
|
||||
self.opensourceMonthlyCredits = opensourceMonthlyCredits
|
||||
self.plan = plan
|
||||
self.billingPeriodEnd = billingPeriodEnd
|
||||
self.subscriptionStatus = subscriptionStatus
|
||||
self.subscriptionEnrichmentUnavailable = subscriptionEnrichmentUnavailable
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
/// USD allocation for the active monthly grant (from the catalog).
|
||||
public var monthlyCreditsTotal: Double? {
|
||||
self.plan?.monthlyCreditsUSD
|
||||
}
|
||||
|
||||
/// USD spent in the current monthly grant (total – remaining), clamped to [0, total].
|
||||
public var monthlyCreditsUsed: Double? {
|
||||
guard let total = self.monthlyCreditsTotal else { return nil }
|
||||
return max(0, min(total, total - self.monthlyCreditsRemaining))
|
||||
}
|
||||
|
||||
public func toUsageSnapshot() -> UsageSnapshot {
|
||||
let primary = self.makePrimaryWindow()
|
||||
|
||||
let identity = ProviderIdentitySnapshot(
|
||||
providerID: .commandcode,
|
||||
accountEmail: nil,
|
||||
accountOrganization: nil,
|
||||
loginMethod: self.makeLoginMethod())
|
||||
|
||||
return UsageSnapshot(
|
||||
primary: primary,
|
||||
secondary: nil,
|
||||
tertiary: nil,
|
||||
providerCost: nil,
|
||||
commandCodeSubscriptionEnrichmentUnavailable: self.subscriptionEnrichmentUnavailable,
|
||||
commandCodeHasSubscriptionPlan: self.plan != nil,
|
||||
commandCodeMonthlyGrantDepleted: self.monthlyCreditsRemaining <= 0,
|
||||
updatedAt: self.updatedAt,
|
||||
identity: identity)
|
||||
}
|
||||
|
||||
private func makePrimaryWindow() -> RateWindow? {
|
||||
guard let total = self.monthlyCreditsTotal, total > 0 else {
|
||||
// Free / unknown plan with no allowance — surface 100% so the bar renders empty.
|
||||
if self.monthlyCreditsRemaining > 0 || self.purchasedCredits > 0 {
|
||||
return RateWindow(
|
||||
usedPercent: 0,
|
||||
windowMinutes: nil,
|
||||
resetsAt: self.billingPeriodEnd,
|
||||
resetDescription: nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let used = self.monthlyCreditsUsed ?? 0
|
||||
let percent = min(100, max(0, (used / total) * 100))
|
||||
return RateWindow(
|
||||
usedPercent: percent,
|
||||
windowMinutes: nil,
|
||||
resetsAt: self.billingPeriodEnd,
|
||||
resetDescription: nil)
|
||||
}
|
||||
|
||||
private func makeLoginMethod() -> String? {
|
||||
var parts: [String] = []
|
||||
if let name = self.plan?.displayName, !name.isEmpty {
|
||||
parts.append(name)
|
||||
}
|
||||
if let total = self.monthlyCreditsTotal {
|
||||
let used = self.monthlyCreditsUsed ?? 0
|
||||
parts.append("\(Self.formatUSD(used)) of \(Self.formatUSD(total))")
|
||||
} else if self.monthlyCreditsRemaining > 0 {
|
||||
parts.append("\(Self.formatUSD(self.monthlyCreditsRemaining)) remaining")
|
||||
}
|
||||
if self.purchasedCredits > 0 {
|
||||
parts.append("+ \(Self.formatUSD(self.purchasedCredits)) credits")
|
||||
}
|
||||
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
static func formatUSD(_ value: Double) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.currencyCode = "USD"
|
||||
formatter.locale = Locale(identifier: "en_US")
|
||||
formatter.maximumFractionDigits = value < 100 ? 2 : 0
|
||||
formatter.minimumFractionDigits = value < 100 ? 2 : 0
|
||||
return formatter.string(from: NSNumber(value: value)) ?? "$\(value)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user