// The Cloudflare API passthrough for the site-hosted setup wizard: open CORS // on its own responses, an allowlist of API paths, and nothing kept. import { afterEach, describe, expect, it, vi } from 'vitest'; import { CF_API_PROXY_PREFIX, cfApiProxyEnabled, handleCfApiProxy, } from '../src/cf-api-proxy.js'; const BASE = `https://start.example.com${CF_API_PROXY_PREFIX.slice(0, -1)}`; afterEach(() => { vi.unstubAllGlobals(); }); describe('cf api proxy', () => { it('is off unless the flag says true', () => { expect(cfApiProxyEnabled({})).toBe(false); expect(cfApiProxyEnabled({ PDS_EXPERIMENTAL_CF_API_PROXY: '1' })).toBe( false, ); expect(cfApiProxyEnabled({ PDS_EXPERIMENTAL_CF_API_PROXY: 'true' })).toBe( true, ); }); it('answers the preflight with open CORS', async () => { const response = await handleCfApiProxy( new Request(`${BASE}/client/v4/accounts`, { method: 'OPTIONS' }), ); expect(response.status).toBe(204); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(response.headers.get('Access-Control-Allow-Headers')).toContain( 'Authorization', ); }); it('refuses paths outside the allowlist', async () => { for (const path of [ '/client/v4/user', '/client/v4/zones/abc123/settings', '/client/v4/zones/00000000000000000000000000000000/purge_cache', ]) { const response = await handleCfApiProxy( new Request(`${BASE}${path}`, { headers: { Authorization: 'Bearer t' }, }), ); expect(response.status, path).toBe(403); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); } }); it('allows the zone list and a zone dns_records path', async () => { const stub = vi.fn( async () => new Response('{"success":true}', { headers: { 'Content-Type': 'application/json' }, }), ); vi.stubGlobal('fetch', stub); for (const path of [ '/client/v4/zones', '/client/v4/zones/00000000000000000000000000000000/dns_records', '/client/v4/zones/00000000000000000000000000000000/dns_records/11111111111111111111111111111111', ]) { const response = await handleCfApiProxy( new Request(`${BASE}${path}`, { headers: { Authorization: 'Bearer t' }, }), ); expect(response.status, path).toBe(200); } }); it('requires a token', async () => { const response = await handleCfApiProxy( new Request(`${BASE}/client/v4/accounts`), ); expect(response.status).toBe(401); }); it('forwards an allowed call with the token and the query string', async () => { const stub = vi.fn( async () => new Response('{"success":true}', { headers: { 'Content-Type': 'application/json', 'Set-Cookie': 'sneaky=1', }, }), ); vi.stubGlobal('fetch', stub); const response = await handleCfApiProxy( new Request(`${BASE}/client/v4/accounts?page=2`, { headers: { Authorization: 'Bearer t' }, }), ); expect(response.status).toBe(200); expect(await response.json()).toEqual({ success: true }); expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); // The upstream cookie does not ride through. expect(response.headers.get('Set-Cookie')).toBe(null); const [url, init] = /** @type {[string, RequestInit]} */ ( /** @type {unknown} */ (stub.mock.calls[0]) ); expect(url).toBe('https://api.cloudflare.com/client/v4/accounts?page=2'); expect( /** @type {Record} */ (init.headers).Authorization, ).toBe('Bearer t'); }); it('forwards a PUT body', async () => { const stub = vi.fn(async (_url, /** @type {any} */ init) => { const echoed = await new Response(init.body).text(); return new Response(echoed, { headers: { 'Content-Type': 'application/json' }, }); }); vi.stubGlobal('fetch', stub); const response = await handleCfApiProxy( new Request(`${BASE}/client/v4/accounts/abc/workers/scripts/pds`, { method: 'PUT', headers: { Authorization: 'Bearer t', 'Content-Type': 'text/plain' }, body: 'module bytes', }), ); expect(await response.text()).toBe('module bytes'); }); });