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