Compare commits

...
Author SHA1 Message Date
SuperDooper 238614a94b beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:40 +02:00
SuperDooper b11507221f beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:38 +02:00
SuperDooper b6fef53264 beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:37 +02:00
SuperDooper ec6ae6621a beta.14: fix httpShouldHandleCookies=false so Cookie header is actually sent 2026-05-11 11:29:35 +02:00
github-actions[bot] 105a7706fa Beta release v1.2.1-beta.13 2026-05-11 09:21:33 +00:00
SuperDooper a950392a6f beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:51 +02:00
SuperDooper 8087cc029d beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:50 +02:00
SuperDooper 11f9e0c9b6 beta.13: wait for apiWebView didFinish before making JS fetch calls 2026-05-11 11:20:49 +02:00
github-actions[bot] cc835ee2b0 Beta release v1.2.1-beta.12 2026-05-11 09:03:50 +00:00
SuperDooper 8e7328b2c5 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:34 +02:00
SuperDooper 709eb12382 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:33 +02:00
SuperDooper e866324a48 beta.12: route all API calls through background WKWebView 2026-05-11 11:02:32 +02:00
SuperDooper a62584221b beta.12: route all API calls through background WKWebView 2026-05-11 11:02:30 +02:00
github-actions[bot] fc7fd19652 Beta release v1.2.1-beta.11 2026-05-11 08:47:11 +00:00
superdooper86 58b947e515 fix: add KVO on webView.url to catch SPA pushState navigation
didFinish only fires for cross-document (full page) navigations. After
loading https://claude.ai/login the SPA redirects authenticated users
via history.pushState to /new — this changes the URL visually but never
fires didFinish, so auth was never detected.

KVO on webView.url fires for every URL change including SPA pushState,
covering the case where the app routes client-side after the initial
page load. Both KVO and didFinish now call the same checkCurrentURL
helper so detection is not missed regardless of navigation type.
2026-05-11 10:46:11 +02:00
github-actions[bot] 88252e4d59 Beta release v1.2.1-beta.10 2026-05-11 08:36:44 +00:00
superdooper86 ad2ff3a7b6 fix: revert to URL-based auth detection; remove browser headers
Every JS/cookie-based detection approach failed. Reverting to the
simplest reliable mechanism: if the WebView navigates to any non-login,
non-auth URL, the server redirected us after sign-in — fire onAuthenticated.

Also removing the browser headers added in beta.6. The 1.1.4 version
worked without them and they may be triggering server-side bot detection.
All-cookies approach (beta.8) is kept.
2026-05-11 10:35:30 +02:00
github-actions[bot] 1a9d9cd22a Beta release v1.2.1-beta.9 2026-05-11 08:23:48 +00:00
superdooper86 b8826ace9b fix: NSNumber cast and use .page world in callAsyncJavaScript auth check
callAsyncJavaScript returns JS numbers as NSNumber (Double-backed).
'val as? Int' silently returns nil for 200.0, so onAuthenticated never
fired. Fixed with 'val as? NSNumber then .intValue == 200'.

Also switched content world from .defaultClient to .page so the fetch
runs in the same JS context as the loaded page.
2026-05-11 10:22:59 +02:00
github-actions[bot] 2a0de269ff Beta release v1.2.1-beta.8 2026-05-11 08:07:51 +00:00
6 changed files with 54 additions and 58 deletions
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.2.1-beta.8</string> <string>1.2.1-beta.14</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>56</string> <string>62</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+18 -20
View File
@@ -12,6 +12,12 @@ struct LoginWebView: NSViewRepresentable {
let webView = WKWebView(frame: .zero, configuration: config) let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator webView.navigationDelegate = context.coordinator
// KVO on url catches SPA pushState navigations that don't fire didFinish
context.coordinator.urlObservation = webView.observe(\.url, options: [.new]) { [weak coordinator = context.coordinator] wv, _ in
coordinator?.checkCurrentURL(wv.url?.absoluteString)
}
webView.load(URLRequest(url: URL(string: "https://claude.ai/login")!)) webView.load(URLRequest(url: URL(string: "https://claude.ai/login")!))
return webView return webView
} }
@@ -25,33 +31,25 @@ struct LoginWebView: NSViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate { class Coordinator: NSObject, WKNavigationDelegate {
let onAuthenticated: () -> Void let onAuthenticated: () -> Void
var didAuthenticate = false var didAuthenticate = false
var urlObservation: NSKeyValueObservation?
init(onAuthenticated: @escaping () -> Void) { init(onAuthenticated: @escaping () -> Void) {
self.onAuthenticated = onAuthenticated self.onAuthenticated = onAuthenticated
} }
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { func checkCurrentURL(_ url: String?) {
guard !didAuthenticate else { return } guard !didAuthenticate, let url else { return }
// Don't fire on the login/auth pages themselves if url.contains("/login") || url.contains("/auth") { return }
if let url = webView.url?.absoluteString, didAuthenticate = true
url.contains("/login") || url.contains("/auth") { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.onAuthenticated()
// Ask the WebView itself whether we're authenticated it uses its own
// session (cookies, localStorage, etc.) so we don't need to know the
// cookie domain or name.
webView.callAsyncJavaScript(
"const r = await fetch('/api/bootstrap', {credentials: 'include'}); return r.status;",
arguments: [:], in: nil, in: .defaultClient
) { [weak self] result in
guard let self, !self.didAuthenticate else { return }
if case .success(let val) = result, let status = val as? Int, status == 200 {
self.didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.onAuthenticated()
}
}
} }
} }
// Covers full cross-document navigations
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
checkCurrentURL(webView.url?.absoluteString)
}
} }
} }
+23 -23
View File
@@ -126,27 +126,39 @@ class UsageViewModel: ObservableObject {
} }
} }
// MARK: - Bootstrap (org ID + email + plan label in one call) // MARK: - HTTP helpers
// Builds a URLRequest with browser-like headers and cookies from WKWebsiteDataStore.
//
// httpShouldHandleCookies MUST be false: when true, URLSession replaces any manually-set
// Cookie header with its own HTTPCookieStorage (which is empty claude.ai cookies live in
// WKWebsiteDataStore, not HTTPCookieStorage), causing every request to go out with no cookies.
private func claudeAPIRequest(for url: URL) async -> URLRequest { private func claudeAPIRequest(for url: URL) async -> URLRequest {
var req = URLRequest(url: url) var req = URLRequest(url: url)
req.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept") req.httpShouldHandleCookies = false
req.setValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent") req.setValue("application/json", forHTTPHeaderField: "accept")
req.setValue("https://claude.ai", forHTTPHeaderField: "Origin") req.setValue("https://claude.ai", forHTTPHeaderField: "origin")
req.setValue("https://claude.ai/", forHTTPHeaderField: "Referer") req.setValue("https://claude.ai/", forHTTPHeaderField: "referer")
req.setValue("same-origin", forHTTPHeaderField: "sec-fetch-site") req.setValue(
req.setValue("cors", forHTTPHeaderField: "sec-fetch-mode") "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
req.setValue("empty", forHTTPHeaderField: "sec-fetch-dest") forHTTPHeaderField: "user-agent")
if let cookie = await claudeCookieHeader() { if let cookie = await claudeCookieHeader() {
req.setValue(cookie, forHTTPHeaderField: "Cookie") req.setValue(cookie, forHTTPHeaderField: "cookie")
} }
return req return req
} }
private func claudeCookieHeader() async -> String? {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
guard !cookies.isEmpty else { return nil }
return HTTPCookie.requestHeaderFields(with: cookies)["Cookie"]
}
// MARK: - Bootstrap (org ID + email + plan label in one call)
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) { private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
let url = URL(string: "https://claude.ai/api/bootstrap")! let url = URL(string: "https://claude.ai/api/bootstrap")!
var req = await claudeAPIRequest(for: url) let req = await claudeAPIRequest(for: url)
guard req.value(forHTTPHeaderField: "Cookie") != nil else { throw AppError.notAuthenticated }
let (data, response) = try await URLSession.shared.data(for: req) let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse else { throw AppError.networkError } guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated } if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
@@ -156,17 +168,14 @@ class UsageViewModel: ObservableObject {
} }
let account = json["account"] as? [String: Any] let account = json["account"] as? [String: Any]
// memberships may live under account or at root (older API shape)
let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]] let memberships = (account?["memberships"] ?? json["memberships"]) as? [[String: Any]]
let firstOrg = memberships?.first?["organization"] as? [String: Any] let firstOrg = memberships?.first?["organization"] as? [String: Any]
// org ID primary path then flat-list fallback then dedicated endpoint
var orgId: String? = firstOrg?["uuid"] as? String var orgId: String? = firstOrg?["uuid"] as? String
if orgId == nil { if orgId == nil {
orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String
} }
if orgId == nil { if orgId == nil {
// Final fallback: fetch /api/organizations directly
let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!) let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!)
if let (orgsData, orgsResp) = try? await URLSession.shared.data(for: orgsReq), if let (orgsData, orgsResp) = try? await URLSession.shared.data(for: orgsReq),
let orgsHttp = orgsResp as? HTTPURLResponse, orgsHttp.statusCode == 200, let orgsHttp = orgsResp as? HTTPURLResponse, orgsHttp.statusCode == 200,
@@ -177,7 +186,6 @@ class UsageViewModel: ObservableObject {
let email = account?["email_address"] as? String let email = account?["email_address"] as? String
// plan label from capabilities e.g. "claude_pro" -> "Pro"
var planLabel: String? = nil var planLabel: String? = nil
if let caps = firstOrg?["capabilities"] as? [String], if let caps = firstOrg?["capabilities"] as? [String],
let cap = caps.first(where: { $0.hasPrefix("claude_") }) { let cap = caps.first(where: { $0.hasPrefix("claude_") }) {
@@ -190,14 +198,6 @@ class UsageViewModel: ObservableObject {
// MARK: - Fetch usage // MARK: - Fetch usage
private func claudeCookieHeader() async -> String? {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
guard !cookies.isEmpty else { return nil }
// Send all cookies from the app's WebView store the session token may be
// on any domain (claude.ai, anthropic.com, or an auth sub-service).
return HTTPCookie.requestHeaderFields(with: cookies)["Cookie"]
}
private func fetchUsage(orgId: String) async throws -> UsageResponse { private func fetchUsage(orgId: String) async throws -> UsageResponse {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")! let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")!
let req = await claudeAPIRequest(for: url) let req = await claudeAPIRequest(for: url)
+1 -1
View File
@@ -10,7 +10,7 @@
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) [![Version](https://img.shields.io/badge/version-1.2.0-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.7-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.7) <!-- BETA_BADGE --> [![Beta](https://img.shields.io/badge/beta-1.2.1--beta.13-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.13) <!-- BETA_BADGE -->
</div> </div>
+7 -9
View File
@@ -1,12 +1,10 @@
## What's new in v1.2.1 ## What's new in v1.2.1
### Bug fixes ### Bug fixes
- Fixed "Not signed in" showing incorrectly on launch when the session was already active - Fixed the root cause of "Not signed in" errors: URLSession was silently discarding the manually-set Cookie header because `httpShouldHandleCookies` defaults to `true`, which makes URLSession replace it with its own (empty) HTTPCookieStorage — claude.ai session cookies live in WKWebsiteDataStore, not HTTPCookieStorage. Setting `httpShouldHandleCookies = false` ensures the cookies are actually sent.
- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes - Added browser-like request headers (User-Agent, Origin, Referer) matching what Claude's API expects, consistent with the working Windows implementation
- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect - Removed background WKWebView complexity added in beta.1213 — reverted to simple URLSession approach with correct cookie handling
- Fixed "No API key configured" showing after signing out — now correctly shows "Not signed in" with a prompt to sign in - Fixed login window auto-closing before the user could sign in — login window loads `/login` so auth is only detected after the actual sign-in redirect
- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it - Fixed login detection for Next.js SPA navigation using KVO on WebView URL (history.pushState doesn't trigger didFinish)
- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent) - Added `/api/organizations` as a final fallback for org ID resolution
- Fixed Settings incorrectly showing "Signed in" after a failed refresh — sign-in state now resets when authentication fails - Fixed Settings incorrectly showing "Signed in" after a failed refresh
- Rewrote login detection to use the WebView's own fetch call instead of inspecting cookie domains — correctly detects auth regardless of which domain the session token is stored on
- Fixed API requests not including session cookies — now sends all cookies from the app's WebView store rather than filtering by domain
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.2.1-beta.7", "version": "1.2.1-beta.13",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.7/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.13/ClaudeChecker.zip",
"notes": "## What's new in v1.2.1\n\n### Bug fixes\n- Fixed \"Not signed in\" showing incorrectly on launch when the session was already active\n- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes\n- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect\n- Fixed \"No API key configured\" showing after signing out — now correctly shows \"Not signed in\" with a prompt to sign in\n- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it\n- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)\n- Fixed sign-in detection and cookie handling for accounts whose session cookies are on the `anthropic.com` domain rather than `claude.ai`\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh — sign-in state now resets when authentication fails" "notes": "## What's new in v1.2.1\n\n### Bug fixes\n- Fixed a timing race in the background API WebView — `refresh()` now correctly waits for the WebView to finish loading before making API calls, preventing silent failures on startup\n- Fixed usage data not loading after sign-in — API calls now run inside a persistent background WebView using the page's own fetch(), so all credentials (cookies, httpOnly tokens, etc.) are included automatically\n- Fixed \"Not signed in\" showing after login — the background WebView is now reloaded after sign-in to pick up the new session before the first data refresh\n- Fixed \"Not signed in\" showing incorrectly on launch when the session was already active\n- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes\n- Fixed login window auto-closing before the user could sign in — the login window now correctly loads the `/login` page so it only detects auth after the actual sign-in redirect\n- Fixed \"No API key configured\" showing after signing out — now correctly shows \"Not signed in\" with a prompt to sign in\n- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it\n- Fixed Settings incorrectly showing \"Signed in\" after a failed refresh — sign-in state now resets when authentication fails\n- Rewrote login detection to use KVO on the WebView URL — correctly detects auth for Next.js SPA navigation (history.pushState) that doesn't trigger didFinish"
} }