Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions Sources/CodexBarCore/Providers/Grok/GrokStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,36 @@ public struct GrokStatusProbe: Sendable {
let localSummary = GrokLocalSessionScanner.summarize(env: env)
let cliVersion = Self.detectVersion(env: env)

// Grok session tokens expire after ~7 days. An expired record on disk
// must be treated like a missing credential when deciding whether to
// mask the RPC auth error: otherwise we render a snapshot with stale
// identity and no `grok login` hint while billing silently 401s.
let activeCredentials = credentials.flatMap { $0.isExpired ? nil : $0 }

// `localSummary` is *not* currently projected into a visible RateWindow or
// identity field, so a stale `~/.grok/sessions/` directory must not
// suppress the auth-required hint. Only swallow the RPC error when we
// actually have something rendrable for the user — credentials or a
// billing response.
if billing == nil, credentials == nil {
// actually have something renderable for the user — fresh credentials
// or a billing response.
if billing == nil, activeCredentials == nil {
throw rpcError ?? GrokRPCError.notAuthenticated
}

return GrokUsageSnapshot(
billing: billing,
credentials: credentials,
credentials: Self.credentialsForSnapshot(credentials: credentials, billing: billing),
localSummary: localSummary,
cliVersion: cliVersion,
updatedAt: Date())
}

static func credentialsForSnapshot(
credentials: GrokCredentials?,
billing: GrokBillingResponse?) -> GrokCredentials?
{
// If billing succeeded, the CLI accepted/refreshed auth and the local
// identity is still useful even when the persisted expires_at is stale.
if billing != nil { return credentials }
return credentials.flatMap { $0.isExpired ? nil : $0 }
}
}
59 changes: 59 additions & 0 deletions Tests/CodexBarTests/GrokAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,65 @@ struct GrokAuthTests {
}
}

@Test
func `isExpired reflects past expires_at`() throws {
// Past expiry
let pastJson = #"""
{
"https://auth.x.ai::client": {
"key": "stale-token",
"expires_at": "2020-01-01T00:00:00Z"
}
}
"""#
let past = try GrokCredentialsStore.parse(data: Data(pastJson.utf8))
#expect(past.isExpired == true)

// Future expiry
let futureJson = #"""
{
"https://auth.x.ai::client": {
"key": "fresh-token",
"expires_at": "2099-01-01T00:00:00Z"
}
}
"""#
let future = try GrokCredentialsStore.parse(data: Data(futureJson.utf8))
#expect(future.isExpired == false)

// Missing expires_at — treated as non-expired so we never spuriously lock
// out clients whose auth.json shape predates this field.
let noExpiryJson = #"""
{
"https://auth.x.ai::client": {
"key": "ageless-token"
}
}
"""#
let noExpiry = try GrokCredentialsStore.parse(data: Data(noExpiryJson.utf8))
#expect(noExpiry.isExpired == false)
}

@Test
func `expired credentials are preserved when billing succeeds`() throws {
let pastJson = #"""
{
"https://auth.x.ai::client": {
"key": "stale-token",
"email": "grok@example.com",
"team_id": "team_123",
"expires_at": "2020-01-01T00:00:00Z"
}
}
"""#
let expired = try GrokCredentialsStore.parse(data: Data(pastJson.utf8))
let billing = try JSONDecoder().decode(GrokBillingResponse.self, from: Data(#"{}"#.utf8))

#expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: nil) == nil)
#expect(GrokStatusProbe.credentialsForSnapshot(credentials: expired, billing: billing)?
.email == "grok@example.com")
}

@Test
func `falls back to legacy when OIDC entry has no key`() throws {
// A stale/partial OIDC record must not shadow a healthy legacy session.
Expand Down
1 change: 1 addition & 0 deletions Tests/CodexBarTests/SettingsStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,7 @@ struct SettingsStoreTests {
.commandcode,
.stepfun,
.bedrock,
.grok,
])

// Move one provider; ensure it's persisted across instances.
Expand Down