|
| 1 | +/** |
| 2 | + * Deno Deploy: CORS proxy for opencode.ai/zen |
| 3 | + * Forwards POST /zen/v1/chat/completions to opencode.ai |
| 4 | + * and adds Access-Control-Allow-Origin: * so Android WebViews can reach it. |
| 5 | + */ |
| 6 | + |
| 7 | +const TARGET_BASE = "https://opencode.ai"; |
| 8 | + |
| 9 | +const CORS_HEADERS: Record<string, string> = { |
| 10 | + "Access-Control-Allow-Origin": "*", |
| 11 | + "Access-Control-Allow-Methods": "POST, GET, OPTIONS", |
| 12 | + "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With", |
| 13 | + "Access-Control-Expose-Headers": "Content-Type, X-Request-Id", |
| 14 | + "Access-Control-Max-Age": "86400", |
| 15 | +}; |
| 16 | + |
| 17 | +Deno.serve(async (request: Request): Promise<Response> => { |
| 18 | + // CORS preflight – must respond immediately with 200 (not 204, some WebViews reject 204) |
| 19 | + if (request.method === "OPTIONS") { |
| 20 | + return new Response(null, { status: 200, headers: CORS_HEADERS }); |
| 21 | + } |
| 22 | + |
| 23 | + if (request.method !== "POST") { |
| 24 | + return new Response("Method not allowed", { status: 405, headers: CORS_HEADERS }); |
| 25 | + } |
| 26 | + |
| 27 | + const url = new URL(request.url); |
| 28 | + const targetUrl = TARGET_BASE + url.pathname + url.search; |
| 29 | + |
| 30 | + // Forward headers (pass Authorization through if present) |
| 31 | + const forwardHeaders = new Headers(); |
| 32 | + forwardHeaders.set("Content-Type", "application/json"); |
| 33 | + forwardHeaders.set("Accept", "text/event-stream, application/json"); |
| 34 | + const auth = request.headers.get("Authorization"); |
| 35 | + if (auth) forwardHeaders.set("Authorization", auth); |
| 36 | + |
| 37 | + let body: string; |
| 38 | + try { |
| 39 | + body = await request.text(); |
| 40 | + } catch (e) { |
| 41 | + const msg = e instanceof Error ? e.message : String(e); |
| 42 | + return new Response( |
| 43 | + JSON.stringify({ error: { message: "Failed to read request body: " + msg, type: "proxy_error" } }), |
| 44 | + { status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } } |
| 45 | + ); |
| 46 | + } |
| 47 | + |
| 48 | + let upstreamResponse: Response; |
| 49 | + try { |
| 50 | + upstreamResponse = await fetch(targetUrl, { |
| 51 | + method: "POST", |
| 52 | + headers: forwardHeaders, |
| 53 | + body, |
| 54 | + }); |
| 55 | + } catch (e) { |
| 56 | + const msg = e instanceof Error ? e.message : String(e); |
| 57 | + return new Response( |
| 58 | + JSON.stringify({ error: { message: "Proxy could not reach opencode.ai: " + msg, type: "proxy_error" } }), |
| 59 | + { status: 502, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } } |
| 60 | + ); |
| 61 | + } |
| 62 | + |
| 63 | + // Build response headers: start from upstream, overlay CORS headers |
| 64 | + const responseHeaders = new Headers(); |
| 65 | + // Copy safe upstream headers |
| 66 | + for (const [k, v] of upstreamResponse.headers.entries()) { |
| 67 | + const kl = k.toLowerCase(); |
| 68 | + if (kl === "content-type" || kl === "x-request-id" || kl === "transfer-encoding") { |
| 69 | + responseHeaders.set(k, v); |
| 70 | + } |
| 71 | + } |
| 72 | + // Always set CORS headers (overrides anything upstream sent) |
| 73 | + for (const [k, v] of Object.entries(CORS_HEADERS)) { |
| 74 | + responseHeaders.set(k, v); |
| 75 | + } |
| 76 | + |
| 77 | + return new Response(upstreamResponse.body, { |
| 78 | + status: upstreamResponse.status, |
| 79 | + headers: responseHeaders, |
| 80 | + }); |
| 81 | +}); |
0 commit comments