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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
311 lines · 9.6 KB Raw History
import Database from 'better-sqlite3';
import { beforeEach, describe, expect, it } from 'vitest';
import { createSharedStorageSql as createCloudflareStorage } from '../packages/cloudflare/src/index.js';
import { createSharedStorage as createSqliteStorage } from '../packages/storage-sqlite/src/index.js';

// The two adapters answer the same contract from mirrored schemas and share no
// code: one runs on better-sqlite3, the other on Durable Object SQLite. Running
// one suite against both is what keeps them honest about the same behaviour.

const DID = 'did:plc:alice';
const OTHER_DID = 'did:plc:bob';

/**
 * A Durable Object `state.storage.sql` over better-sqlite3. The dialects differ
 * in places, so a pass here is not a promise the query runs on the real thing.
 * @param {Database.Database} db
 */
function createDoSql(db) {
  return {
    /** @param {string} query @param {...unknown} params */
    exec(query, ...params) {
      let statement;
      try {
        statement = db.prepare(query);
      } catch (err) {
        // A schema string holds several statements; prepare() takes one.
        if (!/more than one statement/.test(String(err))) throw err;
        db.exec(query);
        return { toArray: () => [], rowsRead: 0, rowsWritten: 0 };
      }
      if (statement.reader) {
        const rows = statement.all(...params);
        return { toArray: () => rows, rowsRead: rows.length, rowsWritten: 0 };
      }
      const info = statement.run(...params);
      return { toArray: () => [], rowsRead: 0, rowsWritten: info.changes };
    },
  };
}

const adapters = {
  'storage-sqlite': () => createSqliteStorage(new Database(':memory:')),
  'cloudflare-do-sqlite': () =>
    createCloudflareStorage(
      /** @type {any} */ (createDoSql(new Database(':memory:'))),
    ),
};

for (const [name, create] of Object.entries(adapters)) {
  describe(`app password storage (${name})`, () => {
    /** @type {ReturnType<typeof create>} */
    let storage;

    beforeEach(() => {
      storage = create();
    });

    it('round-trips a password by hash', async () => {
      const stored = await storage.putAppPassword(
        DID,
        'client',
        'hash-a',
        false,
        '2026-01-01T00:00:00.000Z',
      );
      expect(stored).toBe(true);

      expect(await storage.getAppPasswordByHash(DID, 'hash-a')).toEqual({
        name: 'client',
        privileged: false,
        createdAt: '2026-01-01T00:00:00.000Z',
      });
    });

    it('preserves the privileged flag', async () => {
      await storage.putAppPassword(
        DID,
        'trusted',
        'hash-p',
        true,
        '2026-01-01T00:00:00.000Z',
      );
      const found = await storage.getAppPasswordByHash(DID, 'hash-p');
      expect(found?.privileged).toBe(true);
    });

    it('returns null for an unknown hash', async () => {
      expect(await storage.getAppPasswordByHash(DID, 'nope')).toBeNull();
    });

    it('refuses a duplicate name without disturbing the original', async () => {
      await storage.putAppPassword(
        DID,
        'client',
        'hash-first',
        false,
        '2026-01-01T00:00:00.000Z',
      );
      const second = await storage.putAppPassword(
        DID,
        'client',
        'hash-second',
        true,
        '2026-01-02T00:00:00.000Z',
      );
      expect(second).toBe(false);

      // The first password must still authenticate, and the rejected one must not.
      expect(
        await storage.getAppPasswordByHash(DID, 'hash-first'),
      ).not.toBeNull();
      expect(await storage.getAppPasswordByHash(DID, 'hash-second')).toBeNull();
    });

    it('scopes passwords to their account', async () => {
      await storage.putAppPassword(
        DID,
        'client',
        'hash-shared',
        false,
        '2026-01-01T00:00:00.000Z',
      );

      // Same hash, different DID: must not authenticate. In practice the hash
      // is salted per-DID so this cannot collide, but the query must not rely
      // on that.
      expect(
        await storage.getAppPasswordByHash(OTHER_DID, 'hash-shared'),
      ).toBeNull();
      expect(await storage.listAppPasswords(OTHER_DID)).toEqual([]);
    });

    it('lists passwords oldest first and never exposes a hash', async () => {
      await storage.putAppPassword(
        DID,
        'second',
        'h2',
        false,
        '2026-02-01T00:00:00.000Z',
      );
      await storage.putAppPassword(
        DID,
        'first',
        'h1',
        true,
        '2026-01-01T00:00:00.000Z',
      );

      const listed = await storage.listAppPasswords(DID);
      expect(listed.map((p) => p.name)).toEqual(['first', 'second']);
      expect(listed[0]).toEqual({
        name: 'first',
        privileged: true,
        createdAt: '2026-01-01T00:00:00.000Z',
      });
      for (const entry of listed) {
        expect(Object.keys(entry).sort()).toEqual([
          'createdAt',
          'name',
          'privileged',
        ]);
      }
    });

    it('deletes by name and stops the hash authenticating', async () => {
      await storage.putAppPassword(
        DID,
        'client',
        'hash-x',
        false,
        '2026-01-01T00:00:00.000Z',
      );

      expect(await storage.deleteAppPassword(DID, 'client')).toBe(true);
      expect(await storage.getAppPasswordByHash(DID, 'hash-x')).toBeNull();
      expect(await storage.listAppPasswords(DID)).toEqual([]);
    });

    it('reports deleting a name that is not there', async () => {
      expect(await storage.deleteAppPassword(DID, 'ghost')).toBe(false);
    });

    it('frees the name once revoked', async () => {
      await storage.putAppPassword(
        DID,
        'client',
        'hash-old',
        false,
        '2026-01-01T00:00:00.000Z',
      );
      await storage.deleteAppPassword(DID, 'client');

      const reused = await storage.putAppPassword(
        DID,
        'client',
        'hash-new',
        false,
        '2026-01-03T00:00:00.000Z',
      );
      expect(reused).toBe(true);
      expect(
        await storage.getAppPasswordByHash(DID, 'hash-new'),
      ).not.toBeNull();
      expect(await storage.getAppPasswordByHash(DID, 'hash-old')).toBeNull();
    });
  });

  describe(`oauth token storage (${name})`, () => {
    /** @type {ReturnType<typeof create>} */
    let storage;

    beforeEach(() => {
      storage = create();
    });

    it("lists a DID's tokens with the key needed to revoke one", async () => {
      await storage.putOAuthToken('refresh-a', {
        did: DID,
        clientId: 'https://app.example',
        scope: 'atproto',
        createdAt: 1,
      });
      await storage.putOAuthToken('refresh-b', {
        did: OTHER_DID,
        clientId: 'https://other.example',
        createdAt: 2,
      });

      const listed = await storage.listOAuthTokensByDid(DID);
      expect(listed).toHaveLength(1);
      expect(listed[0].clientId).toBe('https://app.example');
      expect(listed[0].tokenId).toBe('refresh-a');
    });

    it('deletes the token a listing pointed at', async () => {
      await storage.putOAuthToken('refresh-a', { did: DID, createdAt: 1 });
      await storage.putOAuthToken('refresh-c', { did: DID, createdAt: 2 });

      const [first] = await storage.listOAuthTokensByDid(DID);
      await storage.deleteOAuthToken(/** @type {string} */ (first.tokenId));

      const remaining = await storage.listOAuthTokensByDid(DID);
      expect(remaining).toHaveLength(1);
      expect(remaining[0].tokenId).not.toBe(first.tokenId);
    });
  });

  describe(`passkey storage (${name})`, () => {
    /** @type {ReturnType<typeof create>} */
    let storage;

    /** @param {string} credentialId @param {string} did */
    const record = (credentialId, did = DID) => ({
      credentialId,
      did,
      name: 'laptop',
      publicKey: 'spki-bytes',
      algorithm: -7,
      signCount: 0,
      createdAt: '2026-01-01T00:00:00.000Z',
      lastUsedAt: null,
    });

    beforeEach(() => {
      storage = create();
    });

    it('round-trips a credential by its id alone', async () => {
      expect(await storage.putPasskey(record('cred-a'))).toBe(true);

      // A sign-in knows the credential and not the account
      const found = await storage.getPasskey('cred-a');
      expect(found?.did).toBe(DID);
      expect(found?.publicKey).toBe('spki-bytes');
      expect(found?.algorithm).toBe(-7);
    });

    it("lists only the account's own credentials", async () => {
      await storage.putPasskey(record('cred-a'));
      await storage.putPasskey(record('cred-b', OTHER_DID));

      const listed = await storage.listPasskeys(DID);
      expect(listed).toHaveLength(1);
      expect(listed[0].credentialId).toBe('cred-a');
    });

    it('refuses to register the same credential twice', async () => {
      await storage.putPasskey(record('cred-a'));
      expect(await storage.putPasskey(record('cred-a'))).toBe(false);
    });

    it('records the counter and last use', async () => {
      await storage.putPasskey(record('cred-a'));
      await storage.touchPasskey('cred-a', 42, '2026-02-02T00:00:00.000Z');

      const found = await storage.getPasskey('cred-a');
      expect(found?.signCount).toBe(42);
      expect(found?.lastUsedAt).toBe('2026-02-02T00:00:00.000Z');
    });

    it('deletes only on a matching account', async () => {
      await storage.putPasskey(record('cred-a'));
      expect(await storage.deletePasskey(OTHER_DID, 'cred-a')).toBe(false);
      expect(await storage.getPasskey('cred-a')).not.toBeNull();

      expect(await storage.deletePasskey(DID, 'cred-a')).toBe(true);
      expect(await storage.getPasskey('cred-a')).toBeNull();
      expect(await storage.listPasskeys(DID)).toHaveLength(0);
    });
  });
}