this repo has no description
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 * Zero dependencies - uses Node.js built-ins only. 8 * 9 * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 10 */ 11 12import { webcrypto } from 'crypto' 13import { writeFileSync } from 'fs' 14 15// === ARGUMENT PARSING === 16 17function parseArgs() { 18 const args = process.argv.slice(2) 19 const opts = { 20 handle: null, 21 pds: null, 22 plcUrl: 'https://plc.directory', 23 relayUrl: 'https://bsky.network' 24 } 25 26 for (let i = 0; i < args.length; i++) { 27 if (args[i] === '--handle' && args[i + 1]) { 28 opts.handle = args[++i] 29 } else if (args[i] === '--pds' && args[i + 1]) { 30 opts.pds = args[++i] 31 } else if (args[i] === '--plc-url' && args[i + 1]) { 32 opts.plcUrl = args[++i] 33 } else if (args[i] === '--relay-url' && args[i + 1]) { 34 opts.relayUrl = args[++i] 35 } 36 } 37 38 if (!opts.handle || !opts.pds) { 39 console.error('Usage: node scripts/setup.js --handle <handle> --pds <pds-url>') 40 console.error('') 41 console.error('Options:') 42 console.error(' --handle Handle name (e.g., "alice")') 43 console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') 44 console.error(' --plc-url PLC directory URL (default: https://plc.directory)') 45 console.error(' --relay-url Relay URL (default: https://bsky.network)') 46 process.exit(1) 47 } 48 49 return opts 50} 51 52// === KEY GENERATION === 53 54async function generateP256Keypair() { 55 const keyPair = await webcrypto.subtle.generateKey( 56 { name: 'ECDSA', namedCurve: 'P-256' }, 57 true, 58 ['sign', 'verify'] 59 ) 60 61 // Export private key as raw 32 bytes 62 const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey) 63 const privateBytes = base64UrlDecode(privateJwk.d) 64 65 // Export public key as uncompressed point (65 bytes) 66 const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey) 67 const publicBytes = new Uint8Array(publicRaw) 68 69 // Compress public key to 33 bytes 70 const compressedPublic = compressPublicKey(publicBytes) 71 72 return { 73 privateKey: privateBytes, 74 publicKey: compressedPublic, 75 cryptoKey: keyPair.privateKey 76 } 77} 78 79function compressPublicKey(uncompressed) { 80 // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 81 const x = uncompressed.slice(1, 33) 82 const y = uncompressed.slice(33, 65) 83 const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 84 const compressed = new Uint8Array(33) 85 compressed[0] = prefix 86 compressed.set(x, 1) 87 return compressed 88} 89 90function base64UrlDecode(str) { 91 const base64 = str.replace(/-/g, '+').replace(/_/g, '/') 92 const binary = atob(base64) 93 const bytes = new Uint8Array(binary.length) 94 for (let i = 0; i < binary.length; i++) { 95 bytes[i] = binary.charCodeAt(i) 96 } 97 return bytes 98} 99 100function bytesToHex(bytes) { 101 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') 102} 103 104// === DID:KEY ENCODING === 105 106// Multicodec prefix for P-256 public key (0x1200) 107const P256_MULTICODEC = new Uint8Array([0x80, 0x24]) 108 109function publicKeyToDidKey(compressedPublicKey) { 110 // did:key format: "did:key:" + multibase(base58btc) of multicodec + key 111 const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length) 112 keyWithCodec.set(P256_MULTICODEC) 113 keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length) 114 115 return 'did:key:z' + base58btcEncode(keyWithCodec) 116} 117 118function base58btcEncode(bytes) { 119 const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 120 121 // Count leading zeros 122 let zeros = 0 123 for (const b of bytes) { 124 if (b === 0) zeros++ 125 else break 126 } 127 128 // Convert to base58 129 const digits = [0] 130 for (const byte of bytes) { 131 let carry = byte 132 for (let i = 0; i < digits.length; i++) { 133 carry += digits[i] << 8 134 digits[i] = carry % 58 135 carry = (carry / 58) | 0 136 } 137 while (carry > 0) { 138 digits.push(carry % 58) 139 carry = (carry / 58) | 0 140 } 141 } 142 143 // Convert to string 144 let result = '1'.repeat(zeros) 145 for (let i = digits.length - 1; i >= 0; i--) { 146 result += ALPHABET[digits[i]] 147 } 148 149 return result 150} 151 152// === CBOR ENCODING (minimal for PLC operations) === 153 154function cborEncode(value) { 155 const parts = [] 156 157 function encode(val) { 158 if (val === null) { 159 parts.push(0xf6) 160 } else if (typeof val === 'string') { 161 const bytes = new TextEncoder().encode(val) 162 encodeHead(3, bytes.length) 163 parts.push(...bytes) 164 } else if (typeof val === 'number') { 165 if (Number.isInteger(val) && val >= 0) { 166 encodeHead(0, val) 167 } 168 } else if (val instanceof Uint8Array) { 169 encodeHead(2, val.length) 170 parts.push(...val) 171 } else if (Array.isArray(val)) { 172 encodeHead(4, val.length) 173 for (const item of val) encode(item) 174 } else if (typeof val === 'object') { 175 const keys = Object.keys(val).sort() 176 encodeHead(5, keys.length) 177 for (const key of keys) { 178 encode(key) 179 encode(val[key]) 180 } 181 } 182 } 183 184 function encodeHead(majorType, length) { 185 const mt = majorType << 5 186 if (length < 24) { 187 parts.push(mt | length) 188 } else if (length < 256) { 189 parts.push(mt | 24, length) 190 } else if (length < 65536) { 191 parts.push(mt | 25, length >> 8, length & 0xff) 192 } 193 } 194 195 encode(value) 196 return new Uint8Array(parts) 197} 198 199// === HASHING === 200 201async function sha256(data) { 202 const hash = await webcrypto.subtle.digest('SHA-256', data) 203 return new Uint8Array(hash) 204} 205 206// === PLC OPERATIONS === 207 208async function signPlcOperation(operation, privateKey) { 209 // Encode operation without sig field 210 const { sig, ...opWithoutSig } = operation 211 const encoded = cborEncode(opWithoutSig) 212 213 // Sign with P-256 214 const signature = await webcrypto.subtle.sign( 215 { name: 'ECDSA', hash: 'SHA-256' }, 216 privateKey, 217 encoded 218 ) 219 220 // Convert to low-S form and base64url encode 221 const sigBytes = ensureLowS(new Uint8Array(signature)) 222 return base64UrlEncode(sigBytes) 223} 224 225function ensureLowS(sig) { 226 // P-256 order N 227 const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 228 const halfN = N / 2n 229 230 const r = sig.slice(0, 32) 231 const s = sig.slice(32, 64) 232 233 // Convert s to BigInt 234 let sInt = BigInt('0x' + bytesToHex(s)) 235 236 // If s > N/2, replace with N - s 237 if (sInt > halfN) { 238 sInt = N - sInt 239 const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) 240 const result = new Uint8Array(64) 241 result.set(r) 242 result.set(newS, 32) 243 return result 244 } 245 246 return sig 247} 248 249function hexToBytes(hex) { 250 const bytes = new Uint8Array(hex.length / 2) 251 for (let i = 0; i < hex.length; i += 2) { 252 bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 253 } 254 return bytes 255} 256 257function base64UrlEncode(bytes) { 258 const binary = String.fromCharCode(...bytes) 259 return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 260} 261 262async function createGenesisOperation(opts) { 263 const { didKey, handle, pdsUrl, cryptoKey } = opts 264 265 // Build the full handle 266 const pdsHost = new URL(pdsUrl).host 267 const fullHandle = `${handle}.${pdsHost}` 268 269 const operation = { 270 type: 'plc_operation', 271 rotationKeys: [didKey], 272 verificationMethods: { 273 atproto: didKey 274 }, 275 alsoKnownAs: [`at://${fullHandle}`], 276 services: { 277 atproto_pds: { 278 type: 'AtprotoPersonalDataServer', 279 endpoint: pdsUrl 280 } 281 }, 282 prev: null 283 } 284 285 // Sign the operation 286 operation.sig = await signPlcOperation(operation, cryptoKey) 287 288 return { operation, fullHandle } 289} 290 291async function deriveDidFromOperation(operation) { 292 const { sig, ...opWithoutSig } = operation 293 const encoded = cborEncode(opWithoutSig) 294 const hash = await sha256(encoded) 295 // DID is base32 of first 24 bytes of hash 296 return 'did:plc:' + base32Encode(hash.slice(0, 24)) 297} 298 299function base32Encode(bytes) { 300 const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 301 let result = '' 302 let bits = 0 303 let value = 0 304 305 for (const byte of bytes) { 306 value = (value << 8) | byte 307 bits += 8 308 while (bits >= 5) { 309 bits -= 5 310 result += alphabet[(value >> bits) & 31] 311 } 312 } 313 314 if (bits > 0) { 315 result += alphabet[(value << (5 - bits)) & 31] 316 } 317 318 return result 319} 320 321// === PLC DIRECTORY REGISTRATION === 322 323async function registerWithPlc(plcUrl, did, operation) { 324 const url = `${plcUrl}/${encodeURIComponent(did)}` 325 326 const response = await fetch(url, { 327 method: 'POST', 328 headers: { 329 'Content-Type': 'application/json' 330 }, 331 body: JSON.stringify(operation) 332 }) 333 334 if (!response.ok) { 335 const text = await response.text() 336 throw new Error(`PLC registration failed: ${response.status} ${text}`) 337 } 338 339 return true 340} 341 342// === PDS INITIALIZATION === 343 344async function initializePds(pdsUrl, did, privateKeyHex, handle) { 345 const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}` 346 347 const response = await fetch(url, { 348 method: 'POST', 349 headers: { 350 'Content-Type': 'application/json' 351 }, 352 body: JSON.stringify({ 353 did, 354 privateKey: privateKeyHex, 355 handle 356 }) 357 }) 358 359 if (!response.ok) { 360 const text = await response.text() 361 throw new Error(`PDS initialization failed: ${response.status} ${text}`) 362 } 363 364 return response.json() 365} 366 367// === RELAY NOTIFICATION === 368 369async function notifyRelay(relayUrl, pdsHostname) { 370 const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl` 371 372 const response = await fetch(url, { 373 method: 'POST', 374 headers: { 375 'Content-Type': 'application/json' 376 }, 377 body: JSON.stringify({ 378 hostname: pdsHostname 379 }) 380 }) 381 382 // Relay might return 200 or 202, both are OK 383 if (!response.ok && response.status !== 202) { 384 const text = await response.text() 385 console.warn(` Warning: Relay notification returned ${response.status}: ${text}`) 386 return false 387 } 388 389 return true 390} 391 392// === CREDENTIALS OUTPUT === 393 394function saveCredentials(filename, credentials) { 395 writeFileSync(filename, JSON.stringify(credentials, null, 2)) 396} 397 398// === MAIN === 399 400async function main() { 401 const opts = parseArgs() 402 403 console.log('PDS Federation Setup') 404 console.log('====================') 405 console.log(`Handle: ${opts.handle}`) 406 console.log(`PDS: ${opts.pds}`) 407 console.log('') 408 409 // Step 1: Generate keypair 410 console.log('Generating P-256 keypair...') 411 const keyPair = await generateP256Keypair() 412 const didKey = publicKeyToDidKey(keyPair.publicKey) 413 console.log(` did:key: ${didKey}`) 414 console.log('') 415 416 // Step 2: Create genesis operation 417 console.log('Creating PLC genesis operation...') 418 const { operation, fullHandle } = await createGenesisOperation({ 419 didKey, 420 handle: opts.handle, 421 pdsUrl: opts.pds, 422 cryptoKey: keyPair.cryptoKey 423 }) 424 const did = await deriveDidFromOperation(operation) 425 console.log(` DID: ${did}`) 426 console.log(` Handle: ${fullHandle}`) 427 console.log('') 428 429 // Step 3: Register with PLC directory 430 console.log(`Registering with ${opts.plcUrl}...`) 431 await registerWithPlc(opts.plcUrl, did, operation) 432 console.log(' Registered successfully!') 433 console.log('') 434 435 // Step 4: Initialize PDS 436 console.log(`Initializing PDS at ${opts.pds}...`) 437 const privateKeyHex = bytesToHex(keyPair.privateKey) 438 await initializePds(opts.pds, did, privateKeyHex, fullHandle) 439 console.log(' PDS initialized!') 440 console.log('') 441 442 // Step 5: Notify relay 443 const pdsHostname = new URL(opts.pds).host 444 console.log(`Notifying relay at ${opts.relayUrl}...`) 445 const relayOk = await notifyRelay(opts.relayUrl, pdsHostname) 446 if (relayOk) { 447 console.log(' Relay notified!') 448 } 449 console.log('') 450 451 // Step 6: Save credentials 452 const credentials = { 453 handle: fullHandle, 454 did, 455 privateKeyHex: bytesToHex(keyPair.privateKey), 456 didKey, 457 pdsUrl: opts.pds, 458 createdAt: new Date().toISOString() 459 } 460 461 const credentialsFile = `./credentials-${opts.handle}.json` 462 saveCredentials(credentialsFile, credentials) 463 464 // Final output 465 console.log('Setup Complete!') 466 console.log('===============') 467 console.log(`Handle: ${fullHandle}`) 468 console.log(`DID: ${did}`) 469 console.log(`PDS: ${opts.pds}`) 470 console.log('') 471 console.log(`Credentials saved to: ${credentialsFile}`) 472 console.log('Keep this file safe - it contains your private key!') 473} 474 475main().catch(err => { 476 console.error('Error:', err.message) 477 process.exit(1) 478})