#!/usr/bin/env node /** * PDS Setup Script * * Registers a did:plc, initializes the PDS, and notifies the relay. * Zero dependencies - uses Node.js built-ins only. * * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev */ import { webcrypto } from 'crypto' // === 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.handle || !opts.pds) { console.error('Usage: node scripts/setup.js --handle --pds ') console.error('') console.error('Options:') console.error(' --handle Handle name (e.g., "alice")') console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') 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 } // === KEY GENERATION === async function generateP256Keypair() { const keyPair = await webcrypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'] ) // Export private key as raw 32 bytes const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey) const privateBytes = base64UrlDecode(privateJwk.d) // Export public key as uncompressed point (65 bytes) const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey) const publicBytes = new Uint8Array(publicRaw) // Compress public key to 33 bytes const compressedPublic = compressPublicKey(publicBytes) return { privateKey: privateBytes, publicKey: compressedPublic, cryptoKey: keyPair.privateKey } } function compressPublicKey(uncompressed) { // uncompressed is 65 bytes: 0x04 + x(32) + y(32) const x = uncompressed.slice(1, 33) const y = uncompressed.slice(33, 65) const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 const compressed = new Uint8Array(33) compressed[0] = prefix compressed.set(x, 1) return compressed } function base64UrlDecode(str) { const base64 = str.replace(/-/g, '+').replace(/_/g, '/') const binary = atob(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i) } return bytes } function bytesToHex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') } // === 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 } // === CBOR ENCODING (minimal for PLC operations) === function cborEncode(value) { const parts = [] function encode(val) { if (val === null) { parts.push(0xf6) } else if (typeof val === 'string') { const bytes = new TextEncoder().encode(val) encodeHead(3, bytes.length) parts.push(...bytes) } else if (typeof val === 'number') { if (Number.isInteger(val) && val >= 0) { encodeHead(0, val) } } else if (val instanceof Uint8Array) { encodeHead(2, val.length) parts.push(...val) } else if (Array.isArray(val)) { encodeHead(4, val.length) for (const item of val) encode(item) } else if (typeof val === 'object') { const keys = Object.keys(val).sort() encodeHead(5, keys.length) for (const key of keys) { encode(key) encode(val[key]) } } } function encodeHead(majorType, length) { const mt = majorType << 5 if (length < 24) { parts.push(mt | length) } else if (length < 256) { parts.push(mt | 24, length) } else if (length < 65536) { parts.push(mt | 25, length >> 8, length & 0xff) } } encode(value) return new Uint8Array(parts) } // === HASHING === async function sha256(data) { const hash = await webcrypto.subtle.digest('SHA-256', data) return new Uint8Array(hash) } // === PLC OPERATIONS === async function signPlcOperation(operation, privateKey) { // Encode operation without sig field const { sig, ...opWithoutSig } = operation const encoded = cborEncode(opWithoutSig) // Sign with P-256 const signature = await webcrypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, privateKey, encoded ) // Convert to low-S form and base64url encode const sigBytes = ensureLowS(new Uint8Array(signature)) return base64UrlEncode(sigBytes) } function ensureLowS(sig) { // P-256 order N const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') const halfN = N / 2n const r = sig.slice(0, 32) const s = sig.slice(32, 64) // Convert s to BigInt let sInt = BigInt('0x' + bytesToHex(s)) // If s > N/2, replace with N - s if (sInt > halfN) { sInt = N - sInt const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) const result = new Uint8Array(64) result.set(r) result.set(newS, 32) return result } return sig } function hexToBytes(hex) { const bytes = new Uint8Array(hex.length / 2) for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16) } return bytes } function base64UrlEncode(bytes) { const binary = String.fromCharCode(...bytes) return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } async function createGenesisOperation(opts) { const { didKey, handle, pdsUrl, cryptoKey } = opts // Build the full handle const pdsHost = new URL(pdsUrl).host const fullHandle = `${handle}.${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) { const { sig, ...opWithoutSig } = operation const encoded = cborEncode(opWithoutSig) const hash = await sha256(encoded) // DID is base32 of first 24 bytes of hash return 'did:plc:' + base32Encode(hash.slice(0, 24)) } function base32Encode(bytes) { const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' let result = '' let bits = 0 let value = 0 for (const byte of bytes) { value = (value << 8) | byte bits += 8 while (bits >= 5) { bits -= 5 result += alphabet[(value >> bits) & 31] } } if (bits > 0) { result += alphabet[(value << (5 - bits)) & 31] } return result } // === 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 } // === MAIN === async function main() { const opts = parseArgs() console.log('PDS Federation Setup') console.log('====================') console.log(`Handle: ${opts.handle}`) console.log(`PDS: ${opts.pds}`) console.log('') // Step 1: Generate keypair console.log('Generating P-256 keypair...') const keyPair = await generateP256Keypair() 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: keyPair.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('') // TODO: Initialize PDS // TODO: Notify relay } main().catch(err => { console.error('Error:', err.message) process.exit(1) })