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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
114 lines · 3.4 KB Raw History
// getRecommendedDidCredentials must preserve the DID's existing rotation keys.
// Those keys are what can recover or repoint the identity; dropping them on a
// migration or handle change would let a lost server key lock the owner out of
// their DID permanently.

import { afterEach, describe, expect, it, vi } from 'vitest';
import { generateKeyPair } from '../packages/core/src/crypto.js';
import { createIdentityHandlers } from '../packages/core/src/handlers/xrpc-identity.js';
import { publicKeyToDidKey } from '../packages/core/src/plc.js';

const DID = 'did:plc:migrateexampleaccount000';

/** A storage stub already initialized with a given signing key. */
/** @param {Uint8Array} privateKey */
function storageWith(privateKey) {
  return {
    async getDid() {
      return DID;
    },
    async getPrivateKey() {
      return privateKey;
    },
    async getKemKey() {
      return null;
    },
    async getHandle() {
      return 'alice.example.com';
    },
  };
}

/** @param {Uint8Array} privateKey */
function identity(privateKey) {
  return createIdentityHandlers(
    /** @type {any} */ ({
      actorStorage: storageWith(privateKey),
      hostname: 'pds.example.com',
      plcUrl: 'https://plc.test',
      k256Signer: null,
      getDid: async () => DID,
    }),
  );
}

/** Stub the PLC directory's /data endpoint to report `rotationKeys`. */
/**
 * @param {string[]} rotationKeys
 * @param {{ok?: boolean}} [opts]
 */
function stubPlcData(rotationKeys, { ok = true } = {}) {
  vi.stubGlobal(
    'fetch',
    vi.fn(async () =>
      ok
        ? new Response(JSON.stringify({ rotationKeys }))
        : new Response('', { status: 404 }),
    ),
  );
}

describe('getRecommendedDidCredentials rotation keys', () => {
  afterEach(() => vi.unstubAllGlobals());

  it('keeps existing recovery keys and adds this server key first', async () => {
    const { privateKey, publicKey } = await generateKeyPair();
    const ourKey = publicKeyToDidKey(publicKey);
    const recovery = ['did:key:zRecoveryA', 'did:key:zRecoveryB'];
    stubPlcData(recovery);

    const creds = /** @type {any} */ (
      await identity(privateKey).getRecommendedDidCredentials()
    );
    // This server's key leads (so it is never dropped and can sign its own
    // ops); the owner's recovery keys are preserved after it.
    expect(creds.rotationKeys).toEqual([ourKey, ...recovery]);
  });

  it("includes this server's key even when the DID is already at the 5-key limit", async () => {
    const { privateKey, publicKey } = await generateKeyPair();
    const ourKey = publicKeyToDidKey(publicKey);
    const five = [
      'did:key:z1',
      'did:key:z2',
      'did:key:z3',
      'did:key:z4',
      'did:key:z5',
    ];
    stubPlcData(five);

    const creds = /** @type {any} */ (
      await identity(privateKey).getRecommendedDidCredentials()
    );
    // This server's key leads; the lowest-priority existing key (z5) is what
    // the 5-key cap drops.
    expect(creds.rotationKeys).toEqual([
      ourKey,
      'did:key:z1',
      'did:key:z2',
      'did:key:z3',
      'did:key:z4',
    ]);
  });

  it('falls back to this server key alone when the directory is unreachable', async () => {
    const { privateKey, publicKey } = await generateKeyPair();
    const ourKey = publicKeyToDidKey(publicKey);
    stubPlcData([], { ok: false });

    const creds = /** @type {any} */ (
      await identity(privateKey).getRecommendedDidCredentials()
    );
    expect(creds.rotationKeys).toEqual([ourKey]);
  });
});