this repo has no description
at main 9.5 kB view raw
1#!/usr/bin/env node 2 3/** 4 * PDS Setup Script 5 * 6 * Registers a did:plc, initializes the PDS, and notifies the relay. 7 * 8 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 9 */ 10 11import { writeFileSync } from 'node:fs'; 12import { 13 base32Encode, 14 base64UrlEncode, 15 bytesToHex, 16 cborEncodeDagCbor, 17 generateKeyPair, 18 importPrivateKey, 19 sign, 20} from '../src/pds.js'; 21 22// === ARGUMENT PARSING === 23 24function parseArgs() { 25 const args = process.argv.slice(2); 26 const opts = { 27 handle: null, 28 pds: null, 29 plcUrl: 'https://plc.directory', 30 relayUrl: 'https://bsky.network', 31 }; 32 33 for (let i = 0; i < args.length; i++) { 34 if (args[i] === '--handle' && args[i + 1]) { 35 opts.handle = args[++i]; 36 } else if (args[i] === '--pds' && args[i + 1]) { 37 opts.pds = args[++i]; 38 } else if (args[i] === '--plc-url' && args[i + 1]) { 39 opts.plcUrl = args[++i]; 40 } else if (args[i] === '--relay-url' && args[i + 1]) { 41 opts.relayUrl = args[++i]; 42 } 43 } 44 45 if (!opts.pds) { 46 console.error( 47 'Usage: node scripts/setup.js --pds <pds-url> [--handle <subdomain>]', 48 ); 49 console.error(''); 50 console.error('Options:'); 51 console.error( 52 ' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")', 53 ); 54 console.error( 55 ' --handle Subdomain handle (e.g., "alice") - optional, uses bare hostname if omitted', 56 ); 57 console.error( 58 ' --plc-url PLC directory URL (default: https://plc.directory)', 59 ); 60 console.error(' --relay-url Relay URL (default: https://bsky.network)'); 61 process.exit(1); 62 } 63 64 return opts; 65} 66 67// === DID:KEY ENCODING === 68 69// Multicodec prefix for P-256 public key (0x1200) 70const P256_MULTICODEC = new Uint8Array([0x80, 0x24]); 71 72function publicKeyToDidKey(compressedPublicKey) { 73 // did:key format: "did:key:" + multibase(base58btc) of multicodec + key 74 const keyWithCodec = new Uint8Array( 75 P256_MULTICODEC.length + compressedPublicKey.length, 76 ); 77 keyWithCodec.set(P256_MULTICODEC); 78 keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length); 79 80 return `did:key:z${base58btcEncode(keyWithCodec)}`; 81} 82 83function base58btcEncode(bytes) { 84 const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 85 86 // Count leading zeros 87 let zeros = 0; 88 for (const b of bytes) { 89 if (b === 0) zeros++; 90 else break; 91 } 92 93 // Convert to base58 94 const digits = [0]; 95 for (const byte of bytes) { 96 let carry = byte; 97 for (let i = 0; i < digits.length; i++) { 98 carry += digits[i] << 8; 99 digits[i] = carry % 58; 100 carry = (carry / 58) | 0; 101 } 102 while (carry > 0) { 103 digits.push(carry % 58); 104 carry = (carry / 58) | 0; 105 } 106 } 107 108 // Convert to string 109 let result = '1'.repeat(zeros); 110 for (let i = digits.length - 1; i >= 0; i--) { 111 result += ALPHABET[digits[i]]; 112 } 113 114 return result; 115} 116 117// === HASHING === 118 119async function sha256(data) { 120 const hash = await crypto.subtle.digest('SHA-256', data); 121 return new Uint8Array(hash); 122} 123 124// === PLC OPERATIONS === 125 126async function signPlcOperation(operation, cryptoKey) { 127 // Encode operation without sig field 128 const { sig, ...opWithoutSig } = operation; 129 const encoded = cborEncodeDagCbor(opWithoutSig); 130 131 // Sign with P-256 (sign() handles low-S normalization) 132 const signature = await sign(cryptoKey, encoded); 133 return base64UrlEncode(signature); 134} 135 136async function createGenesisOperation(opts) { 137 const { didKey, handle, pdsUrl, cryptoKey } = opts; 138 139 // Build full handle: subdomain.pds-hostname, or just pds-hostname if no subdomain 140 const pdsHost = new URL(pdsUrl).host; 141 const fullHandle = handle ? `${handle}.${pdsHost}` : pdsHost; 142 143 const operation = { 144 type: 'plc_operation', 145 rotationKeys: [didKey], 146 verificationMethods: { 147 atproto: didKey, 148 }, 149 alsoKnownAs: [`at://${fullHandle}`], 150 services: { 151 atproto_pds: { 152 type: 'AtprotoPersonalDataServer', 153 endpoint: pdsUrl, 154 }, 155 }, 156 prev: null, 157 }; 158 159 // Sign the operation 160 operation.sig = await signPlcOperation(operation, cryptoKey); 161 162 return { operation, fullHandle }; 163} 164 165async function deriveDidFromOperation(operation) { 166 // DID is computed from the FULL operation INCLUDING the signature 167 const encoded = cborEncodeDagCbor(operation); 168 const hash = await sha256(encoded); 169 // DID is base32 of first 15 bytes of hash (= 24 base32 chars) 170 return `did:plc:${base32Encode(hash.slice(0, 15))}`; 171} 172 173// === PLC DIRECTORY REGISTRATION === 174 175async function registerWithPlc(plcUrl, did, operation) { 176 const url = `${plcUrl}/${encodeURIComponent(did)}`; 177 178 const response = await fetch(url, { 179 method: 'POST', 180 headers: { 181 'Content-Type': 'application/json', 182 }, 183 body: JSON.stringify(operation), 184 }); 185 186 if (!response.ok) { 187 const text = await response.text(); 188 throw new Error(`PLC registration failed: ${response.status} ${text}`); 189 } 190 191 return true; 192} 193 194// === PDS INITIALIZATION === 195 196async function initializePds(pdsUrl, did, privateKeyHex, handle) { 197 const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}`; 198 199 const response = await fetch(url, { 200 method: 'POST', 201 headers: { 202 'Content-Type': 'application/json', 203 }, 204 body: JSON.stringify({ 205 did, 206 privateKey: privateKeyHex, 207 handle, 208 }), 209 }); 210 211 if (!response.ok) { 212 const text = await response.text(); 213 throw new Error(`PDS initialization failed: ${response.status} ${text}`); 214 } 215 216 return response.json(); 217} 218 219// === HANDLE REGISTRATION === 220 221async function registerHandle(pdsUrl, handle, did) { 222 const url = `${pdsUrl}/register-handle`; 223 224 const response = await fetch(url, { 225 method: 'POST', 226 headers: { 227 'Content-Type': 'application/json', 228 }, 229 body: JSON.stringify({ handle, did }), 230 }); 231 232 if (!response.ok) { 233 const text = await response.text(); 234 throw new Error(`Handle registration failed: ${response.status} ${text}`); 235 } 236 237 return true; 238} 239 240// === RELAY NOTIFICATION === 241 242async function notifyRelay(relayUrl, pdsHostname) { 243 const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl`; 244 245 const response = await fetch(url, { 246 method: 'POST', 247 headers: { 248 'Content-Type': 'application/json', 249 }, 250 body: JSON.stringify({ 251 hostname: pdsHostname, 252 }), 253 }); 254 255 // Relay might return 200 or 202, both are OK 256 if (!response.ok && response.status !== 202) { 257 const text = await response.text(); 258 console.warn( 259 ` Warning: Relay notification returned ${response.status}: ${text}`, 260 ); 261 return false; 262 } 263 264 return true; 265} 266 267// === CREDENTIALS OUTPUT === 268 269function saveCredentials(filename, credentials) { 270 writeFileSync(filename, JSON.stringify(credentials, null, 2)); 271} 272 273// === MAIN === 274 275async function main() { 276 const opts = parseArgs(); 277 278 console.log('PDS Federation Setup'); 279 console.log('===================='); 280 console.log(`PDS: ${opts.pds}`); 281 console.log(''); 282 283 // Step 1: Generate keypair 284 console.log('Generating P-256 keypair...'); 285 const keyPair = await generateKeyPair(); 286 const cryptoKey = await importPrivateKey(keyPair.privateKey); 287 const didKey = publicKeyToDidKey(keyPair.publicKey); 288 console.log(` did:key: ${didKey}`); 289 console.log(''); 290 291 // Step 2: Create genesis operation 292 console.log('Creating PLC genesis operation...'); 293 const { operation, fullHandle } = await createGenesisOperation({ 294 didKey, 295 handle: opts.handle, 296 pdsUrl: opts.pds, 297 cryptoKey, 298 }); 299 const did = await deriveDidFromOperation(operation); 300 console.log(` DID: ${did}`); 301 console.log(` Handle: ${fullHandle}`); 302 console.log(''); 303 304 // Step 3: Register with PLC directory 305 console.log(`Registering with ${opts.plcUrl}...`); 306 await registerWithPlc(opts.plcUrl, did, operation); 307 console.log(' Registered successfully!'); 308 console.log(''); 309 310 // Step 4: Initialize PDS 311 console.log(`Initializing PDS at ${opts.pds}...`); 312 const privateKeyHex = bytesToHex(keyPair.privateKey); 313 await initializePds(opts.pds, did, privateKeyHex, fullHandle); 314 console.log(' PDS initialized!'); 315 console.log(''); 316 317 // Step 4b: Register handle -> DID mapping (only for subdomain handles) 318 if (opts.handle) { 319 console.log(`Registering handle mapping...`); 320 await registerHandle(opts.pds, opts.handle, did); 321 console.log(` Handle ${opts.handle} -> ${did}`); 322 console.log(''); 323 } 324 325 // Step 5: Notify relay 326 const pdsHostname = new URL(opts.pds).host; 327 console.log(`Notifying relay at ${opts.relayUrl}...`); 328 const relayOk = await notifyRelay(opts.relayUrl, pdsHostname); 329 if (relayOk) { 330 console.log(' Relay notified!'); 331 } 332 console.log(''); 333 334 // Step 6: Save credentials 335 const credentials = { 336 handle: fullHandle, 337 did, 338 privateKeyHex: bytesToHex(keyPair.privateKey), 339 didKey, 340 pdsUrl: opts.pds, 341 createdAt: new Date().toISOString(), 342 }; 343 344 const credentialsFile = `./credentials-${opts.handle || new URL(opts.pds).host}.json`; 345 saveCredentials(credentialsFile, credentials); 346 347 // Final output 348 console.log('Setup Complete!'); 349 console.log('==============='); 350 console.log(`Handle: ${fullHandle}`); 351 console.log(`DID: ${did}`); 352 console.log(`PDS: ${opts.pds}`); 353 console.log(''); 354 console.log(`Credentials saved to: ${credentialsFile}`); 355 console.log('Keep this file safe - it contains your private key!'); 356} 357 358main().catch((err) => { 359 console.error('Error:', err.message); 360 process.exit(1); 361});