diff --git a/frontend/editor/src/core/services/apiClientSetup.ts b/frontend/editor/src/core/services/apiClientSetup.ts index 326978717..d30d6be0a 100644 --- a/frontend/editor/src/core/services/apiClientSetup.ts +++ b/frontend/editor/src/core/services/apiClientSetup.ts @@ -13,7 +13,11 @@ export function setupApiInterceptors(client: AxiosInstance): void { ); } -/** Auth headers for raw fetch() calls (SSE streams, etc.). Proprietary overrides with JWT + XSRF. */ -export function getAuthHeaders(): Record { +/** + * Auth headers for raw fetch() calls (SSE streams, etc.). + * Proprietary overrides with stored JWT + XSRF; SaaS overrides with the live + * Supabase access token. Async because SaaS has to consult the auth client. + */ +export async function getAuthHeaders(): Promise> { return {}; } diff --git a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx index 65c2e9dd3..2e3caf33d 100644 --- a/frontend/editor/src/proprietary/components/chat/ChatContext.tsx +++ b/frontend/editor/src/proprietary/components/chat/ChatContext.tsx @@ -467,7 +467,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { { method: "POST", body: formData, - headers: getAuthHeaders(), + headers: await getAuthHeaders(), credentials: "include", signal: controller.signal, }, diff --git a/frontend/editor/src/proprietary/services/apiClientSetup.ts b/frontend/editor/src/proprietary/services/apiClientSetup.ts index d11f02ba9..02708a4a5 100644 --- a/frontend/editor/src/proprietary/services/apiClientSetup.ts +++ b/frontend/editor/src/proprietary/services/apiClientSetup.ts @@ -100,8 +100,11 @@ async function refreshAuthToken(client: AxiosInstance): Promise { } } -/** Auth headers for raw fetch() calls (SSE streams, etc.). */ -export function getAuthHeaders(): Record { +/** + * Auth headers for raw fetch() calls (SSE streams, etc.). + * Async to match the SaaS override, which has to await the Supabase session. + */ +export async function getAuthHeaders(): Promise> { const headers: Record = {}; const jwt = getJwtTokenFromStorage(); if (jwt) { @@ -117,8 +120,8 @@ export function getAuthHeaders(): Record { export function setupApiInterceptors(client: AxiosInstance): void { // Install request interceptor to add JWT token client.interceptors.request.use( - (config) => { - const authHeaders = getAuthHeaders(); + async (config) => { + const authHeaders = await getAuthHeaders(); for (const [key, value] of Object.entries(authHeaders)) { if (!config.headers[key]) { config.headers[key] = value; diff --git a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx index a6c2123ba..cd0142e3c 100644 --- a/frontend/editor/src/prototypes/components/chat/ChatContext.tsx +++ b/frontend/editor/src/prototypes/components/chat/ChatContext.tsx @@ -446,7 +446,7 @@ export function ChatProvider({ children }: { children: ReactNode }) { { method: "POST", body: formData, - headers: getAuthHeaders(), + headers: await getAuthHeaders(), credentials: "include", signal: controller.signal, }, diff --git a/frontend/editor/src/saas/services/apiClientSetup.ts b/frontend/editor/src/saas/services/apiClientSetup.ts new file mode 100644 index 000000000..e84fb6bb9 --- /dev/null +++ b/frontend/editor/src/saas/services/apiClientSetup.ts @@ -0,0 +1,41 @@ +import type { AxiosInstance } from "axios"; +import { supabase } from "@app/auth/supabase"; + +/** + * SaaS auth headers for raw fetch() calls (e.g. AI chat streaming). + * + * Pulls the live Supabase access token. Required because the SaaS apiClient's + * axios interceptor attaches this header to every axios call, but raw fetch() + * calls bypass that path and end up with no Authorization header → backend + * returns 401. The chat streaming endpoint uses fetch() (not axios) because + * axios doesn't stream SSE responses well, so this override exists to give + * it the same bearer token the axios calls already get. + * + * supabase.auth.getSession() reads from in-memory cache when possible; only + * issues a network request if the session needs refreshing. Adds an Accept + * header so the backend negotiates JSON correctly. + */ +export async function getAuthHeaders(): Promise> { + const headers: Record = {}; + try { + const { + data: { session }, + } = await supabase.auth.getSession(); + if (session?.access_token) { + headers["Authorization"] = `Bearer ${session.access_token}`; + } + } catch (e) { + console.warn("[apiClientSetup] Failed to read Supabase session", e); + } + return headers; +} + +/** + * SaaS apiClient wires up its own interceptors inline (see saas/services/apiClient.ts). + * This re-export exists so the cascade through @app/services/apiClientSetup + * remains consistent for callers that import setupApiInterceptors — currently + * none in SaaS mode, but keeps the shape uniform. + */ +export function setupApiInterceptors(_client: AxiosInstance): void { + // No-op: SaaS apiClient handles its own interceptors with the Supabase session. +}