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