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