chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The extension seam: a feature package mounts on the server through a
// factory core calls with a host, and contributes routes, a handler for
// paths outside XRPC, a write guard and a commit tap.
import { describe, expect, it, vi } from 'vitest';
import { createAccountJwt } from '../packages/core/src/auth.js';
import { PersonalDataServer } from '../packages/core/src/pds.js';
const HOSTNAME = 'pds.example.com';
const BASE = `https://${HOSTNAME}`;
const JWT_SECRET = 'test-secret';
/**
* @param {Object} [options]
* @param {import('../packages/core/src/ports.js').ExtensionFactory[]} [options.extensions]
* @param {import('../packages/core/src/ports.js').RecordWriteGuardPort} [options.writeGuard]
* @param {import('../packages/core/src/ports.js').CommitTapPort} [options.onCommit]
*/
function createServer({ extensions, writeGuard, onCommit } = {}) {
return new PersonalDataServer({
actorStorage: /** @type {any} */ ({
async getDid() {
return 'did:plc:extensiontest';
},
async getPrivateKey() {
return null;
},
async getRecord() {
return null;
},
}),
sharedStorage: /** @type {any} */ ({}),
blobs: /** @type {any} */ ({}),
jwtSecret: JWT_SECRET,
hostname: HOSTNAME,
extensions,
writeGuard,
onCommit,
});
}
describe('extension routes', () => {
it('serves a route the extension registers, with CORS decoration', async () => {
const pds = createServer({
extensions: [
() => ({
name: 'demo',
routes: {
'/xrpc/dev.example.demo.ping': {
handler: async () => Response.json({ pong: true }),
},
},
}),
],
});
const res = await pds.fetch(
new Request(`${BASE}/xrpc/dev.example.demo.ping`),
);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ pong: true });
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*');
});
it('lists an extension XRPC method in describe', async () => {
const pds = createServer({
extensions: [
() => ({
name: 'demo',
routes: {
'/xrpc/dev.example.demo.ping': {
handler: async () => Response.json({}),
},
},
}),
],
});
const res = await pds.fetch(
new Request(`${BASE}/xrpc/community.lexicon.service.describe`),
);
const { methods } = await res.json();
expect(methods).toContainEqual({
$type: 'community.lexicon.service.describe#nsid',
value: 'dev.example.demo.ping',
});
});
it('refuses a route core already serves', () => {
expect(() =>
createServer({
extensions: [
() => ({
name: 'demo',
routes: {
'/xrpc/_health': { handler: async () => Response.json({}) },
},
}),
],
}),
).toThrow(/demo registers \/xrpc\/_health/);
});
it('refuses a route another extension registered', () => {
const factory = () => ({
name: 'twin',
routes: {
'/xrpc/dev.example.twin': { handler: async () => Response.json({}) },
},
});
expect(() => createServer({ extensions: [factory, factory] })).toThrow(
/twin registers \/xrpc\/dev.example.twin/,
);
});
});
describe('extension handlers', () => {
it('answers before core dispatch and owns the whole response', async () => {
const pds = createServer({
extensions: [
() => ({
name: 'registry',
prefixes: ['/registry'],
handle: async () => new Response('from the extension'),
}),
],
});
const hit = await pds.fetch(new Request(`${BASE}/registry/thing`));
expect(await hit.text()).toBe('from the extension');
expect(hit.headers.get('Access-Control-Allow-Origin')).toBeNull();
const miss = await pds.fetch(new Request(`${BASE}/xrpc/_health`));
expect(miss.status).toBe(200);
expect(miss.headers.get('Access-Control-Allow-Origin')).toBe('*');
});
it('only sees requests under its own prefixes', async () => {
const handle = vi.fn(async () => null);
const pds = createServer({
extensions: [() => ({ name: 'scoped', prefixes: ['/a', '/b/'], handle })],
});
for (const path of ['/a', '/a/x', '/b/y']) {
await pds.fetch(new Request(BASE + path));
}
expect(handle).toHaveBeenCalledTimes(3);
for (const path of ['/ab', '/xrpc/_health', '/', '/c/a']) {
await pds.fetch(new Request(BASE + path));
}
expect(handle).toHaveBeenCalledTimes(3);
});
it('falls through to core when the handler returns null', async () => {
const pds = createServer({
extensions: [
() => ({
name: 'quiet',
prefixes: ['/quiet'],
handle: async () => null,
}),
],
});
const res = await pds.fetch(new Request(`${BASE}/quiet/nothing`));
expect(res.status).toBe(404);
});
it('needs prefixes to go with a handler', () => {
expect(() =>
createServer({
extensions: [() => ({ name: 'bare', handle: async () => null })],
}),
).toThrow(/bare has a handler but declares no prefixes/);
});
it('refuses a prefix core serves under', () => {
for (const prefix of [
'/xrpc',
'/xrpc/com.atproto.server.createSession',
'/oauth/token',
'/account',
'/.well-known',
'/init',
'/',
'npm',
]) {
expect(
() =>
createServer({
extensions: [
() => ({
name: 'grab',
prefixes: [prefix],
handle: async () => null,
}),
],
}),
prefix,
).toThrow(/grab declares prefix/);
}
});
it('refuses a prefix that overlaps another extension', () => {
/** @param {string} name @param {string} prefix */
const claim = (name, prefix) => () => ({
name,
prefixes: [prefix],
handle: async () => null,
});
expect(() =>
createServer({
extensions: [claim('one', '/reg'), claim('two', '/reg/x')],
}),
).toThrow(/two declares prefix \/reg\/x, which overlaps \/reg/);
expect(() =>
createServer({
extensions: [claim('one', '/reg/x'), claim('two', '/reg')],
}),
).toThrow(/two declares prefix \/reg, which overlaps \/reg\/x/);
// Siblings are fine.
expect(() =>
createServer({
extensions: [claim('one', '/reg'), claim('two', '/registry')],
}),
).not.toThrow();
});
});
describe('extension route binding', () => {
it('calls the handler without the server as this', async () => {
/** @type {unknown} */
let bound = 'unset';
const pds = createServer({
extensions: [
() => ({
name: 'probe',
routes: {
'/xrpc/dev.example.probe': {
handler: function () {
bound = this;
return Response.json({});
},
},
},
}),
],
});
await pds.fetch(new Request(`${BASE}/xrpc/dev.example.probe`));
expect(bound).toBeUndefined();
});
it('keeps the route method and auth requirement', async () => {
const pds = createServer({
extensions: [
() => ({
name: 'strict',
routes: {
'/xrpc/dev.example.strict': {
method: 'POST',
auth: 'required',
handler: () => Response.json({}),
},
},
}),
],
});
const wrongMethod = await pds.fetch(
new Request(`${BASE}/xrpc/dev.example.strict`),
);
expect(wrongMethod.status).toBe(405);
const anonymous = await pds.fetch(
new Request(`${BASE}/xrpc/dev.example.strict`, { method: 'POST' }),
);
expect(anonymous.status).toBe(401);
});
});
describe('extension host xrpc', () => {
it('drops the credentials a request carries before dispatch', async () => {
/** @type {string[]} */
const seen = [];
/** @type {import('../packages/core/src/ports.js').ExtensionHost|null} */
let host = null;
createServer({
extensions: [
(h) => {
host = h;
return {
name: 'echo',
routes: {
'/xrpc/dev.example.echo': {
handler: (request) => {
for (const name of [
'authorization',
'cookie',
'dpop',
'x-keep',
]) {
if (request.headers.has(name)) seen.push(name);
}
return Response.json({});
},
},
},
};
},
],
});
const seenHost =
/** @type {import('../packages/core/src/ports.js').ExtensionHost} */ (
/** @type {unknown} */ (host)
);
const res = await seenHost.xrpc(
new Request(`${BASE}/xrpc/dev.example.echo`, {
headers: {
authorization: 'Bearer secret',
cookie: 'pdsjs_account=secret',
dpop: 'proof',
'x-keep': '1',
},
}),
);
expect(res.status).toBe(200);
expect(seen).toEqual(['x-keep']);
});
});
describe('extension host', () => {
it('late-binds getDid, xrpc and createCommit into the server', async () => {
/** @type {import('../packages/core/src/ports.js').ExtensionHost|null} */
let host = null;
const pds = createServer({
extensions: [
(h) => {
host = h;
return { name: 'probe' };
},
],
});
const seen =
/** @type {import('../packages/core/src/ports.js').ExtensionHost} */ (
/** @type {unknown} */ (host)
);
expect(await seen.getDid()).toBe('did:plc:extensiontest');
expect(seen.hostname).toBe(HOSTNAME);
expect(seen.spaceStorage).toBeUndefined();
expect(seen.spaceBrowser).toBeUndefined();
expect(seen.didResolver).toBeUndefined();
expect(seen.writeGuard.collections).toEqual([]);
await expect(seen.getSigner()).rejects.toThrow(/no signing key/);
expect(seen.plcUrl).toBe('https://plc.directory');
expect(seen.blobUploadLimit).toBe(50 * 1024 * 1024);
expect(seen.readOnly).toBe(false);
const res = await seen.xrpc(new Request(`${BASE}/xrpc/_health`));
expect(res.status).toBe(200);
// accountApi runs behind the owner's session, which this request lacks.
const denied = await seen.accountApi(
new Request(`${BASE}/account/api/x`),
new URL(`${BASE}/account/api/x`),
async () => ({}),
);
expect(denied.status).toBe(401);
expect(
await seen.readAccountSession(new Request(`${BASE}/xrpc/x`)),
).toBeNull();
const jwt = await createAccountJwt('did:plc:extensiontest', JWT_SECRET);
expect(
await seen.readAccountSession(
new Request(`${BASE}/xrpc/x`, {
headers: { cookie: `pdsjs_account=${encodeURIComponent(jwt)}` },
}),
),
).toBe('did:plc:extensiontest');
expect(
await seen.readJsonBounded(
new Request(`${BASE}/xrpc/x`, {
method: 'POST',
body: JSON.stringify({ a: 1 }),
}),
),
).toEqual({ a: 1 });
expect(pds.extensions.map((e) => e.name)).toEqual(['probe']);
});
});
describe('extension collections', () => {
const uri = (/** @type {string} */ collection) =>
`at://did:plc:extensiontest/${collection}/1`;
/** @param {string[]|undefined} collections */
function mount(collections) {
/** @type {import('../packages/core/src/ports.js').ExtensionHost|null} */
let host = null;
createServer({
extensions: [
(h) => {
host = h;
return { name: 'scoped', collections };
},
],
});
return /** @type {import('../packages/core/src/ports.js').ExtensionHost} */ (
/** @type {unknown} */ (host)
);
}
it('lets an extension read its own collections and refuses the rest', async () => {
const host = mount(['app.example.post']);
await expect(
host.actorStorage.getRecord(uri('app.example.post')),
).resolves.toBeNull();
await expect(
host.actorStorage.getRecord(uri('app.example.like')),
).rejects.toThrow(
'Extension scoped may not touch app.example.like: it declares app.example.post',
);
});
it('refuses every record call to an extension that declares none', async () => {
const host = mount(undefined);
await expect(host.actorStorage.getRecord(uri('a.b'))).rejects.toThrow(
/declares no collections/,
);
// The account's own fields are not records.
expect(await host.actorStorage.getDid()).toBe('did:plc:extensiontest');
});
it('refuses a commit that names a collection outside the scope', async () => {
const host = mount(['app.example.post']);
await expect(
host.createCommit('did:plc:extensiontest', [
{ action: 'delete', uri: uri('app.example.like') },
]),
).rejects.toThrow(/may not touch app.example.like/);
});
it('reads the declaration from the mounted extension, not the factory', async () => {
const host = mount(['*']);
await expect(
host.actorStorage.getRecord(uri('any.thing')),
).resolves.toBeNull();
});
});
describe('write guards', () => {
/** @param {string} collection */
const guard = (collection) => ({
collections: [collection],
check: vi.fn(async () => {}),
});
it('is the platform guard alone when no extension adds one', () => {
const platform = guard('app.example.one');
const pds = createServer({ writeGuard: platform });
expect(pds.writeGuard).toBe(platform);
});
it('consults each guard for writes in its own collections', async () => {
const platform = guard('app.example.one');
const extension = guard('app.example.two');
const pds = createServer({
writeGuard: platform,
extensions: [() => ({ name: 'two', writeGuard: extension })],
});
const composed = /** @type {NonNullable<typeof pds.writeGuard>} */ (
pds.writeGuard
);
expect(composed.collections).toEqual([
'app.example.one',
'app.example.two',
]);
const write = {
space: null,
collection: 'app.example.two',
rkey: 'a',
prevValue: null,
nextValue: {},
};
await composed.check(write);
expect(platform.check).not.toHaveBeenCalled();
expect(extension.check).toHaveBeenCalledWith(write);
});
it('lets any guard refuse the write', async () => {
const refusing = {
collections: ['app.example.one'],
check: async () => {
throw new Error('refused');
},
};
const pds = createServer({
writeGuard: guard('app.example.one'),
extensions: [() => ({ name: 'refuser', writeGuard: refusing })],
});
await expect(
/** @type {NonNullable<typeof pds.writeGuard>} */ (pds.writeGuard).check({
space: null,
collection: 'app.example.one',
rkey: 'a',
prevValue: null,
nextValue: {},
}),
).rejects.toThrow('refused');
});
});
describe('commit taps', () => {
const event = { did: 'did:plc:extensiontest', seq: 1, rev: '3l', ops: [] };
it('runs the platform tap and every extension tap', async () => {
const platform = vi.fn();
const extension = vi.fn();
const pds = createServer({
onCommit: platform,
extensions: [() => ({ name: 'tap', onCommit: extension })],
});
await pds.notifyCommitTaps(event);
expect(platform).toHaveBeenCalledWith(event);
expect(extension).toHaveBeenCalledWith(event);
});
it('logs a failing tap and still runs the rest', async () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
const after = vi.fn();
const pds = createServer({
extensions: [
() => ({
name: 'broken',
onCommit: () => {
throw new Error('tap failed');
},
}),
() => ({ name: 'fine', onCommit: after }),
],
});
await pds.notifyCommitTaps(event);
expect(after).toHaveBeenCalledWith(event);
expect(error).toHaveBeenCalledWith(
'onCommit subscriber failed:',
expect.any(Error),
);
error.mockRestore();
});
});