Something went wrong. Try again.
An AT Protocol Personal Data Server written in JavaScript pdsjs.dev
pds atproto
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350#!/usr/bin/env node
/** * PDS Setup Script * * Registers a did:plc, initializes the PDS, and notifies the relay. * * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev */
import { writeFileSync } from 'node:fs';import { base32Encode, bytesToHex } from '@pdsjs/core/crypto';import { publicKeyToDidKey, signPlcOperationObject } from '@pdsjs/core/plc';import { cborEncodeDagCbor } from '@pdsjs/core/repo';import { k256Signer } from '@pdsjs/node';
// === CRYPTO HELPERS ===
/** * An account signing key on either curve, with the signer the PLC operation * needs. secp256k1 is the default: the reference PDS gives every account one, * and some third-party verifiers (Go feed generators, labelers) accept no * other curve for service auth. WebCrypto cannot sign it, so the scalar and * the signer come from the adapter's injected implementation. * @param {'secp256k1'|'p256'} curve */async function generateAccountKey(curve) { if (curve === 'secp256k1') { const privateKey = k256Signer.generatePrivateKey(); const publicKey = k256Signer.getPublicKey(privateKey, true); return { privateKey, publicKey, signingKey: { curve, sign: (data) => k256Signer.sign(privateKey, data), publicKey: async () => publicKey, }, }; } const { generateKeyPair, importPrivateKey } = await import( '@pdsjs/core/crypto' ); const keyPair = await generateKeyPair(); return { privateKey: keyPair.privateKey, publicKey: keyPair.publicKey, signingKey: await importPrivateKey(keyPair.privateKey), };}
// === ARGUMENT PARSING ===
function parseArgs() { const args = process.argv.slice(2); const opts = { handle: null, pds: null, plcUrl: 'https://plc.directory', relayUrl: 'https://bsky.network', curve: 'secp256k1', };
for (let i = 0; i < args.length; i++) { if (args[i] === '--handle' && args[i + 1]) { opts.handle = args[++i]; } else if (args[i] === '--pds' && args[i + 1]) { opts.pds = args[++i]; } else if (args[i] === '--plc-url' && args[i + 1]) { opts.plcUrl = args[++i]; } else if (args[i] === '--relay-url' && args[i + 1]) { opts.relayUrl = args[++i]; } else if (args[i] === '--curve' && args[i + 1]) { opts.curve = args[++i]; } }
if (!opts.pds) { console.error( 'Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]', ); console.error(''); console.error('Options:'); console.error( ' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")', ); console.error( ' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted', ); console.error( ' --plc-url PLC directory URL (default: https://plc.directory)', ); console.error(' --relay-url Relay URL (default: https://bsky.network)'); console.error( ' --curve Signing key curve, secp256k1 or p256 (default: secp256k1)', ); process.exit(1); }
return opts;}
// === DID:KEY ENCODING ===
// Multicodec prefix for P-256 public key (0x1200)// === HASHING ===
async function sha256(data) { const hash = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(hash);}
// === PLC OPERATIONS ===
async function createGenesisOperation(opts) { const { didKey, recoveryDidKey, handle, pdsUrl, cryptoKey } = opts;
// Build full handle: subdomain.pds-hostname, or just pds-hostname if no // subdomain. hostname, not host — a handle must never carry a port. const pdsHost = new URL(pdsUrl).hostname; const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost;
const operation = { type: 'plc_operation', // Highest priority first. The recovery key can rewrite what the signing // key authorized; the reverse is refused. rotationKeys: recoveryDidKey ? [recoveryDidKey, didKey] : [didKey], verificationMethods: { atproto: didKey, }, alsoKnownAs: [`at://${fullHandle}`], services: { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: pdsUrl, }, }, prev: null, };
// Sign the operation const signed = await signPlcOperationObject(operation, cryptoKey);
return { operation: signed, fullHandle };}
async function deriveDidFromOperation(operation) { // DID is computed from the FULL operation INCLUDING the signature const encoded = cborEncodeDagCbor(operation); const hash = await sha256(encoded); // DID is base32 of first 15 bytes of hash (= 24 base32 chars) return `did:plc:${base32Encode(hash.slice(0, 15))}`;}
// === PLC DIRECTORY REGISTRATION ===
async function registerWithPlc(plcUrl, did, operation) { const url = `${plcUrl}/${encodeURIComponent(did)}`;
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(operation), });
if (!response.ok) { const text = await response.text(); throw new Error(`PLC registration failed: ${response.status} ${text}`); }
return true;}
// === PDS INITIALIZATION ===
async function initializePds(pdsUrl, did, privateKeyHex, handle, curve) { const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`;
// A server that starts with a configured password requires it on /init. const password = process.env.PDS_PASSWORD; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ did, privateKey: privateKeyHex, handle, curve, ...(password ? { password } : {}), }), });
if (!response.ok) { const text = await response.text(); throw new Error(`PDS initialization failed: ${response.status} ${text}`); }
return response.json();}
// === RELAY NOTIFICATION ===
async function notifyRelay(relayUrl, pdsHostname) { const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`;
const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ hostname: pdsHostname, }), });
// Relay might return 200 or 202, both are OK if (!response.ok && response.status !== 202) { const text = await response.text(); console.warn( ` Warning: Relay notification returned ${response.status}: ${text}`, ); return false; }
return true;}
// === CREDENTIALS OUTPUT ===
function saveCredentials(filename, credentials) { // Owner-readable only: these files hold private keys. writeFileSync(filename, JSON.stringify(credentials, null, 2), { mode: 0o600, });}
// === MAIN ===
async function main() { const opts = parseArgs();
console.log('PDS Federation Setup'); console.log('===================='); console.log(`PDS: ${opts.pds}`); console.log('');
// Step 1: Generate keypair if (opts.curve !== 'secp256k1' && opts.curve !== 'p256') { console.error('--curve must be secp256k1 or p256'); process.exit(1); } console.log(`Generating ${opts.curve} keypair...`); const keyPair = await generateAccountKey(opts.curve); const didKey = publicKeyToDidKey(keyPair.publicKey, opts.curve); console.log(` signing did:key: ${didKey}`);
// A second rotation key, listed first so it outranks the signing key. PLC // lets a higher-priority rotation key rewrite operations a lower one // signed, so this is what recovers the DID if the signing key is lost. // It never reaches the PDS: it signs nothing but rotations. const recoveryKey = await generateAccountKey(opts.curve); const recoveryDidKey = publicKeyToDidKey(recoveryKey.publicKey, opts.curve); console.log(` recovery did:key: ${recoveryDidKey}`); console.log('');
// Step 2: Create genesis operation console.log('Creating PLC genesis operation...'); const { operation, fullHandle } = await createGenesisOperation({ didKey, recoveryDidKey, handle: opts.handle, pdsUrl: opts.pds, cryptoKey: keyPair.signingKey, }); const did = await deriveDidFromOperation(operation); console.log(` DID: ${did}`); console.log(` Handle: ${fullHandle}`); console.log('');
// Step 3: Register with PLC directory console.log(`Registering with ${opts.plcUrl}...`); await registerWithPlc(opts.plcUrl, did, operation); console.log(' Registered successfully!'); console.log('');
// Step 4: Initialize PDS console.log(`Initializing PDS at ${opts.pds}...`); const privateKeyHex = bytesToHex(keyPair.privateKey); await initializePds(opts.pds, did, privateKeyHex, fullHandle, opts.curve); console.log(' PDS initialized!'); console.log('');
// Step 5: Notify relay const pdsHostname = new URL(opts.pds).hostname; console.log(`Notifying relay at ${opts.relayUrl}...`); const relayOk = await notifyRelay(opts.relayUrl, pdsHostname); if (relayOk) { console.log(' Relay notified!'); } console.log('');
// Step 6: Save credentials const credentials = { handle: fullHandle, did, curve: opts.curve, privateKeyHex: bytesToHex(keyPair.privateKey), didKey, recoveryDidKey, pdsUrl: opts.pds, createdAt: new Date().toISOString(), }; // The recovery key goes in its own file, so a copy of the credentials does // not carry the thing that recovers them. const recoveryFile = `./recovery-${fullHandle}.json`; saveCredentials(recoveryFile, { handle: fullHandle, did, curve: opts.curve, privateKeyHex: bytesToHex(recoveryKey.privateKey), didKey: recoveryDidKey, role: 'rotation key only, never given to the PDS', note: 'Recovers the DID if the signing key is lost. Keep it somewhere the signing key is not.', createdAt: new Date().toISOString(), });
const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`; saveCredentials(credentialsFile, credentials);
// Final output console.log('Setup Complete!'); console.log('==============='); console.log(`Handle: ${fullHandle}`); console.log(`DID: ${did}`); console.log(`PDS: ${opts.pds}`); console.log(''); console.log(`Credentials saved to: ${credentialsFile}`); console.log(`Recovery key saved to: ${recoveryFile}`); console.log('Keep this file safe - it contains your private key!');}
main().catch((err) => { console.error('Error:', err.message); process.exit(1);});