A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
42
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 361 lines 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 generateKeyPair, 17 importPrivateKey, 18 sign, 19} from '@pds/core/crypto'; 20import { cborEncodeDagCbor } from '@pds/core/repo'; 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});