chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The /account/api/spaces endpoints, driven through pds.fetch with a cookie
// session against fake storage, with the space routes composed over a fake
// browser and admin port.
import { describe, expect, it } from 'vitest';
import {
apiGet,
apiPost,
createPds as createBasePds,
DID,
sessionCookie,
} from '../../../test/helpers/account-api-harness.js';
import { createSpaceAccountRoutes } from '../src/account.js';
/**
* @param {Object} [options]
* @param {Object} [options.spaceBrowser]
* @param {Object} [options.spaceAdmin]
*/
function createPds({ spaceBrowser, spaceAdmin } = {}) {
return createBasePds({
extensions: [
(host) => ({
name: 'spaces',
routes: createSpaceAccountRoutes({
host,
spaceBrowser: /** @type {any} */ (spaceBrowser ?? null),
spaceAdmin: /** @type {any} */ (spaceAdmin ?? null),
}),
features: { spaceAdmin: Boolean(spaceAdmin) },
}),
],
});
}
describe('/account/api/spaces', () => {
it('reports the feature off without a browser', async () => {
const pds = createPds();
const body = await (
await apiGet(pds, '/account/api/spaces', await sessionCookie())
).json();
expect(body).toEqual({ enabled: false, spaces: [] });
});
it('lists spaces with their member and record counts', async () => {
const pds = createPds({
spaceBrowser: {
async listSpaces() {
return [
{
uri: 'at://space.example/1',
spaceType: 'chat.example.space',
spaceDid: 'did:web:space.example',
isOwner: true,
policy: {
$type: 'com.atproto.memberspace.policy',
publicWrite: true,
},
createdAt: '2026-01-01T00:00:00.000Z',
deletedAt: null,
},
];
},
async countMembers() {
return 3;
},
async countRecords() {
return 12;
},
},
});
const body = await (
await apiGet(pds, '/account/api/spaces', await sessionCookie())
).json();
expect(body.enabled).toBe(true);
expect(body.spaces).toEqual([
{
uri: 'at://space.example/1',
spaceType: 'chat.example.space',
authority: 'did:web:space.example',
isOwner: true,
policy: {
$type: 'com.atproto.memberspace.policy',
publicWrite: true,
},
createdAt: '2026-01-01T00:00:00.000Z',
deleted: false,
members: 3,
collections: 12,
},
]);
});
});
describe('space admin endpoints', () => {
const SPACE = `at://${DID}/space/com.example.team/main`;
/** A spaceAdmin recording its calls. */
function recordingAdmin() {
/** @type {string[]} */
const calls = [];
return {
calls,
async createSpace(/** @type {{type: string, skey?: string}} */ opts) {
calls.push(`create ${opts.type} ${opts.skey ?? ''}`);
return { uri: SPACE };
},
async putMember(
/** @type {string} */ space,
/** @type {string} */ did,
/** @type {{read: boolean, write: boolean}} */ access,
) {
calls.push(`put ${space} ${did} ${access.read} ${access.write}`);
},
async removeMember(
/** @type {string} */ space,
/** @type {string} */ did,
) {
calls.push(`remove ${space} ${did}`);
},
};
}
it('answers with an error when spaces are not enabled', async () => {
const pds = createPds();
const cookie = await sessionCookie();
for (const [path, body] of [
['/account/api/spaces/create', { type: 'com.example.team' }],
['/account/api/spaces/members/add', { space: SPACE, actor: DID }],
['/account/api/spaces/members/remove', { space: SPACE, did: DID }],
]) {
const response = await apiPost(pds, String(path), body, cookie);
expect(response.status).toBe(400);
expect((await response.json()).error).toMatch(/not enabled/);
}
});
it('creates a space and requires a type', async () => {
const spaceAdmin = recordingAdmin();
const pds = createPds({ spaceAdmin });
const cookie = await sessionCookie();
const created = await apiPost(
pds,
'/account/api/spaces/create',
{ type: 'com.example.team', skey: 'main' },
cookie,
);
expect(created.status).toBe(200);
expect((await created.json()).uri).toBe(SPACE);
expect(spaceAdmin.calls).toContain('create com.example.team main');
const missing = await apiPost(
pds,
'/account/api/spaces/create',
{},
cookie,
);
expect(missing.status).toBe(400);
expect((await missing.json()).error).toMatch(/type is required/);
});
it('lists members through the browser', async () => {
const pds = createPds({
spaceBrowser: {
async listMembers() {
return [DID, 'did:plc:friend'];
},
},
});
const response = await apiGet(
pds,
`/account/api/spaces/members?space=${encodeURIComponent(SPACE)}`,
await sessionCookie(),
);
expect(await response.json()).toEqual({
enabled: true,
members: [DID, 'did:plc:friend'],
});
});
it('adds a member by DID without resolving anything', async () => {
const spaceAdmin = recordingAdmin();
const pds = createPds({ spaceAdmin });
const response = await apiPost(
pds,
'/account/api/spaces/members/add',
{ space: SPACE, actor: 'did:plc:friend' },
await sessionCookie(),
);
expect(response.status).toBe(200);
expect((await response.json()).did).toBe('did:plc:friend');
expect(spaceAdmin.calls).toContain(`put ${SPACE} did:plc:friend true true`);
});
it('resolves a handle through the well-known document', async () => {
const spaceAdmin = recordingAdmin();
const pds = createPds({ spaceAdmin });
const realFetch = globalThis.fetch;
globalThis.fetch = /** @type {any} */ (
async (/** @type {any} */ input) => {
const url = String(input);
if (url === 'https://friend.example/.well-known/atproto-did') {
return new Response('did:plc:friend\n');
}
return realFetch(input);
}
);
try {
const response = await apiPost(
pds,
'/account/api/spaces/members/add',
{ space: SPACE, actor: 'friend.example' },
await sessionCookie(),
);
expect(response.status).toBe(200);
expect((await response.json()).did).toBe('did:plc:friend');
} finally {
globalThis.fetch = realFetch;
}
});
it('falls back to DNS and fails closed when both miss', async () => {
const spaceAdmin = recordingAdmin();
const pds = createPds({ spaceAdmin });
const realFetch = globalThis.fetch;
globalThis.fetch = /** @type {any} */ (
async (/** @type {any} */ input) => {
const url = String(input);
if (url.includes('.well-known')) {
return new Response('nope', { status: 404 });
}
if (url.startsWith('https://cloudflare-dns.com/')) {
if (url.includes('_atproto.friend.example')) {
return Response.json({
Answer: [{ data: '"did=did:plc:dnsfriend"' }],
});
}
return Response.json({ Answer: [] });
}
return realFetch(input);
}
);
try {
const resolved = await apiPost(
pds,
'/account/api/spaces/members/add',
{ space: SPACE, actor: 'friend.example' },
await sessionCookie(),
);
expect((await resolved.json()).did).toBe('did:plc:dnsfriend');
const unresolved = await apiPost(
pds,
'/account/api/spaces/members/add',
{ space: SPACE, actor: 'nobody.example' },
await sessionCookie(),
);
expect(unresolved.status).toBe(400);
expect((await unresolved.json()).error).toMatch(/could not resolve/);
} finally {
globalThis.fetch = realFetch;
}
});
it('refuses an actor that is not a plausible handle', async () => {
const pds = createPds({ spaceAdmin: recordingAdmin() });
const response = await apiPost(
pds,
'/account/api/spaces/members/add',
{ space: SPACE, actor: 'not a handle' },
await sessionCookie(),
);
expect(response.status).toBe(400);
expect((await response.json()).error).toMatch(/not a handle/);
});
it('removes a member and requires both fields', async () => {
const spaceAdmin = recordingAdmin();
const pds = createPds({ spaceAdmin });
const cookie = await sessionCookie();
const removed = await apiPost(
pds,
'/account/api/spaces/members/remove',
{ space: SPACE, did: 'did:plc:friend' },
cookie,
);
expect(removed.status).toBe(200);
expect(spaceAdmin.calls).toContain(`remove ${SPACE} did:plc:friend`);
const missing = await apiPost(
pds,
'/account/api/spaces/members/remove',
{ space: SPACE },
cookie,
);
expect(missing.status).toBe(400);
});
});