// /init adopts a DID and its signing key. On an empty server that is the // bootstrap path — there is no account to authenticate as yet. Once a DID is // adopted the same endpoint could repoint the account at an attacker's // identity and key, so from then on it takes the account's own credentials. import { describe, expect, it } from 'vitest'; import { createAccessJwt } from '../packages/core/src/auth.js'; import { bytesToHex, generateKeyPair } from '../packages/core/src/crypto.js'; import { PersonalDataServer } from '../packages/core/src/pds.js'; const DID = 'did:plc:initexampleaccountaaaaaa'; const JWT_SECRET = 'test-secret'; /** @param {{did?: string|null, password?: string}} [opts] */ function server(opts = {}) { const state = { did: opts.did ?? null, /** @type {Uint8Array|null} */ privateKey: null, /** @type {Uint8Array|null} */ kemKey: null, handle: /** @type {string|null} */ (null), }; const pds = new PersonalDataServer({ actorStorage: /** @type {any} */ ({ async getDid() { return state.did; }, async setDid(/** @type {string} */ did) { state.did = did; }, async getPrivateKey() { return state.privateKey; }, async setPrivateKey(/** @type {Uint8Array} */ key) { state.privateKey = key; }, async getKemKey() { return state.kemKey ?? null; }, async setKemKey(/** @type {Uint8Array} */ key) { state.kemKey = key; }, async getHandle() { return state.handle; }, async setHandle(/** @type {string} */ handle) { state.handle = handle; }, async getLatestCommit() { return null; }, async getLatestSeq() { return 0; }, async putEvent() {}, }), sharedStorage: /** @type {any} */ ({ async putActor() {}, }), blobs: /** @type {any} */ ({}), jwtSecret: JWT_SECRET, hostname: 'pds.example.com', password: opts.password, }); return { pds, state }; } async function initRequest( /** @type {{token?: string, password?: string}} */ opts = {}, ) { const { privateKey } = await generateKeyPair(); /** @type {Record} */ const headers = { 'Content-Type': 'application/json' }; if (opts.token) headers.Authorization = `Bearer ${opts.token}`; return new Request('https://pds.example.com/init', { method: 'POST', headers, body: JSON.stringify({ did: DID, privateKey: bytesToHex(privateKey), handle: 'alice.example.com', ...(opts.password ? { password: opts.password } : {}), }), }); } describe('/init', () => { it('bootstraps an empty server without credentials', async () => { const { pds, state } = server(); const res = await pds.fetch(await initRequest()); expect(res.status).toBe(200); expect(state.did).toBe(DID); expect(state.privateKey).not.toBeNull(); }); it('refuses to repoint an initialized server without credentials', async () => { const { pds, state } = server({ did: 'did:plc:existingaccountaaaaaaaa' }); const res = await pds.fetch(await initRequest()); expect(res.status).toBe(401); expect((await res.json()).error).toBe('AuthenticationRequired'); // The identity and key are untouched: nothing was adopted. expect(state.did).toBe('did:plc:existingaccountaaaaaaaa'); expect(state.privateKey).toBeNull(); }); it('lets the account itself repoint an initialized server', async () => { const existing = 'did:plc:existingaccountaaaaaaaa'; const { pds, state } = server({ did: existing }); const token = await createAccessJwt(existing, JWT_SECRET); const res = await pds.fetch(await initRequest({ token })); expect(res.status).toBe(200); expect(state.did).toBe(DID); }); // A deployment can set the password before the first request reaches the // server. That closes the window between deploy and bootstrap, where the // endpoint would otherwise adopt any caller's identity. it('requires the configured password to bootstrap an empty server', async () => { const { pds, state } = server({ password: 'hunter2' }); const res = await pds.fetch(await initRequest()); expect(res.status).toBe(401); expect((await res.json()).error).toBe('AuthenticationRequired'); expect(state.did).toBeNull(); expect(state.privateKey).toBeNull(); }); it('rejects a wrong bootstrap password', async () => { const { pds, state } = server({ password: 'hunter2' }); const res = await pds.fetch(await initRequest({ password: 'hunter3' })); expect(res.status).toBe(401); expect(state.did).toBeNull(); }); it('bootstraps with the configured password', async () => { const { pds, state } = server({ password: 'hunter2' }); const res = await pds.fetch(await initRequest({ password: 'hunter2' })); expect(res.status).toBe(200); expect(state.did).toBe(DID); expect(state.privateKey).not.toBeNull(); }); });