chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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);
});
});