chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// Password/app-password logins are listed only while their refresh token is
// still valid. listLiveSessions drops the expired ones and prunes them from
// storage, so a client that re-logs in rather than refreshing cannot pile up
// dead records that overstate who has access.
import { describe, expect, it } from 'vitest';
import { createRefreshJwt } from '../packages/core/src/auth.js';
import { PersonalDataServer } from '../packages/core/src/pds.js';
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
/**
* A mock session store that records which jtis were deleted.
* @param {any[]} sessions
*/
function makeStore(sessions) {
const rows = new Map(sessions.map((/** @type {any} */ s) => [s.jti, s]));
/** @type {string[]} */
const deleted = [];
return {
deleted,
storage: {
putSession: async (/** @type {any} */ s) => void rows.set(s.jti, s),
getSession: async (/** @type {any} */ jti) => rows.get(jti) || null,
listSessions: async () => [...rows.values()],
deleteSession: async (/** @type {any} */ jti) => {
deleted.push(jti);
rows.delete(jti);
},
},
};
}
/** @param {any} storage */
function makeServer(storage) {
return new PersonalDataServer({
actorStorage: /** @type {any} */ ({}),
blobs: /** @type {any} */ ({}),
jwtSecret: 'test-secret',
hostname: 'pds.example.com',
sharedStorage: /** @type {any} */ (storage),
});
}
describe('listLiveSessions', () => {
it('keeps live logins and prunes expired ones', async () => {
const now = Date.now();
const nowSec = Math.floor(now / 1000);
const { storage, deleted } = makeStore([
// Live: stored expiry is in the future, and it wins over the legacy
// window even though the row was created days ago.
{
jti: 'live-token',
did: 'did:plc:x',
createdAt: new Date(now - 5 * DAY).toISOString(),
refreshedAt: null,
expiresAt: nowSec + 3600,
},
// Expired: stored expiry has passed, even though it was just created.
{
jti: 'dead-token',
did: 'did:plc:x',
createdAt: new Date(now).toISOString(),
refreshedAt: null,
expiresAt: nowSec - 100,
},
// Legacy row with no expiresAt, inside the 24h window -> kept.
{
jti: 'legacy-fresh',
did: 'did:plc:x',
createdAt: new Date(now - 1 * HOUR).toISOString(),
refreshedAt: null,
},
// Legacy row with no expiresAt, past the 24h window -> pruned.
{
jti: 'legacy-stale',
did: 'did:plc:x',
createdAt: new Date(now - 2 * DAY).toISOString(),
refreshedAt: null,
},
]);
const pds = makeServer(storage);
const live = await pds._sessions.listLiveSessions('did:plc:x');
expect(live.map((s) => s.jti).sort()).toEqual([
'legacy-fresh',
'live-token',
]);
expect(deleted.sort()).toEqual(['dead-token', 'legacy-stale']);
});
it('is empty and prunes nothing when no session store is configured', async () => {
const pds = makeServer({});
expect(await pds._sessions.listLiveSessions('did:plc:x')).toEqual([]);
});
});
describe('recordSession dedup', () => {
const DID = 'did:plc:x';
const req = (/** @type {string} */ ua) =>
new Request('https://pds.example.com/', {
headers: { 'user-agent': ua },
});
const login = async (
/** @type {any} */ pds,
/** @type {string} */ label,
/** @type {string} */ ua,
) => {
const jwt = await createRefreshJwt(DID, 'test-secret', 3600);
await pds._sessions.recordSession(jwt, {
did: DID,
label,
scope: 'atproto',
request: req(ua),
// Collapsing a client's prior session belongs to a login, not a rotation.
dedupe: true,
});
};
it('collapses re-logins from the same client but keeps distinct devices', async () => {
const { storage } = makeStore([]);
const pds = makeServer(storage);
// Same app password, same device, three sign-ins -> one live session.
await login(pds, 'App password: bot', 'bot/1.0');
await login(pds, 'App password: bot', 'bot/1.0');
await login(pds, 'App password: bot', 'bot/1.0');
expect(await pds._sessions.listLiveSessions(DID)).toHaveLength(1);
// Same credential from a different device is a separate session.
await login(pds, 'App password: bot', 'other-device/2.0');
expect(await pds._sessions.listLiveSessions(DID)).toHaveLength(2);
// A different credential is also its own session.
await login(pds, 'Account password', 'bot/1.0');
expect(await pds._sessions.listLiveSessions(DID)).toHaveLength(3);
});
});