~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
134 lines · 4.3 KB Raw History
// 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<string, string>} */ (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');
  });
});