// Typed wrapper around $fetch with the configured API base. // // Adds `Authorization: Bearer ` when the caller is signed in, // and on a 401 transparently asks `/auth/refresh` for a new token and retries // once. Failed refresh clears local auth state — pages can rely on the // returned error to redirect to /login. export async function useApi( path: string, opts: { method?: string; body?: unknown; query?: Record } = {}, ): Promise { const config = useRuntimeConfig() const base = config.public.apiBase as string const auth = useAuth() const request = async (token: string | null): Promise => { const headers: Record = {} // Let the browser set Content-Type (with the multipart boundary) when // the body is FormData / Blob; otherwise default to JSON. const isMultipart = typeof FormData !== 'undefined' && opts.body instanceof FormData if (!isMultipart) headers['Content-Type'] = 'application/json' if (token) headers.Authorization = `Bearer ${token}` return await $fetch(path, { baseURL: base, method: (opts.method ?? 'GET') as any, body: opts.body as any, query: opts.query, headers, credentials: 'include', }) } try { return await request(auth.liveAccessToken()) } catch (err: any) { const status = err?.response?.status ?? err?.statusCode // 402 Payment Required — plan limit hit. Surface the backend's // upgrade payload on a global state slot; the UpgradeModal in // app.vue reads it and prompts the host to upgrade. We still // rethrow so the caller can stop its own UI flow if it wants. if (status === 402) { const data = err?.data if (data && data.upgrade_url) { useBilling().showUpgradePrompt(data) } throw err } if (status !== 401) throw err // /auth/* endpoints set the cookie themselves — never retry-refresh them. if (path.startsWith('/auth/')) throw err const refreshed = await auth.refresh() if (!refreshed) throw err return await request(auth.liveAccessToken()) } }