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.
This commit is contained in:
@@ -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.5</string>
|
<string>1.2.1-beta.6</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>53</string>
|
<string>54</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.0</string>
|
<string>13.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -128,12 +128,25 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Bootstrap (org ID + email + plan label in one call)
|
// MARK: - Bootstrap (org ID + email + plan label in one call)
|
||||||
|
|
||||||
|
private func claudeAPIRequest(for url: URL) async -> URLRequest {
|
||||||
|
var req = URLRequest(url: url)
|
||||||
|
req.setValue("application/json, text/plain, */*", forHTTPHeaderField: "accept")
|
||||||
|
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("https://claude.ai", forHTTPHeaderField: "Origin")
|
||||||
|
req.setValue("https://claude.ai/", forHTTPHeaderField: "Referer")
|
||||||
|
req.setValue("same-origin", forHTTPHeaderField: "sec-fetch-site")
|
||||||
|
req.setValue("cors", forHTTPHeaderField: "sec-fetch-mode")
|
||||||
|
req.setValue("empty", forHTTPHeaderField: "sec-fetch-dest")
|
||||||
|
if let cookie = await claudeCookieHeader() {
|
||||||
|
req.setValue(cookie, forHTTPHeaderField: "Cookie")
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
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 = URLRequest(url: url)
|
var req = await claudeAPIRequest(for: url)
|
||||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
guard req.value(forHTTPHeaderField: "Cookie") != nil else { throw AppError.notAuthenticated }
|
||||||
guard let cookie = await claudeCookieHeader() else { throw AppError.notAuthenticated }
|
|
||||||
req.setValue(cookie, forHTTPHeaderField: "Cookie")
|
|
||||||
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 }
|
||||||
@@ -152,11 +165,9 @@ class UsageViewModel: ObservableObject {
|
|||||||
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, let cookie = await claudeCookieHeader() {
|
if orgId == nil {
|
||||||
// Final fallback: fetch /api/organizations directly
|
// Final fallback: fetch /api/organizations directly
|
||||||
var orgsReq = URLRequest(url: URL(string: "https://claude.ai/api/organizations")!)
|
let orgsReq = await claudeAPIRequest(for: URL(string: "https://claude.ai/api/organizations")!)
|
||||||
orgsReq.setValue("application/json", forHTTPHeaderField: "accept")
|
|
||||||
orgsReq.setValue(cookie, forHTTPHeaderField: "Cookie")
|
|
||||||
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,
|
||||||
let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] {
|
let orgs = try? JSONSerialization.jsonObject(with: orgsData) as? [[String: Any]] {
|
||||||
@@ -187,9 +198,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
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")!
|
||||||
var req = URLRequest(url: url)
|
let req = await claudeAPIRequest(for: url)
|
||||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
|
||||||
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
|
|
||||||
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 }
|
||||||
@@ -199,9 +208,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
|
private func fetchPrepaidCredits(orgId: String) async throws -> PrepaidCredits? {
|
||||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
|
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/prepaid/credits")!
|
||||||
var req = URLRequest(url: url)
|
let req = await claudeAPIRequest(for: url)
|
||||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
|
||||||
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
|
|
||||||
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, http.statusCode == 200 else { return nil }
|
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
|
||||||
return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
|
return try? JSONDecoder().decode(PrepaidCredits.self, from: data)
|
||||||
@@ -209,9 +216,7 @@ class UsageViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
|
private func fetchOverageSpendLimit(orgId: String) async throws -> OverageSpendLimit? {
|
||||||
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
|
let url = URL(string: "https://claude.ai/api/organizations/\(orgId)/overage_spend_limit")!
|
||||||
var req = URLRequest(url: url)
|
let req = await claudeAPIRequest(for: url)
|
||||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
|
||||||
if let cookie = await claudeCookieHeader() { req.setValue(cookie, forHTTPHeaderField: "Cookie") }
|
|
||||||
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, http.statusCode == 200 else { return nil }
|
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { return nil }
|
||||||
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
|
return try? JSONDecoder().decode(OverageSpendLimit.self, from: data)
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
- 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 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
|
- 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
|
- Added `/api/organizations` as a final fallback for org ID resolution when the bootstrap API response doesn't include it
|
||||||
|
- Fixed usage data not loading after sign-in — API requests now include required browser-like headers (Origin, Referer, User-Agent) that Claude's usage endpoints require
|
||||||
|
|||||||
Reference in New Issue
Block a user