// @pdsjs/cloudflare/cf-api-proxy - a passthrough to api.cloudflare.com for // the setup wizard, which runs as a static site and cannot call the API from // the browser: api.cloudflare.com answers no CORS preflight. The proxy // forwards the caller's Authorization header, allows only the paths the // wizard uses, and keeps nothing. Enabled by PDS_EXPERIMENTAL_CF_API_PROXY; // the prefix answers on every hostname the worker serves, so a site page // reaches it same-origin. const CF_API = 'https://api.cloudflare.com'; // Everything the wizard touches hangs off the visitor's own account: their // account paths, their zone list, and DNS records in a zone they name (the // handle-verification TXT record). Zone and record ids are 32 hex chars. const PATH_ALLOWED = /^\/client\/v4\/(accounts(\/|$)|oauth\/scopes$|zones$|zones\/[a-f0-9]{32}\/dns_records(\/[a-f0-9]{32})?$)/; export const CF_API_PROXY_PREFIX = '/cf-api/'; const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Max-Age': '86400', }; /** * @param {{PDS_EXPERIMENTAL_CF_API_PROXY?: string}} env */ export function cfApiProxyEnabled(env) { return env.PDS_EXPERIMENTAL_CF_API_PROXY === 'true'; } /** * @param {unknown} body * @param {number} status */ function json(body, status) { return Response.json(body, { status, headers: CORS_HEADERS }); } /** * Handle a request whose path starts with the proxy prefix. * @param {Request} request * @returns {Promise} */ export async function handleCfApiProxy(request) { if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: CORS_HEADERS }); } const url = new URL(request.url); const apiPath = url.pathname.slice(CF_API_PROXY_PREFIX.length - 1); if (!PATH_ALLOWED.test(apiPath)) { return json({ error: 'PathNotAllowed', message: 'Path not proxied' }, 403); } const auth = request.headers.get('Authorization'); if (!auth) { return json( { error: 'AuthenticationRequired', message: 'Missing token' }, 401, ); } /** @type {Record} */ const headers = { Authorization: auth }; const contentType = request.headers.get('Content-Type'); if (contentType) headers['Content-Type'] = contentType; const response = await fetch(`${CF_API}${apiPath}${url.search}`, { method: request.method, headers, body: request.method === 'GET' || request.method === 'HEAD' ? undefined : request.body, }); // A fresh Response strips Set-Cookie and lets the body stream through. return new Response(response.body, { status: response.status, headers: { ...CORS_HEADERS, 'Content-Type': response.headers.get('Content-Type') ?? 'text/plain', }, }); }