chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The /account/api/sites endpoints: session-gated, backed entirely by the
// injected siteBrowser, write path blocked in read-only mode.
import { PersonalDataServer } from '@pdsjs/core';
import {
CID,
cborEncodeDagCbor,
cidToString,
createBlobCid,
createCid,
} from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createSiteAccountRoutes } from '../src/account.js';
import { createSiteBrowser } from '../src/browser.js';
const DID = 'did:plc:apitest';
const PASSWORD = 'account-api-password';
const PDS_HOST = 'pds.example.com';
async function fixtures() {
const data = new TextEncoder().encode('<h1>hi</h1>');
const value = cborEncodeDagCbor({
$type: 'dev.pdsjs.site.deploy',
files: [
{
path: 'index.html',
blob: {
$type: 'blob',
ref: new CID(await createBlobCid(data)),
mimeType: 'text/html',
size: data.length,
},
},
],
createdAt: '2026-08-06T00:00:00.000Z',
});
return {
uri: `at://${DID}/dev.pdsjs.site.deploy/home`,
cid: cidToString(await createCid(value)),
value,
};
}
/**
* @param {{sites?: boolean, readOnly?: boolean, installer?: unknown, installs?: boolean}} [opts]
*/
async function makePds(opts = {}) {
const record = await fixtures();
const deleted = /** @type {string[]} */ ([]);
const records = new Map([[record.uri, record]]);
if (opts.installs !== false) {
const installValue = cborEncodeDagCbor({
$type: 'dev.pdsjs.app.install',
name: 'Photos',
manifest: {
uri: `at://${DID}/dev.pdsjs.app.manifest/photos`,
cid: 'bafym1',
},
site: 'home',
settings: [
{ key: 'topo', label: 'Terrain', type: 'boolean', default: true },
{ key: 'rows', type: 'select', options: ['compact', 'roomy'] },
],
config: { topo: true },
icon: {
$type: 'blob',
ref: new CID(await createBlobCid(new Uint8Array([1]))),
mimeType: 'image/svg+xml',
size: 1,
},
createdAt: '2026-08-11T00:00:00.000Z',
});
records.set(`at://${DID}/dev.pdsjs.app.install/3installtid`, {
uri: `at://${DID}/dev.pdsjs.app.install/3installtid`,
cid: cidToString(await createCid(installValue)),
value: installValue,
});
}
const actorStorage = /** @type {any} */ ({
getDid: async () => DID,
getHandle: async () => 'user.example.com',
getAccountStatus: async () => 'active',
getRecord: async (/** @type {string} */ uri) => records.get(uri) || null,
listRecords: async (/** @type {string} */ collection) => ({
records: [...records.values()]
.filter((row) => row.uri.includes(`/${collection}/`))
.map((row) => ({ ...row })),
cursor: null,
}),
putBlock: async () => {},
putRecord: async (
/** @type {string} */ uri,
/** @type {string} */ cid,
/** @type {string} */ _collection,
/** @type {string} */ _rkey,
/** @type {Uint8Array} */ value,
) => {
records.set(uri, { uri, cid, value });
},
deleteRecord: async (/** @type {string} */ uri) => {
deleted.push(uri);
records.delete(uri);
},
unlinkBlobsFromRecord: async () => {},
});
const siteBrowser =
opts.sites === false
? null
: createSiteBrowser({
actorStorage,
getDid: async () => DID,
siteDomain: 'example.com',
});
const pds = new PersonalDataServer({
actorStorage,
sharedStorage: /** @type {any} */ ({}),
blobs: /** @type {any} */ ({}),
jwtSecret: 'account-api-secret',
hostname: PDS_HOST,
password: PASSWORD,
readOnly: opts.readOnly,
extensions: [
(host) => ({
name: 'sites',
collections: [
'dev.pdsjs.site.deploy',
'dev.pdsjs.query.def',
'dev.pdsjs.app.install',
'dev.pdsjs.app.manifest',
],
routes: createSiteAccountRoutes({
host,
browser: siteBrowser,
installer: /** @type {any} */ (opts.installer ?? null),
}),
features: { sites: Boolean(siteBrowser) },
}),
],
});
// Commit and reap run their full course in integration; here the API
// contract is under test, not the repo machinery.
const commits = /** @type {Array<{action: string, uri: string}[]>} */ ([]);
pds.createCommit = /** @type {any} */ (
async (/** @type {string} */ _did, /** @type {any[]} */ writes) => {
commits.push(writes);
}
);
let reaped = 0;
pds.reapOrphanedBlobs = /** @type {any} */ (
async () => {
reaped++;
return [];
}
);
return { pds, deleted, commits, reapCount: () => reaped };
}
/** @param {PersonalDataServer} pds */
async function signInCookie(pds) {
const response = await pds.fetch(
new Request(`https://${PDS_HOST}/account/sign-in`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
username: DID,
password: PASSWORD,
}).toString(),
}),
);
const header = response.headers.get('set-cookie') || '';
const value = decodeURIComponent(
header.match(/pdsjs_account=([^;]*)/)?.[1] ?? '',
);
expect(value).not.toBe('');
return `pdsjs_account=${encodeURIComponent(value)}`;
}
/**
* @param {PersonalDataServer} pds
* @param {string} path
* @param {{cookie?: string, method?: string, body?: unknown}} [opts]
*/
async function api(pds, path, opts = {}) {
const response = await pds.fetch(
new Request(`https://${PDS_HOST}${path}`, {
method: opts.method || 'GET',
headers: {
...(opts.cookie ? { Cookie: opts.cookie } : {}),
...(opts.body !== undefined
? { 'Content-Type': 'application/json' }
: {}),
},
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
}),
);
return { status: response.status, body: await response.json() };
}
describe('/account/api/sites', () => {
it('requires a session', async () => {
const { pds } = await makePds();
const { status } = await api(pds, '/account/api/sites');
expect(status).toBe(401);
});
it('lists sites through the browser port', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/sites', { cookie });
expect(status).toBe(200);
expect(body).toMatchObject({
enabled: true,
domain: 'example.com',
apexName: 'home',
apexShadowed: false,
});
expect(body.sites).toHaveLength(1);
expect(body.sites[0]).toMatchObject({
name: 'home',
url: 'https://example.com',
fileCount: 1,
servable: true,
});
});
it('reports disabled without a browser port', async () => {
const { pds } = await makePds({ sites: false });
const cookie = await signInCookie(pds);
const { body } = await api(pds, '/account/api/sites', { cookie });
expect(body).toEqual({ enabled: false, sites: [] });
});
});
describe('/account/api/sites/files', () => {
it('lists the served files of a named site', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(
pds,
'/account/api/sites/files?name=home',
{ cookie },
);
expect(status).toBe(200);
expect(body.files).toHaveLength(1);
expect(body.files[0]).toMatchObject({
path: 'index.html',
contentType: 'text/html',
});
expect(body.files[0].cid).toMatch(/^b/);
});
it('rejects unknown sites and missing names', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const missing = await api(pds, '/account/api/sites/files', { cookie });
expect(missing.status).toBe(400);
const unknown = await api(pds, '/account/api/sites/files?name=nope', {
cookie,
});
expect(unknown.status).toBe(400);
expect(unknown.body.error).toMatch(/No such site/);
});
});
describe('/account/api/installs', () => {
it('lists installs with the snapshot the page renders', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const { status, body } = await api(pds, '/account/api/installs', {
cookie,
});
expect(status).toBe(200);
expect(body.installs).toHaveLength(1);
expect(body.installs[0]).toMatchObject({ name: 'Photos', site: 'home' });
expect(body.installs[0].iconUrl).toMatch(/getBlob/);
expect(body.installs[0].settings).toHaveLength(2);
});
it('saves config against the snapshot schema and rejects what it refuses', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const uri = `at://${DID}/dev.pdsjs.app.install/3installtid`;
const ok = await api(pds, '/account/api/installs/config', {
cookie,
method: 'POST',
body: { uri, config: { topo: false, rows: 'compact' } },
});
expect(ok.status).toBe(200);
const listed = await api(pds, '/account/api/installs', { cookie });
expect(listed.body.installs[0].config).toEqual({
topo: false,
rows: 'compact',
});
for (const [body, error] of /** @type {Array<[Object, RegExp]>} */ ([
[{ uri, config: { nope: 1 } }, /No such setting/],
[{ uri, config: { topo: 'yes' } }, /true or false/],
[{ uri, config: { rows: 'wide' } }, /not one of/],
[
{ uri: `at://${DID}/dev.pdsjs.app.install/none`, config: {} },
/No such install/,
],
[
{ uri: 'at://did:plc:other/dev.pdsjs.app.install/x', config: {} },
/Name the install/,
],
[{ uri, config: [] }, /as an object/],
])) {
const bad = await api(pds, '/account/api/installs/config', {
cookie,
method: 'POST',
body,
});
expect(bad.status).toBe(400);
expect(bad.body.error).toMatch(error);
}
});
it('delegates preview, install, and rename to the installer port', async () => {
const calls = /** @type {string[]} */ ([]);
const installer = {
preview: async (/** @type {string} */ uri) => {
calls.push(`preview:${uri}`);
return { name: 'Photos' };
},
install: async (
/** @type {string} */ uri,
/** @type {any} */ options,
) => {
calls.push(`install:${uri}:${options.site}`);
return { site: options.site || 'photos', name: 'Photos' };
},
rename: async (/** @type {string} */ uri, /** @type {string} */ name) => {
calls.push(`rename:${name}`);
return { site: name };
},
checkUpdate: async () => ({ currentCid: 'bafym2', changed: true }),
};
const { pds } = await makePds({ installer });
const cookie = await signInCookie(pds);
const preview = await api(pds, '/account/api/installs/preview', {
cookie,
method: 'POST',
body: { uri: 'at://dev/dev.pdsjs.app.manifest/photos' },
});
expect(preview.status).toBe(200);
expect(preview.body.name).toBe('Photos');
const install = await api(pds, '/account/api/installs/install', {
cookie,
method: 'POST',
body: {
uri: 'at://dev/dev.pdsjs.app.manifest/photos',
site: 'pics',
config: { topo: false },
},
});
expect(install.status).toBe(200);
expect(install.body.site).toBe('pics');
const rename = await api(pds, '/account/api/installs/rename', {
cookie,
method: 'POST',
body: {
uri: `at://${DID}/dev.pdsjs.app.install/3installtid`,
site: 'pics',
},
});
expect(rename.status).toBe(200);
const updates = await api(pds, '/account/api/installs/updates', { cookie });
expect(updates.status).toBe(200);
expect(
updates.body.updates[`at://${DID}/dev.pdsjs.app.install/3installtid`],
).toEqual({ currentCid: 'bafym2', changed: true });
expect(calls).toEqual([
'preview:at://dev/dev.pdsjs.app.manifest/photos',
'install:at://dev/dev.pdsjs.app.manifest/photos:pics',
'rename:pics',
]);
});
it('reports the feature off without an installer, and blocks writes read-only', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
for (const path of [
'/account/api/installs/preview',
'/account/api/installs/install',
'/account/api/installs/rename',
]) {
const res = await api(pds, path, {
cookie,
method: 'POST',
body: { uri: 'at://x/dev.pdsjs.app.manifest/y', site: 'z' },
});
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/not enabled/);
}
const updates = await api(pds, '/account/api/installs/updates', { cookie });
expect(updates.body.updates).toEqual({});
const readOnly = await makePds({
readOnly: true,
installer: { install: async () => ({ site: 'x', name: 'x' }) },
});
const roCookie = await signInCookie(readOnly.pds);
const blocked = await api(readOnly.pds, '/account/api/installs/install', {
cookie: roCookie,
method: 'POST',
body: { uri: 'at://x/dev.pdsjs.app.manifest/y' },
});
expect(blocked.status).toBe(403);
});
it('rejects an update check with missing fields quietly and bad rename input loudly', async () => {
const installer = {
rename: async () => ({ site: 'x' }),
checkUpdate: async () => {
throw new Error('unreachable developer');
},
};
const { pds } = await makePds({ installer });
const cookie = await signInCookie(pds);
// A developer that cannot be reached is omitted, not an error
const updates = await api(pds, '/account/api/installs/updates', { cookie });
expect(updates.status).toBe(200);
expect(updates.body.updates).toEqual({});
const noSite = await api(pds, '/account/api/installs/rename', {
cookie,
method: 'POST',
body: { uri: `at://${DID}/dev.pdsjs.app.install/3installtid` },
});
expect(noSite.status).toBe(400);
expect(noSite.body.error).toMatch(/new subdomain/);
const wrongRepo = await api(pds, '/account/api/installs/rename', {
cookie,
method: 'POST',
body: { uri: 'at://did:plc:other/dev.pdsjs.app.install/x', site: 'y' },
});
expect(wrongRepo.status).toBe(400);
const noUri = await api(pds, '/account/api/installs/preview', {
cookie,
method: 'POST',
body: {},
});
expect(noUri.status).toBe(400);
});
});
describe('/account/api/sites/delete', () => {
it('deletes the record, commits, and reaps orphans', async () => {
const { pds, deleted, commits, reapCount } = await makePds();
const cookie = await signInCookie(pds);
const { status } = await api(pds, '/account/api/sites/delete', {
cookie,
method: 'POST',
body: { name: 'home' },
});
expect(status).toBe(200);
expect(deleted).toEqual([`at://${DID}/dev.pdsjs.site.deploy/home`]);
expect(commits).toEqual([
[{ action: 'delete', uri: `at://${DID}/dev.pdsjs.site.deploy/home` }],
]);
expect(reapCount()).toBe(1);
});
it('rejects unknown sites and missing names', async () => {
const { pds } = await makePds();
const cookie = await signInCookie(pds);
const missing = await api(pds, '/account/api/sites/delete', {
cookie,
method: 'POST',
body: { name: 'nope' },
});
expect(missing.status).toBe(400);
expect(missing.body.error).toMatch(/No such site/);
const unnamed = await api(pds, '/account/api/sites/delete', {
cookie,
method: 'POST',
body: {},
});
expect(unnamed.status).toBe(400);
});
it('is blocked in read-only mode while listing still works', async () => {
const { pds, deleted } = await makePds({ readOnly: true });
const cookie = await signInCookie(pds);
const list = await api(pds, '/account/api/sites', { cookie });
expect(list.status).toBe(200);
const del = await api(pds, '/account/api/sites/delete', {
cookie,
method: 'POST',
body: { name: 'home' },
});
expect(del.status).toBe(403);
expect(deleted).toHaveLength(0);
});
});