Compare commits

...
Author SHA1 Message Date
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
superdooper86 ac7ffe81af fix: use WebView JS fetch for auth detection; send all cookies to API
Cookie domain filtering was wrong — the session token domain is unknown
and was never found by claude.ai/anthropic.com filters.

LoginView: replace getAllCookies domain check with callAsyncJavaScript
that fetches /api/bootstrap directly from the WebView. The WebView uses
its own full session (all cookies, any domain) so auth is detected
correctly regardless of where the token lives.

UsageViewModel: claudeCookieHeader now sends all cookies from the app's
WKWebsiteDataStore instead of filtering by domain. checkInitialSignInState
likewise checks for any cookie.
2026-05-11 10:06:33 +02:00
github-actions[bot] f28f7b8a5a Beta release v1.2.1-beta.7 2026-05-11 07:51:25 +00:00
superdooper86 aaed64484c fix: include anthropic.com cookies in all auth checks and API requests
Claude session cookies are on anthropic.com, not claude.ai. The login
window was not detecting auth (Cancel stayed, no Done) and API calls
were sent without the actual session token.

- claudeCookieHeader: include anthropic.com cookies so the token is
  sent to the usage/bootstrap endpoints
- checkInitialSignInState: detect anthropic.com cookies on startup
- didFinish in LoginView: fire auth when anthropic.com cookies found
- signOut: clear anthropic.com data alongside claude.ai
- notAuthenticated catch: set isSignedIn = false so Settings stays
  in sync with the main panel
2026-05-11 09:49:57 +02:00
github-actions[bot] 2e3ee350ca Beta release v1.2.1-beta.6 2026-05-11 07:37:40 +00:00
superdooper86 e939bdb88b fix: add browser headers to all API requests to resolve 403 on usage endpoint
Claude's usage/prepaid/overage endpoints require Origin, Referer, and
User-Agent headers to pass CORS/auth checks. Without them, bootstrap
succeeds (more permissive) but usage returns 403 -> 'Not signed in'.

Added claudeAPIRequest(for:) helper that sets all required browser-like
headers on every request. Bootstrap, usage, prepaid, overage, and the
orgs fallback all go through it.
2026-05-11 09:36:35 +02:00
github-actions[bot] 6749c5f158 Beta release v1.2.1-beta.5 2026-05-11 07:27:17 +00:00
superdooper86 de71ec2794 fix: load /login instead of root so premature auth detection is prevented
Loading https://claude.ai as the start URL caused didFinish to fire on
the landing page while stale/tracking cookies were already in
WKWebsiteDataStore. The 'any claude.ai cookie' check then fired
immediately, closing the login sheet before the user could sign in.

Loading /login ensures the URL-guard catches the initial page load and
only checks cookies after the real post-login redirect.
2026-05-11 09:25:45 +02:00
github-actions[bot] ac17ce154f Beta release v1.2.1-beta.4 2026-05-11 06:59:20 +00:00
6 changed files with 53 additions and 45 deletions
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.1-beta.4</string>
<string>1.2.1-beta.11</string>
<key>CFBundleVersion</key>
<string>52</string>
<string>59</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
+19 -15
View File
@@ -12,7 +12,13 @@ struct LoginWebView: NSViewRepresentable {
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: URL(string: "https://claude.ai")!))
// 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")!))
return webView
}
@@ -25,27 +31,25 @@ struct LoginWebView: NSViewRepresentable {
class Coordinator: NSObject, WKNavigationDelegate {
let onAuthenticated: () -> Void
var didAuthenticate = false
var urlObservation: NSKeyValueObservation?
init(onAuthenticated: @escaping () -> Void) {
self.onAuthenticated = onAuthenticated
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard !didAuthenticate else { return }
// Don't fire on the login/auth pages themselves
if let url = webView.url?.absoluteString,
url.contains("/login") || url.contains("/auth") { return }
// URL is not a login/auth page, so if any claude.ai cookie exists we're signed in
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
let hasAnyCookie = cookies.contains { $0.domain.contains("claude.ai") }
guard hasAnyCookie, !self.didAuthenticate else { return }
self.didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.onAuthenticated()
}
func checkCurrentURL(_ url: String?) {
guard !didAuthenticate, let url else { return }
if url.contains("/login") || url.contains("/auth") { return }
didAuthenticate = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.onAuthenticated()
}
}
// Covers full cross-document navigations
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
checkCurrentURL(webView.url?.absoluteString)
}
}
}
+23 -22
View File
@@ -43,15 +43,14 @@ class UsageViewModel: ObservableObject {
private func checkInitialSignInState() async {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
let hasAnyCookie = cookies.contains { $0.domain.contains("claude.ai") }
if hasAnyCookie { isSignedIn = true }
if !cookies.isEmpty { isSignedIn = true }
}
func signOut() async {
let store = WKWebsiteDataStore.default()
let types = WKWebsiteDataStore.allWebsiteDataTypes()
let records = await store.dataRecords(ofTypes: types)
let claudeRecords = records.filter { $0.displayName.contains("claude.ai") }
let claudeRecords = records.filter { $0.displayName.contains("claude.ai") || $0.displayName.contains("anthropic.com") }
await store.removeData(ofTypes: types, for: claudeRecords)
UserDefaults.standard.removeObject(forKey: "claude_org_id")
isSignedIn = false
@@ -109,6 +108,7 @@ class UsageViewModel: ObservableObject {
checkLimitNotifications(for: limits)
} catch AppError.notAuthenticated {
isNotAuthenticated = true
isSignedIn = false
errorMessage = "Not signed in"
} catch let error as DecodingError {
switch error {
@@ -128,12 +128,19 @@ class UsageViewModel: ObservableObject {
// MARK: - Bootstrap (org ID + email + plan label in one call)
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
let url = URL(string: "https://claude.ai/api/bootstrap")!
private func claudeAPIRequest(for url: URL) async -> URLRequest {
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
guard let cookie = await claudeCookieHeader() else { throw AppError.notAuthenticated }
req.setValue(cookie, forHTTPHeaderField: "Cookie")
if let cookie = await claudeCookieHeader() {
req.setValue(cookie, forHTTPHeaderField: "Cookie")
}
return req
}
private func fetchBootstrap() async throws -> (orgId: String?, email: String?, planLabel: String?) {
let url = URL(string: "https://claude.ai/api/bootstrap")!
var 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)
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
@@ -152,11 +159,9 @@ class UsageViewModel: ObservableObject {
if orgId == nil {
orgId = (json["organizations"] as? [[String: Any]])?.first?["uuid"] as? String
}
if orgId == nil, let cookie = await claudeCookieHeader() {
if orgId == nil {
// Final fallback: fetch /api/organizations directly
var orgsReq = URLRequest(url: URL(string: "https://claude.ai/api/organizations")!)
orgsReq.setValue("application/json", forHTTPHeaderField: "accept")
orgsReq.setValue(cookie, forHTTPHeaderField: "Cookie")
let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!)
if let (orgsData, orgsResp) = try? await URLSession.shared.data(for: orgsReq),
let orgsHttp = orgsResp as? HTTPURLResponse, orgsHttp.statusCode == 200,
let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] {
@@ -181,15 +186,15 @@ class UsageViewModel: ObservableObject {
private func claudeCookieHeader() async -> String? {
let cookies = await WKWebsiteDataStore.default().httpCookieStore.allCookies()
let claudeCookies = cookies.filter { $0.domain.contains("claude.ai") }
return HTTPCookie.requestHeaderFields(with: claudeCookies)["Cookie"]
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 {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/usage")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let req = await claudeAPIRequest(for: url)
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse else { throw AppError.networkError }
if http.statusCode == 401 || http.statusCode == 403 { throw AppError.notAuthenticated }
@@ -199,9 +204,7 @@ class UsageViewModel: ObservableObject {
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let req = await claudeAPIRequest(for: url)
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
@@ -209,9 +212,7 @@ class UsageViewModel: ObservableObject {
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
var req = URLRequest(url: url)
req.setValue("application/json", forHTTPHeaderField: "accept")
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
let req = await claudeAPIRequest(for: url)
let (data, response) = try await URLSession.shared.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
+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)
[![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)
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.3-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.3) <!-- BETA_BADGE -->
[![Beta](https://img.shields.io/badge/beta-1.2.1--beta.10-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases/tag/v1.2.1-beta.10) <!-- BETA_BADGE -->
</div>
+5 -2
View File
@@ -3,7 +3,10 @@
### Bug fixes
- Fixed "Not signed in" showing incorrectly on launch when the session was already active
- Sign-in state is now detected immediately from stored cookies on startup, before the first data refresh completes
- Fixed Re-authenticate (and Sign In) not detecting an existing session — the login window now loads `claude.ai` directly so already-signed-in users are detected correctly and the window auto-dismisses
- 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
- Fixed "No API key configured" showing after signing out — now correctly shows "Not signed in" with a prompt to sign in
- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it
- Session cookie detection now checks both `sessionKey` and `__Secure-next-auth.session-token` cookie names
- Fixed usage data not loading — API requests now include required browser-like headers (Origin, Referer, User-Agent)
- Fixed Settings incorrectly showing "Signed in" after a failed refresh — sign-in state now resets when authentication fails
- 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.3",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.3/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 Re-authenticate (and Sign In) not detecting an existing session — the login window now loads `claude.ai` directly so already-signed-in users are detected correctly and the window auto-dismisses\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- Session cookie detection now checks both `sessionKey` and `__Secure-next-auth.session-token` cookie names"
"version": "1.2.1-beta.10",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.2.1-beta.10/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 Settings incorrectly showing \"Signed in\" after a failed refresh — sign-in state now resets when authentication fails\n- 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\n- Fixed API requests not including session cookies — now sends all cookies from the app's WebView store rather than filtering by domain"
}