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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

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