(options: {
baseURL: string;
getCookie: () => string;
onUnauthorized?: () => void;
})
| 15 | * before the user signs in. The server decides whether auth is required. |
| 16 | */ |
| 17 | export function buildAppSdkClient(options: { |
| 18 | baseURL: string; |
| 19 | getCookie: () => string; |
| 20 | onUnauthorized?: () => void; |
| 21 | }) { |
| 22 | const base = createAppClient({ |
| 23 | baseURL: options.baseURL, |
| 24 | headers: undefined, |
| 25 | }); |
| 26 | |
| 27 | return async <TData, TError = unknown, TVariables = unknown>( |
| 28 | config: RequestConfig<TVariables> |
| 29 | ): Promise<ResponseConfig<TData>> => { |
| 30 | const headers = new Headers(); |
| 31 | headers.set("Accept", "application/json"); |
| 32 | // Do NOT set credentials: "include" — we carry the cookie explicitly and |
| 33 | // `credentials: include` would trigger Electron's fetch CORS preflight. |
| 34 | const cookie = options.getCookie(); |
| 35 | if (cookie) { |
| 36 | headers.set("Cookie", cookie); |
| 37 | } |
| 38 | for (const [key, value] of new Headers( |
| 39 | (config.headers as HeadersInit | undefined) ?? undefined |
| 40 | ).entries()) { |
| 41 | headers.set(key, value); |
| 42 | } |
| 43 | |
| 44 | try { |
| 45 | return await base<TData, TError, TVariables>({ |
| 46 | ...config, |
| 47 | headers, |
| 48 | }); |
| 49 | } catch (error) { |
| 50 | const status = |
| 51 | error && typeof error === "object" && "status" in error |
| 52 | ? (error as { status?: number }).status |
| 53 | : undefined; |
| 54 | if (status === 401 || status === 403) { |
| 55 | options.onUnauthorized?.(); |
| 56 | // Re-throw with a diagnostic message so the caller can tell whether |
| 57 | // the request was even carrying a cookie. This is the single biggest |
| 58 | // source of "why am I 401" confusion on the desktop BFF path. |
| 59 | const hadCookie = cookie.length > 0; |
| 60 | const diagnostic = hadCookie |
| 61 | ? `sent Cookie header (${cookie.length} bytes) but server rejected it` |
| 62 | : "no Cookie header — Better-Auth session missing or expired. Sign in to desktop first."; |
| 63 | // The SDK base client attaches the parsed response body to `.data` |
| 64 | // on the error. Surface that so we can see Hono's actual reason. |
| 65 | let bodyDump = ""; |
| 66 | const errData = (error as { data?: unknown }).data; |
| 67 | if (errData !== undefined) { |
| 68 | try { |
| 69 | bodyDump = ` body=${JSON.stringify(errData)}`; |
| 70 | } catch { |
| 71 | bodyDump = ` body=<unserializable>`; |
| 72 | } |
| 73 | } |
| 74 | // Cookie name hint — shows what names are in the cookie header so we |
no test coverage detected