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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
318 lines · 10.9 KB Raw History
// The wire adapter's write half and the server's own routes: record and
// blob writes carry the request's token, the identity route answers, and
// the static surface falls back to the app shell.

import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createAppViewStore } from '@pdsjs/appview/store';
import { cborDecode, cborEncodeDagCbor } from '@pdsjs/core/repo';
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it } from 'vitest';
import { createForgeAuth } from '../src/auth.js';
import { createRemoteAccount } from '../src/remote.js';
import { createGitHostServer } from '../src/server.js';

const ORIGIN = 'http://git.example.com';
const DID = 'did:plc:forgeowner';

/** @type {Array<{close: () => Promise<void>}>} */
const closers = [];
/** @type {string[]} */
const dirs = [];

afterEach(async () => {
  for (const closer of closers.splice(0)) await closer.close().catch(() => {});
  for (const dir of dirs.splice(0))
    rmSync(dir, { recursive: true, force: true });
});

describe('remote account writes', () => {
  it('carries the request token on a record write', async () => {
    /** @type {string[]} */
    const seen = [];
    const account = createRemoteAccount({
      service: 'https://pds.example.com',
      account: DID,
      getToken: async () => 'the-token',
      fetch: async (input, init) => {
        const url = String(input);
        const headers = /** @type {Record<string, string>} */ (
          init?.headers ?? {}
        );
        seen.push(`${url} ${headers.authorization ?? ''}`);
        if (url.endsWith('/putRecord')) {
          const body = JSON.parse(String(init?.body));
          return Response.json({ uri: body.record.name }, { status: 200 });
        }
        return Response.json({ did: DID, handle: 'forge.test' });
      },
    });
    await account.putRecord(
      `at://${DID}/dev.pdsjs.git.repo/proj`,
      'dev.pdsjs.git.repo',
      'proj',
      cborEncodeDagCbor({ name: 'proj' }),
    );
    expect(seen).toHaveLength(1);
    expect(seen[0]).toContain('Bearer the-token');
  });

  it('refuses a failed record write with the server answer', async () => {
    const account = createRemoteAccount({
      service: 'https://pds.example.com',
      account: DID,
      fetch: async () => Response.json({ error: 'IsARepo' }, { status: 422 }),
    });
    await expect(
      account.putRecord(
        `at://${DID}/dev.pdsjs.git.repo/proj`,
        'dev.pdsjs.git.repo',
        'proj',
        cborEncodeDagCbor({}),
      ),
    ).rejects.toThrow(/refused/);
  });

  it('round-trips a record read through the PDS JSON', async () => {
    const account = createRemoteAccount({
      service: 'https://pds.example.com',
      account: DID,
      fetch: async (input) => {
        const url = String(input);
        if (url.includes('getRecord')) {
          return Response.json({ cid: 'bafy', value: { name: 'proj' } });
        }
        return Response.json({ did: DID, handle: 'forge.test' });
      },
    });
    const row = await account.getRecord(`at://${DID}/dev.pdsjs.git.repo/proj`);
    // The port's shape is the CBOR-encoded value, which the write path
    // decodes again.
    expect(row?.cid).toBe('bafy');
    expect(cborDecode(/** @type {Uint8Array} */ (row?.value))).toEqual({
      name: 'proj',
    });
    expect(await account.getRecord('not-a-uri')).toBe(null);
  });

  it('uploads a blob as a form with the token', async () => {
    /** @type {string[]} */
    const seen = [];
    const account = createRemoteAccount({
      service: 'https://pds.example.com',
      account: DID,
      getToken: async () => 'blob-token',
      fetch: async (_input, init) => {
        const headers = /** @type {Record<string, string>} */ (
          init?.headers ?? {}
        );
        seen.push(String(headers.authorization ?? ''));
        return Response.json({ blob: { cid: 'bafyblob' } });
      },
    });
    await account.uploadBlob(new Uint8Array([1, 2, 3]), 'application/zip');
    expect(seen).toEqual(['Bearer blob-token']);
  });

  it('reads a blob range with the header the caller asked for', async () => {
    /** @type {string[]} */
    const ranges = [];
    const account = createRemoteAccount({
      service: 'https://pds.example.com',
      account: DID,
      fetch: async (_input, init) => {
        const headers = /** @type {Record<string, string>} */ (
          init?.headers ?? {}
        );
        ranges.push(String(headers.range ?? ''));
        return new Response(new Uint8Array([1, 2, 3]), {
          headers: { 'content-type': 'application/octet-stream' },
        });
      },
    });
    const found = await account.getBlobStream(DID, 'bafyblob', {
      start: 10,
      end: 20,
    });
    expect(ranges).toEqual(['bytes=10-20']);
    expect(found?.body).toBeTruthy();
  });
});

describe('git host server routes', () => {
  it('serves the static app with a shell fallback', async () => {
    const dir = mkdtempSync(join(tmpdir(), 'git-host-static-'));
    dirs.push(dir);
    writeFileSync(
      join(dir, 'index.html'),
      '<html><title>git-host</title></html>',
    );
    writeFileSync(join(dir, 'app.js'), 'console.log(1)');
    const server = createGitHostServer({
      account: createRemoteAccount({
        service: 'https://pds.example.com',
        account: DID,
        fetch: async () => Response.json({ did: DID, handle: 'forge.test' }),
      }),
      store: createAppViewStore(new Database(':memory:')),
      uiDir: dir,
      origin: ORIGIN,
    });
    closers.push(server);
    await server.listen(2524);
    const base = 'http://127.0.0.1:2524';

    const page = await fetch(`${base}/repos`);
    expect(await page.text()).toContain('git-host');
    const script = await fetch(`${base}/app.js`);
    expect(script.headers.get('content-type')).toContain('text/javascript');
    const traversal = await fetch(`${base}/../etc/passwd`);
    expect([200, 404]).toContain(traversal.status);
  });

  it('answers the blob route from the PDS', async () => {
    const server = createGitHostServer({
      account: createRemoteAccount({
        service: 'https://pds.example.com',
        account: DID,
        fetch: async (input) => {
          const url = String(input);
          if (url.includes('getBlob')) {
            return new Response(new Uint8Array([9, 9]), {
              headers: { 'content-type': 'application/octet-stream' },
            });
          }
          return Response.json({ did: DID, handle: 'forge.test' });
        },
      }),
      store: createAppViewStore(new Database(':memory:')),
      origin: ORIGIN,
    });
    closers.push(server);
    await server.listen(2525);
    const base = 'http://127.0.0.1:2525';

    const blob = await fetch(`${base}/.blobs/bafyblob`);
    expect(blob.status).toBe(206);
    expect(blob.headers.get('accept-ranges')).toBe('bytes');
    expect(new Uint8Array(await blob.arrayBuffer())).toEqual(
      new Uint8Array([9, 9]),
    );
    const missing = await fetch(`${base}/.blobs/none`);
    // The PDS answers 404 for an unknown blob and the route passes it on.
    expect([404, 206]).toContain(missing.status);
  });

  it('issues the login redirect against the account handle', async () => {
    /** @type {string} */
    let parBody = '';
    /** @type {typeof fetch} */
    const fakeOauth = async (input, init) => {
      const url = String(input);
      if (url.includes('par')) {
        parBody = String(init?.body ?? '');
        return Response.json({ request_uri: 'urn:par' });
      }
      if (url.includes('oauth-authorization-server')) {
        return Response.json({
          issuer: 'https://pds.example.com',
          pushed_authorization_request_endpoint: 'https://pds.example.com/par',
          authorization_endpoint: 'https://pds.example.com/oauth/authorize',
          token_endpoint: 'https://pds.example.com/oauth/token',
        });
      }
      return Response.json({});
    };
    const server = createGitHostServer({
      account: createRemoteAccount({
        service: 'https://pds.example.com',
        account: DID,
        fetch: async () => Response.json({ did: DID, handle: 'forge.test' }),
      }),
      store: createAppViewStore(new Database(':memory:')),
      auth: createForgeAuth({
        origin: ORIGIN,
        accountDid: DID,
        service: 'https://pds.example.com',
        fetch: fakeOauth,
      }),
      origin: ORIGIN,
    });
    closers.push(server);
    await server.listen(2526);
    const base = 'http://127.0.0.1:2526';

    const login = await fetch(`${base}/login?handle=${DID}`, {
      redirect: 'manual',
    });
    expect(login.status).toBe(302);
    const location = login.headers.get('location') ?? '';
    expect(location).toContain('request_uri=urn%3Apar');
    expect(parBody).toContain('client_id=');
  });

  it('describes the xrpc methods from the route table', async () => {
    const server = createGitHostServer({
      account: createRemoteAccount({
        service: 'https://pds.example.com',
        account: DID,
        fetch: async () => Response.json({ did: DID, handle: 'forge.test' }),
      }),
      store: createAppViewStore(new Database(':memory:')),
      origin: ORIGIN,
    });
    closers.push(server);
    await server.listen(2527);
    const base = 'http://127.0.0.1:2527';

    const describe = await fetch(
      `${base}/xrpc/community.lexicon.service.describe`,
    );
    expect(describe.ok).toBe(true);
    const body =
      /** @type {{roles: string[], methods: Array<{value: string}>}} */ (
        await describe.json()
      );
    expect(body.roles).toEqual(['appview']);
    const methods = body.methods.map(({ value }) => value);
    expect(methods).toContain('dev.pdsjs.git.listRepos');
    expect(methods).toEqual([...methods].sort());
  });

  it('passes a requested blob range through to the PDS', async () => {
    /** @type {string[]} */
    const ranges = [];
    const server = createGitHostServer({
      account: createRemoteAccount({
        service: 'https://pds.example.com',
        account: DID,
        fetch: async (input, init) => {
          const url = String(input);
          const headers = /** @type {Record<string, string>} */ (
            init?.headers ?? {}
          );
          if (url.includes('getBlob')) {
            ranges.push(String(headers.range ?? ''));
            return new Response(new Uint8Array([1, 2, 3, 4]), {
              headers: { 'content-type': 'application/octet-stream' },
            });
          }
          return Response.json({ did: DID, handle: 'forge.test' });
        },
      }),
      store: createAppViewStore(new Database(':memory:')),
      origin: ORIGIN,
    });
    closers.push(server);
    await server.listen(2528);
    const base = 'http://127.0.0.1:2528';

    const blob = await fetch(`${base}/.blobs/bafyranged`, {
      headers: { Range: 'bytes=1-2' },
    });
    expect(ranges).toEqual(['bytes=1-2']);
    expect(blob.status).toBe(206);
    expect(blob.headers.get('content-range')).toBe('bytes 1-2');
  });
});