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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
85 lines · 2.8 KB Raw History
// @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<Response>}
 */
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<string, string>} */
  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',
    },
  });
}