chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The /account/api JSON endpoints, driven through pds.fetch with a cookie
// session: the wrapper's authentication and same-origin rules, then each
// section endpoint against fake storage.
import { describe, expect, it } from 'vitest';
import {
buildCarFile,
cborEncode,
cborEncodeDagCbor,
cidToString,
createCid,
} from '../packages/core/src/repo.js';
import {
apiGet,
apiPost,
BASE,
createActorStorage,
createEmailer,
createPds,
createSharedStorage,
DID,
HANDLE,
sessionCookie,
} from './helpers/account-api-harness.js';
import { createFakeSpaceStorage } from './helpers/fake-space-storage.js';
describe('accountApi wrapper', () => {
it('rejects a request with no session cookie', async () => {
const pds = createPds();
const response = await pds.fetch(new Request(`${BASE}/account/api/email`));
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ error: 'Not signed in' });
});
it('rejects a cross-origin POST before touching the session', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/status',
{ action: 'activate' },
await sessionCookie(),
{ origin: 'https://evil.example' },
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ error: 'Forbidden' });
});
it('rejects a write on a read-only server', async () => {
const pds = createPds({ readOnly: true });
const response = await apiPost(
pds,
'/account/api/status',
{ action: 'activate' },
await sessionCookie(),
);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ error: 'This PDS is read-only' });
});
});
describe('/account/api/lexicon-status', () => {
it('answers an empty map without a resolver', async () => {
const pds = createPds();
const response = await apiGet(
pds,
'/account/api/lexicon-status?collections=app.bsky.feed.post',
await sessionCookie(),
);
expect(await response.json()).toEqual({ published: {} });
});
it('reports each collection, folding a resolver error to false', async () => {
const pds = createPds({
lexiconResolver: {
async isPublished(/** @type {string} */ nsid) {
if (nsid === 'bad.example.record') throw new Error('resolve failed');
return nsid === 'app.bsky.feed.post';
},
},
});
const response = await apiGet(
pds,
'/account/api/lexicon-status?collections=app.bsky.feed.post, com.example.other,bad.example.record,',
await sessionCookie(),
);
expect(await response.json()).toEqual({
published: {
'app.bsky.feed.post': true,
'com.example.other': false,
'bad.example.record': false,
},
});
});
});
describe('/account/api/lexicon-schema', () => {
it('answers null without a collection or resolver', async () => {
const pds = createPds();
const response = await apiGet(
pds,
'/account/api/lexicon-schema?collection=app.bsky.feed.post',
await sessionCookie(),
);
expect(await response.json()).toEqual({ lexicon: null });
});
it('returns the resolved document, and null when resolution throws', async () => {
const doc = { lexicon: 1, id: 'app.bsky.feed.post' };
const pds = createPds({
lexiconResolver: {
async getLexicon(/** @type {string} */ nsid) {
if (nsid === 'bad.example.record') throw new Error('resolve failed');
return nsid === 'app.bsky.feed.post' ? doc : null;
},
},
});
const cookie = await sessionCookie();
expect(
await (
await apiGet(
pds,
'/account/api/lexicon-schema?collection=app.bsky.feed.post',
cookie,
)
).json(),
).toEqual({ lexicon: doc });
expect(
await (
await apiGet(
pds,
'/account/api/lexicon-schema?collection=bad.example.record',
cookie,
)
).json(),
).toEqual({ lexicon: null });
});
});
describe('/account/api/activity', () => {
it('answers empty when the storage keeps no events', async () => {
const storage = createActorStorage();
// @ts-expect-error the handler feature-tests this method
storage.getEventsBefore = undefined;
const pds = createPds({ actorStorage: storage });
const response = await apiGet(
pds,
'/account/api/activity',
await sessionCookie(),
);
expect(await response.json()).toEqual({ events: [], cursor: null });
});
it('flattens each commit into rows and skips an unreadable event', async () => {
const storage = createActorStorage();
storage.events.push(
{
seq: 1,
evt: cborEncode({
ops: [{ path: 'app.bsky.feed.post/3aaa', action: 'create' }],
time: '2026-01-01T00:00:00.000Z',
}),
},
{ seq: 2, evt: new Uint8Array([0xff, 0xff]) },
{
seq: 3,
evt: cborEncode({
ops: [
{ path: 'app.bsky.feed.post/3bbb', action: 'update' },
{ path: 'app.bsky.feed.like/3ccc', action: 'delete' },
],
time: '2026-01-02T00:00:00.000Z',
}),
},
);
const pds = createPds({ actorStorage: storage });
const response = await apiGet(
pds,
'/account/api/activity?limit=2',
await sessionCookie(),
);
const body = await response.json();
expect(body.events).toEqual([
{
seq: 3,
action: 'update',
collection: 'app.bsky.feed.post',
rkey: '3bbb',
time: '2026-01-02T00:00:00.000Z',
},
{
seq: 3,
action: 'delete',
collection: 'app.bsky.feed.like',
rkey: '3ccc',
time: '2026-01-02T00:00:00.000Z',
},
]);
// A full page hands back a cursor; following it reaches the older event.
expect(body.cursor).toBe('2');
const older = await apiGet(
pds,
'/account/api/activity?cursor=2',
await sessionCookie(),
);
const olderBody = await older.json();
expect(olderBody.events).toHaveLength(1);
expect(olderBody.events[0].seq).toBe(1);
expect(olderBody.cursor).toBeNull();
});
});
describe('/account/api/apps', () => {
const token = {
did: DID,
clientId: 'https://client.example/metadata.json',
scope: 'atproto',
dpopJkt: 'abcdef123456',
createdAt: Date.parse('2026-01-01T00:00:00Z'),
userAgent: 'curl/8.7.1',
tokenId: 'refresh-1',
};
it('revokes a session by the handle the list rendered', async () => {
const shared = createSharedStorage({ oauthTokens: [token] });
const pds = createPds({ sharedStorage: shared });
const cookie = await sessionCookie();
const list = await (await apiGet(pds, '/account/api/apps', cookie)).json();
expect(list.apps).toHaveLength(1);
const response = await apiPost(
pds,
'/account/api/apps/revoke',
{ session: list.apps[0].sessionId },
cookie,
);
expect(await response.json()).toEqual({
revoked: 'https://client.example/metadata.json',
});
expect(shared.tokens).toHaveLength(0);
});
it('rejects revoking a session that does not exist', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/apps/revoke',
{ session: 'nonsense' },
await sessionCookie(),
);
expect(response.status).toBe(400);
expect((await response.json()).error).toBe(
'That session no longer exists.',
);
});
});
describe('/account/api/apps/alias', () => {
it('combines, dismisses, and separates rows', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const combined = await (
await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'combine', from: 'feeds.example.com', to: 'bsky.app' },
cookie,
)
).json();
expect(combined.aliases).toEqual({ 'feeds.example.com': 'bsky.app' });
const dismissed = await (
await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'dismiss', from: 'other.example.com', to: 'bsky.app' },
cookie,
)
).json();
expect(dismissed.dismissedPairs).toEqual(['other.example.com>bsky.app']);
const separated = await (
await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'separate', from: 'feeds.example.com' },
cookie,
)
).json();
expect(separated.aliases).toEqual({});
});
it('rejects a bad hostname, a self-pair, and an unknown action', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const badFrom = await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'combine', from: 'not a host', to: 'bsky.app' },
cookie,
);
expect((await badFrom.json()).error).toBe('from must be a hostname');
const selfPair = await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'combine', from: 'bsky.app', to: 'bsky.app' },
cookie,
);
expect((await selfPair.json()).error).toBe(
'to must be a different hostname',
);
const unknown = await apiPost(
pds,
'/account/api/apps/alias',
{ action: 'merge', from: 'bsky.app' },
cookie,
);
expect((await unknown.json()).error).toBe(
'action must be combine, separate, or dismiss',
);
});
});
describe('/account/api/sessions', () => {
const live = {
jti: 'jti-live',
did: DID,
label: 'Password',
scope: 'com.atproto.access',
userAgent: 'curl/8.7.1',
createdAt: new Date().toISOString(),
refreshedAt: null,
};
const expired = {
jti: 'jti-expired',
did: DID,
label: 'Password',
scope: 'com.atproto.access',
userAgent: null,
createdAt: '2020-01-01T00:00:00.000Z',
refreshedAt: null,
};
it('lists live sessions and reaps expired ones', async () => {
const shared = createSharedStorage({ sessions: [live, expired] });
const pds = createPds({ sharedStorage: shared });
const body = await (
await apiGet(pds, '/account/api/sessions', await sessionCookie())
).json();
expect(body.supported).toBe(true);
expect(body.sessions.map((/** @type {any} */ s) => s.jti)).toEqual([
'jti-live',
]);
expect(shared.sessionRows.map((s) => s.jti)).toEqual(['jti-live']);
});
it('revokes one session and refuses an unknown id', async () => {
const shared = createSharedStorage({ sessions: [{ ...live }] });
const pds = createPds({ sharedStorage: shared });
const cookie = await sessionCookie();
const missing = await apiPost(
pds,
'/account/api/sessions/revoke',
{ jti: 'nonsense' },
cookie,
);
expect(missing.status).toBe(400);
const revoked = await apiPost(
pds,
'/account/api/sessions/revoke',
{ jti: 'jti-live' },
cookie,
);
expect(await revoked.json()).toEqual({ revoked: 'jti-live' });
expect(shared.sessionRows).toHaveLength(0);
});
it('revokes every session at once', async () => {
const shared = createSharedStorage({
sessions: [
{ ...live },
{ ...live, jti: 'jti-two', label: 'App password: cli' },
],
});
const pds = createPds({ sharedStorage: shared });
const response = await apiPost(
pds,
'/account/api/sessions/revoke-all',
{},
await sessionCookie(),
);
expect(await response.json()).toEqual({ revoked: 2 });
expect(shared.sessionRows).toHaveLength(0);
});
});
describe('/account/api/app-passwords', () => {
it('creates one, lists it, and hands the secret back once', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const created = await (
await apiPost(
pds,
'/account/api/app-passwords/create',
{ name: 'cli', privileged: false },
cookie,
)
).json();
expect(created.name).toBe('cli');
expect(created.password).toMatch(/^[a-z0-9]{4}(-[a-z0-9]{4}){3}$/);
const list = await (
await apiGet(pds, '/account/api/app-passwords', cookie)
).json();
expect(list.passwords).toHaveLength(1);
expect(list.passwords[0].name).toBe('cli');
expect(list.passwords[0].password).toBeUndefined();
});
it('rejects a blank name and a duplicate', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const blank = await apiPost(
pds,
'/account/api/app-passwords/create',
{ name: ' ' },
cookie,
);
expect((await blank.json()).error).toBe('A name is required.');
await apiPost(
pds,
'/account/api/app-passwords/create',
{ name: 'cli' },
cookie,
);
const duplicate = await apiPost(
pds,
'/account/api/app-passwords/create',
{ name: 'cli' },
cookie,
);
expect((await duplicate.json()).error).toBe(
'App password "cli" already exists.',
);
});
it('revokes the credential and the sessions it opened', async () => {
const shared = createSharedStorage({
sessions: [
{
jti: 'jti-cli',
did: DID,
label: 'App password: cli',
scope: 'com.atproto.appPass',
createdAt: new Date().toISOString(),
},
],
});
const pds = createPds({ sharedStorage: shared });
const cookie = await sessionCookie();
await apiPost(
pds,
'/account/api/app-passwords/create',
{ name: 'cli' },
cookie,
);
const missing = await apiPost(
pds,
'/account/api/app-passwords/revoke',
{ name: 'nope' },
cookie,
);
expect((await missing.json()).error).toBe('No app password named "nope".');
const revoked = await apiPost(
pds,
'/account/api/app-passwords/revoke',
{ name: 'cli' },
cookie,
);
expect(await revoked.json()).toEqual({ revoked: 'cli' });
expect(shared.sessionRows).toHaveLength(0);
});
});
describe('/account/api/passkeys', () => {
it('lists passkeys with a registration challenge and its cookie', async () => {
const shared = createSharedStorage({
passkeys: [
{
credentialId: 'cred-1',
name: 'Laptop',
createdAt: '2026-01-01T00:00:00.000Z',
lastUsedAt: null,
},
],
});
const pds = createPds({ sharedStorage: shared });
const response = await apiGet(
pds,
'/account/api/passkeys',
await sessionCookie(),
);
const body = await response.json();
expect(body.supported).toBe(true);
expect(body.rpId).toBe('pds.example.com');
expect(body.accountName).toBe(HANDLE);
expect(body.challenge).not.toBe('');
expect(response.headers.get('set-cookie')).toContain('pdsjs_webauthn');
expect(body.passkeys).toEqual([
{
credentialId: 'cred-1',
name: 'Laptop',
createdAt: '2026-01-01T00:00:00.000Z',
lastUsedAt: null,
},
]);
});
it('revokes a passkey and refuses an unknown credential', async () => {
const shared = createSharedStorage({
passkeys: [{ credentialId: 'cred-1', name: 'Laptop' }],
});
const pds = createPds({ sharedStorage: shared });
const cookie = await sessionCookie();
const missing = await apiPost(
pds,
'/account/api/passkeys/revoke',
{ credentialId: 'nope' },
cookie,
);
expect((await missing.json()).error).toBe(
'No such passkey on this account.',
);
const revoked = await apiPost(
pds,
'/account/api/passkeys/revoke',
{ credentialId: 'cred-1' },
cookie,
);
expect(await revoked.json()).toEqual({ revoked: 'cred-1' });
});
});
describe('/account/api/email', () => {
it('sets an address without a sender, recorded unconfirmed', async () => {
const pds = createPds();
const cookie = await sessionCookie();
const set = await (
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
)
).json();
expect(set).toEqual({ ok: true, unconfirmed: true });
const state = await (
await apiGet(pds, '/account/api/email', cookie)
).json();
expect(state.address).toBe('chad@example.com');
expect(state.verified).toBe(false);
expect(state.canSend).toBe(false);
});
it('rejects an address that does not parse', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/email/action',
{ address: 'not-an-address' },
await sessionCookie(),
);
expect((await response.json()).error).toBe(
'That does not look like an email address.',
);
});
it('mails a code and verifies it', async () => {
const emailer = createEmailer();
const pds = createPds({ emailer });
const cookie = await sessionCookie();
const set = await (
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
)
).json();
expect(set.ok).toBe(true);
expect(set.sent).toBe('chad@example.com');
expect(emailer.sent).toHaveLength(1);
const pending = await (
await apiGet(pds, '/account/api/email', cookie)
).json();
expect(pending.pending.address).toBe('chad@example.com');
const wrong = await apiPost(
pds,
'/account/api/email/action',
{ action: 'verify', code: '000000' },
cookie,
);
expect((await wrong.json()).error).toBe('That code is wrong.');
const code = /** @type {RegExpMatchArray} */ (
emailer.sent[0].text.match(/\d{6}/)
)[0];
const verified = await (
await apiPost(
pds,
'/account/api/email/action',
{ action: 'verify', code },
cookie,
)
).json();
expect(verified).toEqual({ ok: true, address: 'chad@example.com' });
const state = await (
await apiGet(pds, '/account/api/email', cookie)
).json();
expect(state.verified).toBe(true);
});
it('refuses to verify or resend with nothing pending', async () => {
const emailer = createEmailer();
const pds = createPds({ emailer });
const cookie = await sessionCookie();
for (const action of ['verify', 'resend']) {
const response = await apiPost(
pds,
'/account/api/email/action',
{ action },
cookie,
);
expect((await response.json()).error).toBe('No address is waiting.');
}
});
it('rejects an expired code and clears the pending address', async () => {
const emailer = createEmailer();
const storage = createActorStorage();
const pds = createPds({ emailer, actorStorage: storage });
const cookie = await sessionCookie();
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
);
const state = /** @type {any} */ (await storage.getEmail());
await storage.setEmail({
...state,
pending: { ...state.pending, expiresAt: Date.now() - 1 },
});
const response = await apiPost(
pds,
'/account/api/email/action',
{ action: 'verify', code: '123456' },
cookie,
);
expect((await response.json()).error).toBe(
'That code expired. Start again.',
);
expect(/** @type {any} */ (await storage.getEmail()).pending).toBeNull();
});
it('holds a resend inside the cooldown and allows it after', async () => {
const emailer = createEmailer();
const storage = createActorStorage();
const pds = createPds({ emailer, actorStorage: storage });
const cookie = await sessionCookie();
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
);
const early = await apiPost(
pds,
'/account/api/email/action',
{ action: 'resend' },
cookie,
);
expect((await early.json()).error).toBe(
'Wait a moment before asking for another code.',
);
const state = /** @type {any} */ (await storage.getEmail());
await storage.setEmail({
...state,
pending: { ...state.pending, sentAt: Date.now() - 60_000 },
});
const resent = await (
await apiPost(
pds,
'/account/api/email/action',
{ action: 'resend' },
cookie,
)
).json();
expect(resent.ok).toBe(true);
expect(emailer.sent).toHaveLength(2);
});
it('keeps the earlier code usable when the resend fails to send', async () => {
const emailer = createEmailer();
const storage = createActorStorage();
const pds = createPds({ emailer, actorStorage: storage });
const cookie = await sessionCookie();
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
);
const before = /** @type {any} */ (await storage.getEmail());
await storage.setEmail({
...before,
pending: { ...before.pending, sentAt: Date.now() - 60_000 },
});
const held = /** @type {any} */ (await storage.getEmail());
emailer.setFailing(true);
const response = await apiPost(
pds,
'/account/api/email/action',
{ action: 'resend' },
cookie,
);
expect((await response.json()).error).toBe(
'The message could not be sent. Try again later.',
);
expect(/** @type {any} */ (await storage.getEmail()).pending.codeHash).toBe(
held.pending.codeHash,
);
});
it('records nothing pending when the first send fails', async () => {
const emailer = createEmailer();
emailer.setFailing(true);
const storage = createActorStorage();
const pds = createPds({ emailer, actorStorage: storage });
const response = await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
await sessionCookie(),
);
expect((await response.json()).error).toBe(
'The message could not be sent. Try again later.',
);
expect(/** @type {any} */ (await storage.getEmail()).pending).toBeNull();
});
it('cancels a pending address and removes a set one', async () => {
const emailer = createEmailer();
const pds = createPds({ emailer });
const cookie = await sessionCookie();
await apiPost(
pds,
'/account/api/email/action',
{ address: 'chad@example.com' },
cookie,
);
const cancelled = await (
await apiPost(
pds,
'/account/api/email/action',
{ action: 'cancel' },
cookie,
)
).json();
expect(cancelled).toEqual({ ok: true });
const removed = await (
await apiPost(
pds,
'/account/api/email/action',
{ action: 'remove' },
cookie,
)
).json();
expect(removed).toEqual({ ok: true });
const state = await (
await apiGet(pds, '/account/api/email', cookie)
).json();
expect(state.address).toBeNull();
});
});
describe('/account/api/identity', () => {
it('describes the identity, with no signing key before init', async () => {
const pds = createPds();
const body = await (
await apiGet(pds, '/account/api/identity', await sessionCookie())
).json();
expect(body).toEqual({
did: DID,
handle: HANDLE,
status: 'active',
signingKey: null,
pdsEndpoint: 'https://pds.example.com',
plcUrl: 'https://plc.directory',
readOnly: false,
});
});
it('surfaces a rename rejection as a caller error', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/identity/handle',
{ handle: 'not a handle' },
await sessionCookie(),
);
expect(response.status).toBe(400);
expect((await response.json()).error).toBeTruthy();
});
});
describe('/account/api/status', () => {
it('deactivates and reactivates the account', async () => {
const storage = createActorStorage();
const pds = createPds({ actorStorage: storage });
const cookie = await sessionCookie();
const down = await (
await apiPost(
pds,
'/account/api/status',
{ action: 'deactivate' },
cookie,
)
).json();
expect(down).toEqual({ status: 'deactivated' });
expect(await storage.getAccountStatus()).toBe('deactivated');
const up = await (
await apiPost(pds, '/account/api/status', { action: 'activate' }, cookie)
).json();
expect(up).toEqual({ status: 'active' });
});
it('rejects an unknown action', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/status',
{ action: 'explode' },
await sessionCookie(),
);
expect((await response.json()).error).toBe('Unknown status action.');
});
});
describe('/account/api/records', () => {
const COLLECTION = 'app.bsky.feed.post';
it('requires a collection', async () => {
const pds = createPds();
const response = await apiGet(
pds,
'/account/api/records',
await sessionCookie(),
);
expect((await response.json()).error).toBe('Name a collection to read.');
});
it('lists a collection, decoding each record for display', async () => {
const value = cborEncodeDagCbor({ $type: COLLECTION, text: 'hello' });
const storage = createActorStorage({
async listRecords() {
return {
records: [
{ uri: `at://${DID}/${COLLECTION}/3aaa`, cid: 'bafyaaa', value },
],
cursor: null,
};
},
});
const pds = createPds({ actorStorage: storage });
const body = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}`,
await sessionCookie(),
)
).json();
expect(body.records).toEqual([
{
uri: `at://${DID}/${COLLECTION}/3aaa`,
cid: 'bafyaaa',
value: { $type: COLLECTION, text: 'hello' },
},
]);
expect(body.blobBase).toContain(encodeURIComponent(DID));
});
it('fetches one record by rkey without listing', async () => {
const value = cborEncodeDagCbor({ $type: COLLECTION, text: 'direct' });
const storage = createActorStorage({
async getRecord(/** @type {string} */ uri) {
return uri === `at://${DID}/${COLLECTION}/3aaa`
? { cid: 'bafyaaa', value }
: null;
},
});
const pds = createPds({ actorStorage: storage });
const cookie = await sessionCookie();
const found = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&rkey=3aaa`,
cookie,
)
).json();
expect(found.records).toHaveLength(1);
expect(found.records[0].value.text).toBe('direct');
const missing = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&rkey=3zzz`,
cookie,
)
).json();
expect(missing.records).toEqual([]);
});
it('shows a record as it stood at a create, against the live row', async () => {
const data = cborEncodeDagCbor({ $type: COLLECTION, text: 'v1' });
const cid = cidToString(await createCid(data));
const car = buildCarFile(cid, [{ cid, data }]);
const storage = createActorStorage({
async getRecord() {
return { cid, value: data };
},
});
storage.events.push({
seq: 5,
evt: cborEncode({
ops: [
{ path: `${COLLECTION}/3aaa`, action: 'create', cid: { $link: cid } },
],
time: '2026-01-01T00:00:00.000Z',
blocks: car,
}),
});
const pds = createPds({ actorStorage: storage });
const body = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&rkey=3aaa&seq=5`,
await sessionCookie(),
)
).json();
expect(body.snapshot).toEqual({
action: 'create',
time: '2026-01-01T00:00:00.000Z',
liveExists: true,
liveCurrent: true,
});
expect(body.records).toEqual([
{
uri: `at://${DID}/${COLLECTION}/3aaa`,
cid,
value: { $type: COLLECTION, text: 'v1' },
},
]);
});
it('recovers a deleted record from the event that wrote it', async () => {
const data = cborEncodeDagCbor({ $type: COLLECTION, text: 'v1' });
const cid = cidToString(await createCid(data));
const car = buildCarFile(cid, [{ cid, data }]);
const storage = createActorStorage();
storage.events.push(
{
seq: 5,
evt: cborEncode({
ops: [
{
path: `${COLLECTION}/3aaa`,
action: 'create',
cid: { $link: cid },
},
],
time: '2026-01-01T00:00:00.000Z',
blocks: car,
}),
},
{
seq: 6,
evt: cborEncode({
ops: [{ path: `${COLLECTION}/3aaa`, action: 'delete' }],
time: '2026-01-02T00:00:00.000Z',
blocks: new Uint8Array(0),
}),
},
);
const pds = createPds({ actorStorage: storage });
const body = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&rkey=3aaa&seq=6`,
await sessionCookie(),
)
).json();
expect(body.snapshot.action).toBe('delete');
expect(body.snapshot.liveExists).toBe(false);
expect(body.records[0].value.text).toBe('v1');
});
it('reports a change that is no longer available', async () => {
const pds = createPds();
const response = await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&rkey=3aaa&seq=9`,
await sessionCookie(),
);
expect((await response.json()).error).toBe(
'That change is no longer available.',
);
});
it('reads a space collection through the browser, refusing unknown spaces', async () => {
const SPACE = 'at://space.example/1';
const pds = createPds({
spaceStorage: createFakeSpaceStorage({
spaces: [{ uri: SPACE }],
records: {
[SPACE]: [
{
collection: COLLECTION,
rkey: '3aaa',
cid: 'bafyaaa',
value: { text: 'in space' },
},
],
},
}),
});
const cookie = await sessionCookie();
const unknown = await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&space=${encodeURIComponent('at://nope')}`,
cookie,
);
expect((await unknown.json()).error).toBe('No such space on this account.');
const body = await (
await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&space=${encodeURIComponent(SPACE)}`,
cookie,
)
).json();
expect(body.records).toEqual([
{
uri: `${SPACE}/${DID}/${COLLECTION}/3aaa`,
cid: 'bafyaaa',
indexedAt: '2026-01-01T00:00:00.000Z',
value: { text: 'in space' },
},
]);
});
it('refuses a space query when permissioned data is off', async () => {
const pds = createPds();
const response = await apiGet(
pds,
`/account/api/records?collection=${COLLECTION}&space=${encodeURIComponent('at://nope')}`,
await sessionCookie(),
);
expect((await response.json()).error).toBe('Permissioned data is off.');
});
});
describe('/account/api/blobs', () => {
const blob = {
cid: 'bafyblob',
mimeType: 'image/jpeg',
size: 2048,
createdAt: Date.parse('2026-01-01T00:00:00Z'),
time: Date.parse('2026-01-02T00:00:00Z'),
timeFromRecord: true,
records: [{ uri: `at://${DID}/app.bsky.feed.post/3aaa` }],
};
const facets = {
total: { count: 2, bytes: 4096 },
unused: { count: 1, bytes: 2048 },
kinds: [{ kind: 'image', count: 2, bytes: 4096 }],
apps: [{ authority: 'app.bsky', count: 2, bytes: 4096 }],
};
/** @param {(cursor: any, limit: any, options: any) => void} [onList] */
function blobPds(onList) {
const storage = createActorStorage({
async listBlobDetails(
/** @type {any} */ cursor,
/** @type {any} */ limit,
/** @type {any} */ options,
) {
onList?.(cursor, limit, options);
return { blobs: [blob], cursor: 'next-page' };
},
async getBlobFacets() {
return facets;
},
});
return createPds({ actorStorage: storage });
}
it('answers an error where the storage cannot list files', async () => {
const pds = createPds();
const response = await apiGet(
pds,
'/account/api/blobs',
await sessionCookie(),
);
expect((await response.json()).error).toBe(
'This server cannot list files.',
);
});
it('lists files with facets on the first page only', async () => {
/** @type {any[]} */
const calls = [];
const pds = blobPds((...args) => calls.push(args));
const cookie = await sessionCookie();
const first = await (
await apiGet(pds, '/account/api/blobs', cookie)
).json();
expect(first.facets.apps).toEqual([
{ domain: 'bsky.app', count: 2, bytes: 4096 },
]);
expect(first.blobs).toEqual([
{
cid: 'bafyblob',
mimeType: 'image/jpeg',
size: 2048,
createdAt: '2026-01-01T00:00:00.000Z',
time: '2026-01-02T00:00:00.000Z',
timeFromRecord: true,
records: [{ uri: `at://${DID}/app.bsky.feed.post/3aaa` }],
},
]);
expect(first.cursor).toBe('next-page');
const second = await (
await apiGet(pds, '/account/api/blobs?cursor=next-page', cookie)
).json();
expect(second.facets).toBeUndefined();
expect(calls[1][0]).toBe('next-page');
});
it('translates the filter params for the storage', async () => {
/** @type {any} */
let seen;
const pds = blobPds((_cursor, _limit, options) => {
seen = options;
});
await apiGet(
pds,
'/account/api/blobs?kind=image&app=bsky.app&unused=1&sort=largest',
await sessionCookie(),
);
expect(seen).toEqual({
kind: 'image',
appAuthority: 'app.bsky',
unused: true,
sort: 'largest',
});
});
});
describe('/account/api/blobs/relink', () => {
it('rebuilds the link table and reports the reference count', async () => {
const pds = createPds();
const response = await apiPost(
pds,
'/account/api/blobs/relink',
{},
await sessionCookie(),
);
expect(await response.json()).toEqual({ references: 0 });
});
});
describe('extensions on the account page', () => {
it('merges the flags an extension reports into features', async () => {
const pds = createPds({
extensions: [() => ({ name: 'demo', features: { demo: true } })],
});
const overview = await apiGet(
pds,
'/account/api/overview',
await sessionCookie(),
);
const { features } = await overview.json();
expect(features.demo).toBe(true);
expect(features.settings).toBe(true);
});
it('merges the counts an extension reports, across the public repo and spaces', async () => {
const SPACE =
'at://did:plc:abcdefghij234567abcdefgh/space/app.example.team/main';
const pds = createPds({
actorStorage: createActorStorage({
async listAllRecords() {
return ['1', '2', '3'].map((rkey) => ({
key: `app.example.note/${rkey}`,
cid: `bafy${rkey}`,
}));
},
}),
spaceStorage: createFakeSpaceStorage({
spaces: [
{ uri: SPACE, deletedAt: null },
{ uri: `${SPACE}-gone`, deletedAt: 'x' },
],
records: {
[SPACE]: [
{
collection: 'app.example.note',
rkey: 'a',
cid: 'bafya',
value: {},
},
{
collection: 'app.example.note',
rkey: 'b',
cid: 'bafyb',
value: {},
},
],
[`${SPACE}-gone`]: [
{
collection: 'app.example.note',
rkey: 'c',
cid: 'bafyc',
value: {},
},
],
},
}),
extensions: [
() => ({
name: 'notes',
stats: (count) => ({ notes: count('app.example.note') }),
}),
],
});
const overview = await apiGet(
pds,
'/account/api/overview',
await sessionCookie(),
);
const { stats } = await overview.json();
expect(stats.notes).toBe(5);
expect(stats.records).toBe(3);
});
it('answers an extension route behind the owner session', async () => {
const pds = createPds({
extensions: [
(host) => ({
name: 'demo',
routes: {
'/account/api/demo/list': {
handler: (request, url) =>
host.accountApi(request, url, async (did) => ({ did })),
},
},
}),
],
});
const anonymous = await pds.fetch(
new Request(`${BASE}/account/api/demo/list`),
);
expect(anonymous.status).toBe(401);
const res = await apiGet(
pds,
'/account/api/demo/list',
await sessionCookie(),
);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ did: DID });
});
});