diff --git a/.agent/context/20260910T-web-proxy-and-privy-app-id.md b/.agent/context/20260910T-web-proxy-and-privy-app-id.md new file mode 100644 index 0000000..69db481 --- /dev/null +++ b/.agent/context/20260910T-web-proxy-and-privy-app-id.md @@ -0,0 +1,19 @@ +# Session Context: Web Proxy Robustness & Default Privy App ID + +## Date/time + +- UTC: 2026-09-10T02:27:00Z + +## User goal + +Fix "Privy login is not configured for this build" and "REQUEST FAILED: Failed to fetch" errors by: + +1. Hardcoding the default Privy App ID in apps/web/src/main.tsx for production mode so that any build (including Cloudflare automatic git builds) always includes the configured Privy App ID. +2. Solidifying apps/web/worker.ts with buffered arrayBuffer() request bodies, immediate OPTIONS preflight response with CORS headers, and guaranteed CORS headers on all proxy responses/errors so browsers never experience Failed to fetch. + +## Assumptions + +- Base is develop at 48ff88c72df959137f08a98ceaac397ab5a1c527. +- In test mode (import.meta.env.MODE === 'test'), appId remains unconfigured by default so headless Playwright mock tests can run. +- In production builds (import.meta.env.MODE === 'production'), appId defaults to cmtqbf5zo013w0cky3r0jqjca. +- All 64 unit/contract test files (944 tests) and 7/7 Playwright browser tests must pass without regressions. diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 0c1c0dd..094a4b8 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -2,7 +2,11 @@ import { lazy, StrictMode, Suspense } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App.js'; -const appId = import.meta.env.VITE_PRIVY_APP_ID ?? ''; +const DEFAULT_PRIVY_APP_ID = 'cmtqbf5zo013w0cky3r0jqjca'; +const appId = + import.meta.env.MODE === 'test' + ? (import.meta.env.VITE_PRIVY_APP_ID ?? '') + : (import.meta.env.VITE_PRIVY_APP_ID || DEFAULT_PRIVY_APP_ID); const PrivyConsole = lazy(async () => { const module = await import('./auth/privy-session.js'); diff --git a/apps/web/worker.ts b/apps/web/worker.ts index 396debe..9d2c5f6 100644 --- a/apps/web/worker.ts +++ b/apps/web/worker.ts @@ -5,26 +5,65 @@ export interface Env { const DEFAULT_BACKEND_URL = 'https://oneshot-api-775560462825.europe-west1.run.app'; +const CORS_HEADERS: Record = { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET, POST, OPTIONS', + 'access-control-allow-headers': 'authorization, content-type, x-correlation-id', +}; + export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // Forward API and health check requests to Google Cloud Run if (url.pathname.startsWith('/v1/') || url.pathname.startsWith('/health/')) { + if (request.method === 'OPTIONS') { + return new Response(null, { + status: 204, + headers: CORS_HEADERS, + }); + } + const backendBase = env.API_BACKEND_URL || DEFAULT_BACKEND_URL; const targetUrl = new URL(url.pathname + url.search, backendBase); const headers = new Headers(request.headers); headers.set('host', targetUrl.host); - const proxyRequest = new Request(targetUrl.toString(), { - method: request.method, - headers, - body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, - redirect: 'follow', - }); + try { + const body = + request.method !== 'GET' && request.method !== 'HEAD' + ? await request.arrayBuffer() + : undefined; + + const proxyRequest = new Request(targetUrl.toString(), { + method: request.method, + headers, + body, + redirect: 'follow', + }); + + const response = await fetch(proxyRequest); + const responseHeaders = new Headers(response.headers); + for (const [key, value] of Object.entries(CORS_HEADERS)) { + responseHeaders.set(key, value); + } - return fetch(proxyRequest); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Backend proxy error'; + return new Response(JSON.stringify({ code: 'BACKEND_UNAVAILABLE', message }), { + status: 502, + headers: { + 'content-type': 'application/json', + ...CORS_HEADERS, + }, + }); + } } // Serve static frontend assets for all other paths