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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
504 lines · 15.5 KB Raw History
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
  combineOffers,
  lastActiveAt,
  mergeApps,
  pickLangString,
  scopeNsids,
} from '../src/lib/format.js';

describe('lastActiveAt', () => {
  it('returns the newest session timestamp', () => {
    expect(
      lastActiveAt({
        sessions: [
          { lastAccessedAt: '2026-07-01T00:00:00.000Z' },
          { lastAccessedAt: '2026-08-01T00:00:00.000Z' },
          { lastAccessedAt: '2026-06-01T00:00:00.000Z' },
        ],
      }),
    ).toBe('2026-08-01T00:00:00.000Z');
  });

  it('returns the latest record write for a data-only app', () => {
    expect(
      lastActiveAt({
        collections: [
          { lastRecordAt: Date.parse('2026-05-01T00:00:00.000Z') },
          { lastRecordAt: Date.parse('2026-07-15T00:00:00.000Z') },
          { lastRecordAt: null },
        ],
      }),
    ).toBe('2026-07-15T00:00:00.000Z');
  });

  it('takes the newest of a session and a record write', () => {
    expect(
      lastActiveAt({
        sessions: [{ lastAccessedAt: '2026-07-01T00:00:00.000Z' }],
        collections: [{ lastRecordAt: Date.parse('2026-08-10T00:00:00.000Z') }],
      }),
    ).toBe('2026-08-10T00:00:00.000Z');
  });

  it('takes a space write over an older session and repo write', () => {
    expect(
      lastActiveAt({
        sessions: [{ lastAccessedAt: '2026-07-01T00:00:00.000Z' }],
        collections: [{ lastRecordAt: Date.parse('2026-07-20T00:00:00.000Z') }],
        spaces: [
          {
            collections: [
              { lastRecordAt: Date.parse('2026-08-09T00:00:00.000Z') },
              { lastRecordAt: null },
            ],
          },
        ],
      }),
    ).toBe('2026-08-09T00:00:00.000Z');
  });

  it('returns the latest space write for an app that stores nothing else', () => {
    expect(
      lastActiveAt({
        spaces: [
          {
            collections: [
              { lastRecordAt: Date.parse('2026-06-01T00:00:00.000Z') },
            ],
          },
          {
            collections: [
              { lastRecordAt: Date.parse('2026-08-02T00:00:00.000Z') },
            ],
          },
        ],
      }),
    ).toBe('2026-08-02T00:00:00.000Z');
  });

  it('returns null when no session, record or space time is known', () => {
    expect(lastActiveAt({ sessions: [] })).toBeNull();
    expect(lastActiveAt({})).toBeNull();
    expect(lastActiveAt({ collections: [{ lastRecordAt: null }] })).toBeNull();
    expect(lastActiveAt({ spaces: [{ collections: [] }] })).toBeNull();
  });
});

describe('mergeApps ordering', () => {
  // Collection authority domains that match their app's client host, so a
  // record bucket and its OAuth session fold into one row.
  const collections = [
    { name: 'app.bsky.feed.post', count: 100 }, // -> bsky.app
    { name: 'social.grain.photo', count: 5 }, // -> grain.social
  ];

  it('puts the most recently accessed session first, regardless of record count', () => {
    const oauthApps = [
      {
        clientId: 'https://bsky.app/client-metadata.json',
        lastAccessedAt: '2026-07-01T00:00:00.000Z',
      },
      {
        clientId: 'https://grain.social/client-metadata.json',
        lastAccessedAt: '2026-08-01T00:00:00.000Z',
      },
    ];
    const apps = mergeApps(collections, oauthApps);
    // grain.social has far fewer records but was accessed more recently.
    expect(apps.map((a) => a.domain)).toEqual(['grain.social', 'bsky.app']);
  });

  it('falls back to record count for apps that have no live session', () => {
    // No sessions at all: pure data apps sort by record count, most first.
    const apps = mergeApps(collections, []);
    expect(apps.map((a) => a.domain)).toEqual(['bsky.app', 'grain.social']);
  });

  it('orders data-only apps by their latest record write, not just count', () => {
    const dated = [
      // Many records, but the newest is old.
      {
        name: 'app.bsky.feed.post',
        count: 100,
        lastRecordAt: Date.parse('2026-01-01T00:00:00.000Z'),
      },
      // Few records, but written recently.
      {
        name: 'social.grain.photo',
        count: 5,
        lastRecordAt: Date.parse('2026-08-01T00:00:00.000Z'),
      },
    ];
    const apps = mergeApps(dated, []);
    expect(apps.map((a) => a.domain)).toEqual(['grain.social', 'bsky.app']);
  });

  it('sorts apps with sessions ahead of data-only apps', () => {
    const oauthApps = [
      {
        // tangled.org holds a session but stores no records here.
        clientId: 'https://tangled.org/client-metadata.json',
        lastAccessedAt: '2026-08-01T00:00:00.000Z',
      },
    ];
    const apps = mergeApps(collections, oauthApps);
    // The app with recent activity leads even the 100-record bsky.app.
    expect(apps[0].domain).toBe('tangled.org');
  });

  it('orders an app by a space write newer than anything in the repo', () => {
    const oauthApps = [
      {
        clientId: 'https://bsky.app/client-metadata.json',
        lastAccessedAt: '2026-08-01T00:00:00.000Z',
      },
      {
        clientId: 'https://grain.social/client-metadata.json',
        lastAccessedAt: '2026-07-01T00:00:00.000Z',
      },
    ];
    const spaces = [
      {
        spaceType: 'social.grain.space',
        collections: [
          {
            collection: 'social.grain.photo',
            lastRecordAt: Date.parse('2026-08-09T00:00:00.000Z'),
          },
        ],
      },
    ];
    const apps = mergeApps(collections, oauthApps, {}, spaces);
    expect(apps.map((a) => a.domain)).toEqual(['grain.social', 'bsky.app']);
    expect(apps[0].spaces).toHaveLength(1);
  });

  it('gives a space to the app whose collections it holds', () => {
    const spaces = [
      {
        spaceType: 'community.lexicon.space',
        collections: [{ collection: 'social.grain.photo', lastRecordAt: null }],
      },
    ];
    const apps = mergeApps(collections, [], {}, spaces);
    const grain = apps.find((a) => a.domain === 'grain.social');
    expect(grain?.spaces).toHaveLength(1);
    expect(apps.find((a) => a.domain === 'bsky.app')?.spaces).toEqual([]);
  });

  it('gives a space to the row that absorbed the domain it belongs to', () => {
    const spaces = [
      {
        spaceType: 'sh.tangled.space',
        collections: [
          {
            collection: 'sh.tangled.repo',
            lastRecordAt: Date.parse('2026-08-09T00:00:00.000Z'),
          },
        ],
      },
    ];
    const apps = mergeApps(
      [{ name: 'sh.tangled.repo', count: 40 }],
      [
        {
          clientId: 'https://tangled.org/client-metadata.json',
          lastAccessedAt: '2026-01-01T00:00:00.000Z',
        },
      ],
      { 'tangled.sh': 'tangled.org' },
      spaces,
    );
    expect(apps).toHaveLength(1);
    expect(apps[0].domain).toBe('tangled.org');
    expect(apps[0].folded).toEqual(['tangled.sh']);
    expect(apps[0].spaces).toHaveLength(1);
    expect(lastActiveAt(apps[0])).toBe('2026-08-09T00:00:00.000Z');
  });
});

describe('scopeNsids', () => {
  it('reads the NSIDs granular repo and rpc grants name', () => {
    expect(
      scopeNsids(
        'atproto repo:sh.tangled.repo?action=create rpc:sh.tangled.ci.triggerPipeline?aud=* blob:*/*',
      ),
    ).toEqual(['sh.tangled.repo', 'sh.tangled.ci.triggerPipeline']);
  });

  it('trims a wildcard tail down to the prefix that still names an authority', () => {
    expect(scopeNsids('repo:sh.tangled.*')).toEqual(['sh.tangled']);
  });

  it('names nothing for a generic grant or a bare wildcard', () => {
    expect(scopeNsids('atproto transition:generic')).toEqual([]);
    expect(scopeNsids('repo:*')).toEqual([]);
  });

  it('reads the query forms the scope grammar also allows', () => {
    expect(
      scopeNsids(
        'repo?collection=sh.tangled.repo&action=create rpc?lxm=sh.tangled.knot.addMember&aud=*',
      ),
    ).toEqual(['sh.tangled.repo', 'sh.tangled.knot.addMember']);
  });
});

describe('combineOffers', () => {
  // The rebrand signature: tangled moved its sign-in to a new TLD while its
  // records keep the old NSID authority.
  const rebrand = [
    {
      domain: 'tangled.org',
      count: 0,
      sessions: [{ scope: 'atproto repo:sh.tangled.repo?action=create' }],
    },
    { domain: 'tangled.sh', count: 40, sessions: [] },
  ];

  it('offers to fold the record row into the session row after a rebrand', () => {
    expect(combineOffers(rebrand)).toEqual([
      { from: 'tangled.sh', to: 'tangled.org' },
    ]);
  });

  it('still offers when the session grant is generic', () => {
    const generic = [
      {
        domain: 'tangled.org',
        count: 0,
        sessions: [{ scope: 'atproto transition:generic' }],
      },
      rebrand[1],
    ];
    expect(combineOffers(generic)).toEqual([
      { from: 'tangled.sh', to: 'tangled.org' },
    ]);
  });

  it('never pairs rows whose names differ, even when scopes would cover them', () => {
    // A third-party client writing another app's lexicons is not that app.
    const alt = [
      {
        domain: 'deer.social',
        count: 0,
        sessions: [{ scope: 'atproto repo:app.bsky.feed.post' }],
      },
      { domain: 'bsky.app', count: 100, sessions: [] },
    ];
    expect(combineOffers(alt)).toEqual([]);
  });

  it('drops a pair whose granular scopes never touch the record authority', () => {
    // A look-alike domain that only ever asked for its own lexicons.
    const squat = [
      {
        domain: 'tangled.click',
        count: 0,
        sessions: [{ scope: 'atproto repo:click.tangled.note' }],
      },
      rebrand[1],
    ];
    expect(combineOffers(squat)).toEqual([]);
  });

  it('stays quiet once the pair is combined or turned down', () => {
    expect(
      combineOffers(rebrand, { aliases: { 'tangled.sh': 'tangled.org' } }),
    ).toEqual([]);
    expect(
      combineOffers(rebrand, { dismissedPairs: ['tangled.sh>tangled.org'] }),
    ).toEqual([]);
  });

  it('folds a subdomain client into the apex the records are named by', () => {
    // The app is known as currents.is; api.currents.is only hosts the
    // sign-in, so the apex keeps the row.
    const subdomain = [
      {
        domain: 'api.currents.is',
        count: 0,
        sessions: [{ scope: 'atproto repo:is.currents.note' }],
      },
      { domain: 'currents.is', count: 12, sessions: [] },
    ];
    expect(combineOffers(subdomain)).toEqual([
      { from: 'api.currents.is', to: 'currents.is' },
    ]);
  });

  it('keys dismissals by the direction the offer would fold', () => {
    const subdomain = [
      { domain: 'api.currents.is', count: 0, sessions: [{ scope: 'atproto' }] },
      { domain: 'currents.is', count: 12, sessions: [] },
    ];
    expect(
      combineOffers(subdomain, {
        dismissedPairs: ['api.currents.is>currents.is'],
      }),
    ).toEqual([]);
  });

  it('needs the complement: two rows that both hold records never pair', () => {
    const both = [
      {
        domain: 'tangled.org',
        count: 3,
        sessions: [{ scope: 'atproto repo:sh.tangled.repo' }],
      },
      rebrand[1],
    ];
    expect(combineOffers(both)).toEqual([]);
  });
});

describe('mergeApps aliases', () => {
  const collections = [
    {
      name: 'sh.tangled.repo',
      count: 40,
      lastRecordAt: Date.parse('2026-08-01T00:00:00.000Z'),
    },
    { name: 'app.bsky.feed.post', count: 100 },
  ];
  const oauthApps = [
    {
      clientId: 'https://tangled.org/client-metadata.json',
      lastAccessedAt: '2026-08-05T00:00:00.000Z',
    },
  ];

  it('folds an aliased record row into the row that absorbed it', () => {
    const apps = mergeApps(collections, oauthApps, {
      'tangled.sh': 'tangled.org',
    });
    expect(apps.map((a) => a.domain)).toEqual(['tangled.org', 'bsky.app']);
    const tangled = apps[0];
    expect(tangled.count).toBe(40);
    expect(tangled.collections.map((c) => c.name)).toEqual(['sh.tangled.repo']);
    expect(tangled.sessions).toHaveLength(1);
    expect(tangled.folded).toEqual(['tangled.sh']);
  });

  it('keeps a folded row alive when its session is gone', () => {
    const apps = mergeApps(collections, [], { 'tangled.sh': 'tangled.org' });
    const tangled = apps.find((a) => a.domain === 'tangled.org');
    expect(tangled?.count).toBe(40);
    expect(tangled?.sessions).toEqual([]);
  });

  it('folds a session-only client row into the record row it signs in for', () => {
    const apps = mergeApps(
      [{ name: 'is.currents.note', count: 12 }],
      [
        {
          clientId: 'https://api.currents.is/oauth/client-metadata.json',
          lastAccessedAt: '2026-08-05T00:00:00.000Z',
        },
      ],
      { 'api.currents.is': 'currents.is' },
    );
    expect(apps).toHaveLength(1);
    expect(apps[0].domain).toBe('currents.is');
    expect(apps[0].count).toBe(12);
    expect(apps[0].sessions).toHaveLength(1);
    expect(apps[0].folded).toEqual(['api.currents.is']);
  });

  it('ignores an alias whose source row does not exist', () => {
    const apps = mergeApps(collections, oauthApps, {
      'gone.example': 'tangled.org',
    });
    expect(
      apps.find((a) => a.domain === 'tangled.org')?.folded,
    ).toBeUndefined();
    expect(apps.find((a) => a.domain === 'tangled.sh')).toBeDefined();
  });
});

describe('pickLangString', () => {
  /** @param {string[]} languages */
  const speak = (languages) => {
    vi.stubGlobal('navigator', { languages });
  };
  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('keeps the English base for an English-first reader', () => {
    // The untagged base is English by convention, so a French translation
    // later in the reader's language list must not beat it.
    speak(['en-US', 'fr']);
    expect(pickLangString('Full access', { fr: 'Accès complet' })).toBe(
      'Full access',
    );
  });

  it("prefers a translation in the reader's first language", () => {
    speak(['fr-BE', 'en']);
    expect(pickLangString('Full access', { fr: 'Accès complet' })).toBe(
      'Accès complet',
    );
  });

  it('reaches across regions of the same language', () => {
    speak(['pt-PT']);
    expect(pickLangString('Full access', { 'pt-BR': 'Acesso total' })).toBe(
      'Acesso total',
    );
  });

  it('lets an explicit en tag beat the base', () => {
    speak(['en-US']);
    expect(pickLangString('base', { en: 'tagged English' })).toBe(
      'tagged English',
    );
  });

  it('falls back to the base when nothing matches', () => {
    speak(['ja']);
    expect(pickLangString('Full access', { fr: 'Accès complet' })).toBe(
      'Full access',
    );
  });
});

describe('scopeDisplay', () => {
  it('parses a granted scope string into the consent display shape', async () => {
    const { scopeDisplay } = await import('../src/lib/scope-display.js');
    const perms = scopeDisplay(
      'atproto repo:social.grain.photo repo:app.bsky.feed.post?action=create rpc:social.grain.actor.getProfile?aud=did:web:api.grain.social#grain blob:image/* account:email',
    );
    expect(perms.hasAtproto).toBe(true);
    expect(perms.hasEmail).toBe(true);
    expect(perms.repo).toEqual([
      {
        collection: 'social.grain.photo',
        create: true,
        update: true,
        delete: true,
      },
      {
        collection: 'app.bsky.feed.post',
        create: true,
        update: false,
        delete: false,
      },
    ]);
    expect(perms.rpc).toEqual([
      {
        aud: 'did:web:api.grain.social#grain',
        lxms: ['social.grain.actor.getProfile'],
      },
    ]);
    expect(perms.blobs).toEqual(['image/*']);
  });

  it('renders a bare atproto grant as identity-only', async () => {
    const { scopeDisplay } = await import('../src/lib/scope-display.js');
    const perms = scopeDisplay('atproto');
    expect(perms.repo).toEqual([]);
    expect(perms.hasAtproto).toBe(true);
  });
});