chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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');
});
});