#!/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, base64UrlEncode, bytesToHex, cborEncodeDagCbor, generateKeyPair, importPrivateKey, sign, } from '../src/pds.js'; // === ARGUMENT PARSING === function parseArgs() { const args = process.argv.slice(2); const opts = { handle: null, pds: null, plcUrl: 'https://plc.directory', relayUrl: 'https://bsky.network', }; 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]; } } if (!opts.pds) { console.error( 'Usage: node scripts/setup.js --pds [--handle ]', ); 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)'); process.exit(1); } return opts; } // === DID:KEY ENCODING === // Multicodec prefix for P-256 public key (0x1200) const P256_MULTICODEC = new Uint8Array([0x80, 0x24]); function publicKeyToDidKey(compressedPublicKey) { // did:key format: "did:key:" + multibase(base58btc) of multicodec + key const keyWithCodec = new Uint8Array( P256_MULTICODEC.length + compressedPublicKey.length, ); keyWithCodec.set(P256_MULTICODEC); keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length); return `did:key:z${base58btcEncode(keyWithCodec)}`; } function base58btcEncode(bytes) { const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; // Count leading zeros let zeros = 0; for (const b of bytes) { if (b === 0) zeros++; else break; } // Convert to base58 const digits = [0]; for (const byte of bytes) { let carry = byte; for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; } while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; } } // Convert to string let result = '1'.repeat(zeros); for (let i = digits.length - 1; i >= 0; i--) { result += ALPHABET[digits[i]]; } return result; } // === HASHING === async function sha256(data) { const hash = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(hash); } // === PLC OPERATIONS === async function signPlcOperation(operation, cryptoKey) { // Encode operation without sig field const { sig, ...opWithoutSig } = operation; const encoded = cborEncodeDagCbor(opWithoutSig); // Sign with P-256 (sign() handles low-S normalization) const signature = await sign(cryptoKey, encoded); return base64UrlEncode(signature); } async function createGenesisOperation(opts) { const { didKey, handle, pdsUrl, cryptoKey } = opts; // Build full handle: subdomain.pds-hostname, or just pds-hostname if no subdomain const pdsHost = new URL(pdsUrl).host; const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost; const operation = { type: 'plc_operation', rotationKeys: [didKey], verificationMethods: { atproto: didKey, }, alsoKnownAs: [`at://${fullHandle}`], services: { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: pdsUrl, }, }, prev: null, }; // Sign the operation operation.sig = await signPlcOperation(operation, cryptoKey); return { operation, 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) { const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ did, privateKey: privateKeyHex, handle, }), }); if (!response.ok) { const text = await response.text(); throw new Error(`PDS initialization failed: ${response.status} ${text}`); } return response.json(); } // === HANDLE REGISTRATION === async function registerHandle(pdsUrl, handle, did) { const url = `${pdsUrl}/register-handle`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ handle, did }), }); if (!response.ok) { const text = await response.text(); throw new Error(`Handle registration failed: ${response.status} ${text}`); } return true; } // === 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) { writeFileSync(filename, JSON.stringify(credentials, null, 2)); } // === 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 console.log('Generating P-256 keypair...'); const keyPair = await generateKeyPair(); const cryptoKey = await importPrivateKey(keyPair.privateKey); const didKey = publicKeyToDidKey(keyPair.publicKey); console.log(` did:key: ${didKey}`); console.log(''); // Step 2: Create genesis operation console.log('Creating PLC genesis operation...'); const { operation, fullHandle } = await createGenesisOperation({ didKey, handle: opts.handle, pdsUrl: opts.pds, cryptoKey, }); 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); console.log(' PDS initialized!'); console.log(''); // Step 4b: Register handle -> DID mapping (only for subdomain handles) if (opts.handle) { console.log(`Registering handle mapping...`); await registerHandle(opts.pds, opts.handle, did); console.log(` Handle ${opts.handle} -> ${did}`); console.log(''); } // Step 5: Notify relay const pdsHostname = new URL(opts.pds).host; 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, privateKeyHex: bytesToHex(keyPair.privateKey), didKey, pdsUrl: opts.pds, 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('Keep this file safe - it contains your private key!'); } main().catch((err) => { console.error('Error:', err.message); process.exit(1); });