~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
1314 lines · 36.6 KB Raw History
// 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 });
  });
});