diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7a69c98f..6d957bd5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixed - Ollama: recognize current WorkOS AuthKit sessions during browser-cookie discovery and manual cookie validation. Thanks @joeVenner! +- Ollama: classify current WorkOS sign-in redirects as expired sessions, enabling cookie-candidate fallback instead of a parser error. Thanks @joeVenner! ## 0.41.0 — 2026-07-06 diff --git a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift index 7cfc2464ec..d55b4b6325 100644 --- a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift @@ -532,6 +532,10 @@ public struct OllamaUsageFetcher: Sendable { statusCode: httpResponse.statusCode, url: httpResponse.response.url?.absoluteString ?? "unknown") + if httpResponse.statusCode == 200, Self.isSignInRedirect(httpResponse.response.url) { + throw OllamaUsageError.invalidCredentials + } + guard httpResponse.statusCode == 200 else { if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw OllamaUsageError.invalidCredentials @@ -628,6 +632,25 @@ public struct OllamaUsageFetcher: Sendable { if host == "ollama.com" || host == "www.ollama.com" { return true } return host.hasSuffix(".ollama.com") } + + static func isSignInRedirect(_ url: URL?) -> Bool { + guard url?.scheme?.lowercased() == "https" else { return false } + guard let url, let host = url.host?.lowercased() else { return false } + let path = url.path.lowercased() + if host == "ollama.com" || host == "www.ollama.com" { + return path == "/signin" + } + // WorkOS AuthKit ultimately bounces unauthenticated requests to a hosted + // Ollama sign-in page on the `signin.ollama.com` subdomain; any landing + // there means the session is expired and the user must sign in again. + if host == "signin.ollama.com" { + return true + } + // WorkOS AuthKit serves the hosted authorization flow from auth.workos.com + // (and historically api.workos.com); match any WorkOS host carrying the + // authorize path so the detection survives host changes or CNAMEs. + return host.hasSuffix(".workos.com") && path.hasPrefix("/user_management/authorize") + } } public struct OllamaAPISettingsReader: Sendable { diff --git a/Tests/CodexBarTests/OllamaUsageFetcherRetryMappingTests.swift b/Tests/CodexBarTests/OllamaUsageFetcherRetryMappingTests.swift index 80207015bc..749fd4662e 100644 --- a/Tests/CodexBarTests/OllamaUsageFetcherRetryMappingTests.swift +++ b/Tests/CodexBarTests/OllamaUsageFetcherRetryMappingTests.swift @@ -122,13 +122,7 @@ struct OllamaUsageFetcherRetryMappingTests { return Self.makeResponse(url: url, body: body, statusCode: 200) } - let fetcher = OllamaUsageFetcher( - browserDetection: BrowserDetection(cacheTTL: 0), - makeURLSession: { delegate in - let config = URLSessionConfiguration.ephemeral - config.protocolClasses = [OllamaRetryMappingStubURLProtocol.self] - return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) - }) + let fetcher = self.makeCookieFetcher() do { _ = try await fetcher.fetch( cookieHeaderOverride: "session=test-cookie", @@ -145,6 +139,70 @@ struct OllamaUsageFetcherRetryMappingTests { } } + @Test + func `workos sign in landing surfaces invalid credentials before parsing`() async throws { + defer { OllamaRetryMappingStubURLProtocol.handler = nil } + + let landingURL = try #require(URL( + string: "https://signin.ollama.com/?client_id=test&authorization_session_id=expired")) + OllamaRetryMappingStubURLProtocol.handler = { request in + #expect(request.url == URL(string: "https://ollama.com/settings")) + let body = "Sign in to Ollama" + return Self.makeResponse(url: landingURL, body: body, statusCode: 200) + } + + let fetcher = self.makeCookieFetcher() + do { + _ = try await fetcher.fetch( + cookieHeaderOverride: "session=expired-cookie", + manualCookieMode: true) + Issue.record("Expected OllamaUsageError.invalidCredentials") + } catch let error as OllamaUsageError { + guard case .invalidCredentials = error else { + Issue.record("Expected invalidCredentials, got \(error)") + return + } + } catch { + Issue.record("Expected OllamaUsageError.invalidCredentials, got \(error)") + } + } + + @Test + func `workos sign in service failure remains a network error`() async throws { + defer { OllamaRetryMappingStubURLProtocol.handler = nil } + + let landingURL = try #require(URL(string: "https://signin.ollama.com/")) + OllamaRetryMappingStubURLProtocol.handler = { _ in + Self.makeResponse(url: landingURL, body: "Service unavailable", statusCode: 503) + } + + let fetcher = self.makeCookieFetcher() + do { + _ = try await fetcher.fetch( + cookieHeaderOverride: "session=expired-cookie", + manualCookieMode: true) + Issue.record("Expected OllamaUsageError.networkError") + } catch let error as OllamaUsageError { + guard case let .networkError(message) = error else { + Issue.record("Expected networkError, got \(error)") + return + } + #expect(message == "HTTP 503") + } catch { + Issue.record("Expected OllamaUsageError.networkError, got \(error)") + } + } + + private func makeCookieFetcher() -> OllamaUsageFetcher { + OllamaUsageFetcher( + browserDetection: BrowserDetection(cacheTTL: 0), + makeURLSession: { delegate in + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [OllamaRetryMappingStubURLProtocol.self] + return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) + }) + } + private static func makeResponse( url: URL, body: String, diff --git a/Tests/CodexBarTests/OllamaUsageFetcherTests.swift b/Tests/CodexBarTests/OllamaUsageFetcherTests.swift index d53be31995..c2f141b39e 100644 --- a/Tests/CodexBarTests/OllamaUsageFetcherTests.swift +++ b/Tests/CodexBarTests/OllamaUsageFetcherTests.swift @@ -31,6 +31,27 @@ struct OllamaUsageFetcherTests { #expect(!OllamaUsageFetcher.shouldAttachCookie(to: URL(string: "http://app.ollama.com/path"))) } + @Test + func `recognizes current ollama sign in redirects`() { + #expect(OllamaUsageFetcher.isSignInRedirect(URL(string: "https://ollama.com/signin"))) + #expect(OllamaUsageFetcher.isSignInRedirect(URL( + string: "https://api.workos.com/user_management/authorize?client_id=test"))) + #expect(OllamaUsageFetcher.isSignInRedirect(URL( + string: "https://auth.workos.com/user_management/authorize?client_id=test"))) + // The real unauthenticated chain lands on the WorkOS-hosted Ollama sign-in + // page on the `signin.ollama.com` subdomain (verified live); that terminal + // landing must also classify as a sign-in redirect. + #expect(OllamaUsageFetcher.isSignInRedirect(URL( + string: "https://signin.ollama.com/?client_id=test&authorization_session_id=x"))) + #expect(!OllamaUsageFetcher.isSignInRedirect(URL(string: "https://ollama.com/settings"))) + #expect(!OllamaUsageFetcher.isSignInRedirect(URL(string: "https://api.workos.com/other"))) + #expect(!OllamaUsageFetcher.isSignInRedirect(URL(string: "http://ollama.com/signin"))) + #expect(!OllamaUsageFetcher.isSignInRedirect(URL( + string: "http://auth.workos.com/user_management/authorize?client_id=test"))) + #expect(!OllamaUsageFetcher.isSignInRedirect(URL( + string: "https://example.com/user_management/authorize?client_id=test"))) + } + @Test func `manual mode without valid header throws no session cookie`() { do { diff --git a/docs/ollama.md b/docs/ollama.md index 49b9df697f..e6c981e978 100644 --- a/docs/ollama.md +++ b/docs/ollama.md @@ -41,6 +41,8 @@ Ollama API keys currently do not expire, but they can be revoked from the key se - Cookie mode fetches `https://ollama.com/settings` using browser cookies. - Cookie discovery recognizes the current WorkOS AuthKit `wos-session` cookie alongside legacy Ollama and NextAuth session names. +- Redirects from settings to `/signin` or the WorkOS AuthKit authorization page are treated as expired sessions, so + CodexBar can try the next cookie candidate and show sign-in guidance instead of a parser error. - Parses: - Plan badge under **Cloud Usage**. - **Session usage** and **Weekly usage** percentages.