this repo has no description

docs: add federation support implementation plan

Covers handle resolution, DID:PLC registration, and relay notification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+1036
docs
+1036
docs/plans/2026-01-05-federation-support.md
··· 1 + # Federation Support Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Enable the Cloudflare PDS to federate with Bluesky by adding handle resolution, DID:PLC registration, and relay notification. 6 + 7 + **Architecture:** Add `/.well-known/atproto-did` endpoint to resolve handles to DIDs. Create a zero-dependency Node.js setup script that generates P-256 keys, registers a did:plc with plc.directory, initializes the PDS, and notifies the relay. 8 + 9 + **Tech Stack:** Cloudflare Workers (existing), Node.js crypto (setup script), plc.directory API, bsky.network relay 10 + 11 + --- 12 + 13 + ## Task 1: Add Handle Storage to PDS 14 + 15 + **Files:** 16 + - Modify: `src/pds.js` 17 + 18 + **Step 1: Update /init endpoint to accept handle** 19 + 20 + In `src/pds.js`, modify the `/init` endpoint to also store the handle: 21 + 22 + ```javascript 23 + if (url.pathname === '/init') { 24 + const body = await request.json() 25 + if (!body.did || !body.privateKey) { 26 + return Response.json({ error: 'missing did or privateKey' }, { status: 400 }) 27 + } 28 + await this.initIdentity(body.did, body.privateKey, body.handle || null) 29 + return Response.json({ ok: true, did: body.did, handle: body.handle || null }) 30 + } 31 + ``` 32 + 33 + **Step 2: Update initIdentity method** 34 + 35 + Modify the `initIdentity` method to store handle: 36 + 37 + ```javascript 38 + async initIdentity(did, privateKeyHex, handle = null) { 39 + await this.state.storage.put('did', did) 40 + await this.state.storage.put('privateKey', privateKeyHex) 41 + if (handle) { 42 + await this.state.storage.put('handle', handle) 43 + } 44 + } 45 + ``` 46 + 47 + **Step 3: Add getHandle method** 48 + 49 + Add after `getDid()`: 50 + 51 + ```javascript 52 + async getHandle() { 53 + return this.state.storage.get('handle') 54 + } 55 + ``` 56 + 57 + **Step 4: Test locally** 58 + 59 + Run: `npx wrangler dev --port 8788` 60 + 61 + ```bash 62 + curl -X POST "http://localhost:8788/init?did=did:plc:test" \ 63 + -H "Content-Type: application/json" \ 64 + -d '{"did":"did:plc:test","privateKey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","handle":"alice.example.com"}' 65 + ``` 66 + 67 + Expected: `{"ok":true,"did":"did:plc:test","handle":"alice.example.com"}` 68 + 69 + **Step 5: Commit** 70 + 71 + ```bash 72 + git add src/pds.js 73 + git commit -m "feat: add handle storage to identity" 74 + ``` 75 + 76 + --- 77 + 78 + ## Task 2: Add Handle Resolution Endpoint 79 + 80 + **Files:** 81 + - Modify: `src/pds.js` 82 + 83 + **Step 1: Add /.well-known/atproto-did endpoint** 84 + 85 + Add this at the START of the `fetch()` method, BEFORE the `if (url.pathname === '/init')` check. This endpoint should not require the `?did=` query parameter: 86 + 87 + ```javascript 88 + async fetch(request) { 89 + const url = new URL(request.url) 90 + 91 + // Handle resolution - doesn't require ?did= param 92 + if (url.pathname === '/.well-known/atproto-did') { 93 + const did = await this.getDid() 94 + if (!did) { 95 + return new Response('User not found', { status: 404 }) 96 + } 97 + return new Response(did, { 98 + headers: { 'Content-Type': 'text/plain' } 99 + }) 100 + } 101 + 102 + // ... rest of existing fetch code 103 + ``` 104 + 105 + **Step 2: Update the default export to handle /.well-known without ?did=** 106 + 107 + The main router needs to route `/.well-known/atproto-did` requests differently. Modify the default export: 108 + 109 + ```javascript 110 + export default { 111 + async fetch(request, env) { 112 + const url = new URL(request.url) 113 + 114 + // For /.well-known/atproto-did, extract DID from subdomain 115 + // e.g., alice.atproto-pds.chad-53c.workers.dev -> look up "alice" 116 + if (url.pathname === '/.well-known/atproto-did') { 117 + const host = request.headers.get('Host') || '' 118 + // For now, use the first Durable Object (single-user PDS) 119 + // Extract handle from subdomain if present 120 + const did = url.searchParams.get('did') || 'default' 121 + const id = env.PDS.idFromName(did) 122 + const pds = env.PDS.get(id) 123 + return pds.fetch(request) 124 + } 125 + 126 + const did = url.searchParams.get('did') 127 + if (!did) { 128 + return new Response('missing did param', { status: 400 }) 129 + } 130 + 131 + const id = env.PDS.idFromName(did) 132 + const pds = env.PDS.get(id) 133 + return pds.fetch(request) 134 + } 135 + } 136 + ``` 137 + 138 + **Step 3: Test locally** 139 + 140 + Run: `npx wrangler dev --port 8788` 141 + 142 + First init: 143 + ```bash 144 + curl -X POST "http://localhost:8788/init?did=did:plc:testhandle" \ 145 + -H "Content-Type: application/json" \ 146 + -d '{"did":"did:plc:testhandle","privateKey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","handle":"alice"}' 147 + ``` 148 + 149 + Then test resolution: 150 + ```bash 151 + curl "http://localhost:8788/.well-known/atproto-did?did=did:plc:testhandle" 152 + ``` 153 + 154 + Expected: `did:plc:testhandle` 155 + 156 + **Step 4: Commit** 157 + 158 + ```bash 159 + git add src/pds.js 160 + git commit -m "feat: add handle resolution endpoint" 161 + ``` 162 + 163 + --- 164 + 165 + ## Task 3: Create Setup Script Skeleton 166 + 167 + **Files:** 168 + - Create: `scripts/setup.js` 169 + - Modify: `package.json` 170 + 171 + **Step 1: Create scripts directory and setup.js** 172 + 173 + Create `scripts/setup.js`: 174 + 175 + ```javascript 176 + #!/usr/bin/env node 177 + 178 + /** 179 + * PDS Setup Script 180 + * 181 + * Registers a did:plc, initializes the PDS, and notifies the relay. 182 + * Zero dependencies - uses Node.js built-ins only. 183 + * 184 + * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev 185 + */ 186 + 187 + import { webcrypto } from 'crypto' 188 + 189 + // === ARGUMENT PARSING === 190 + 191 + function parseArgs() { 192 + const args = process.argv.slice(2) 193 + const opts = { 194 + handle: null, 195 + pds: null, 196 + plcUrl: 'https://plc.directory', 197 + relayUrl: 'https://bsky.network' 198 + } 199 + 200 + for (let i = 0; i < args.length; i++) { 201 + if (args[i] === '--handle' && args[i + 1]) { 202 + opts.handle = args[++i] 203 + } else if (args[i] === '--pds' && args[i + 1]) { 204 + opts.pds = args[++i] 205 + } else if (args[i] === '--plc-url' && args[i + 1]) { 206 + opts.plcUrl = args[++i] 207 + } else if (args[i] === '--relay-url' && args[i + 1]) { 208 + opts.relayUrl = args[++i] 209 + } 210 + } 211 + 212 + if (!opts.handle || !opts.pds) { 213 + console.error('Usage: node scripts/setup.js --handle <handle> --pds <pds-url>') 214 + console.error('') 215 + console.error('Options:') 216 + console.error(' --handle Handle name (e.g., "alice")') 217 + console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') 218 + console.error(' --plc-url PLC directory URL (default: https://plc.directory)') 219 + console.error(' --relay-url Relay URL (default: https://bsky.network)') 220 + process.exit(1) 221 + } 222 + 223 + return opts 224 + } 225 + 226 + // === MAIN === 227 + 228 + async function main() { 229 + const opts = parseArgs() 230 + 231 + console.log('PDS Federation Setup') 232 + console.log('====================') 233 + console.log(`Handle: ${opts.handle}`) 234 + console.log(`PDS: ${opts.pds}`) 235 + console.log(`PLC: ${opts.plcUrl}`) 236 + console.log(`Relay: ${opts.relayUrl}`) 237 + console.log('') 238 + 239 + // TODO: Implement in subsequent tasks 240 + console.log('TODO: Generate keypair') 241 + console.log('TODO: Register DID:PLC') 242 + console.log('TODO: Initialize PDS') 243 + console.log('TODO: Notify relay') 244 + } 245 + 246 + main().catch(err => { 247 + console.error('Error:', err.message) 248 + process.exit(1) 249 + }) 250 + ``` 251 + 252 + **Step 2: Add npm script** 253 + 254 + Modify `package.json`: 255 + 256 + ```json 257 + { 258 + "name": "cloudflare-pds", 259 + "version": "0.1.0", 260 + "private": true, 261 + "type": "module", 262 + "scripts": { 263 + "dev": "wrangler dev", 264 + "deploy": "wrangler deploy", 265 + "test": "node --test test/*.test.js", 266 + "setup": "node scripts/setup.js" 267 + } 268 + } 269 + ``` 270 + 271 + **Step 3: Test the skeleton** 272 + 273 + Run: `node scripts/setup.js --handle alice --pds https://example.com` 274 + 275 + Expected: 276 + ``` 277 + PDS Federation Setup 278 + ==================== 279 + Handle: alice 280 + PDS: https://example.com 281 + ... 282 + ``` 283 + 284 + **Step 4: Commit** 285 + 286 + ```bash 287 + git add scripts/setup.js package.json 288 + git commit -m "feat: add setup script skeleton" 289 + ``` 290 + 291 + --- 292 + 293 + ## Task 4: Add P-256 Key Generation 294 + 295 + **Files:** 296 + - Modify: `scripts/setup.js` 297 + 298 + **Step 1: Add key generation utilities** 299 + 300 + Add after the argument parsing section: 301 + 302 + ```javascript 303 + // === KEY GENERATION === 304 + 305 + async function generateP256Keypair() { 306 + const keyPair = await webcrypto.subtle.generateKey( 307 + { name: 'ECDSA', namedCurve: 'P-256' }, 308 + true, 309 + ['sign', 'verify'] 310 + ) 311 + 312 + // Export private key as raw 32 bytes 313 + const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey) 314 + const privateBytes = base64UrlDecode(privateJwk.d) 315 + 316 + // Export public key as uncompressed point (65 bytes) 317 + const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey) 318 + const publicBytes = new Uint8Array(publicRaw) 319 + 320 + // Compress public key to 33 bytes 321 + const compressedPublic = compressPublicKey(publicBytes) 322 + 323 + return { 324 + privateKey: privateBytes, 325 + publicKey: compressedPublic, 326 + cryptoKey: keyPair.privateKey 327 + } 328 + } 329 + 330 + function compressPublicKey(uncompressed) { 331 + // uncompressed is 65 bytes: 0x04 + x(32) + y(32) 332 + const x = uncompressed.slice(1, 33) 333 + const y = uncompressed.slice(33, 65) 334 + const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 335 + const compressed = new Uint8Array(33) 336 + compressed[0] = prefix 337 + compressed.set(x, 1) 338 + return compressed 339 + } 340 + 341 + function base64UrlDecode(str) { 342 + const base64 = str.replace(/-/g, '+').replace(/_/g, '/') 343 + const binary = atob(base64) 344 + const bytes = new Uint8Array(binary.length) 345 + for (let i = 0; i < binary.length; i++) { 346 + bytes[i] = binary.charCodeAt(i) 347 + } 348 + return bytes 349 + } 350 + 351 + function bytesToHex(bytes) { 352 + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') 353 + } 354 + ``` 355 + 356 + **Step 2: Add did:key encoding for P-256** 357 + 358 + ```javascript 359 + // === DID:KEY ENCODING === 360 + 361 + // Multicodec prefix for P-256 public key (0x1200) 362 + const P256_MULTICODEC = new Uint8Array([0x80, 0x24]) 363 + 364 + function publicKeyToDidKey(compressedPublicKey) { 365 + // did:key format: "did:key:" + multibase(base58btc) of multicodec + key 366 + const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length) 367 + keyWithCodec.set(P256_MULTICODEC) 368 + keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length) 369 + 370 + return 'did:key:z' + base58btcEncode(keyWithCodec) 371 + } 372 + 373 + function base58btcEncode(bytes) { 374 + const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 375 + 376 + // Count leading zeros 377 + let zeros = 0 378 + for (const b of bytes) { 379 + if (b === 0) zeros++ 380 + else break 381 + } 382 + 383 + // Convert to base58 384 + const digits = [0] 385 + for (const byte of bytes) { 386 + let carry = byte 387 + for (let i = 0; i < digits.length; i++) { 388 + carry += digits[i] << 8 389 + digits[i] = carry % 58 390 + carry = (carry / 58) | 0 391 + } 392 + while (carry > 0) { 393 + digits.push(carry % 58) 394 + carry = (carry / 58) | 0 395 + } 396 + } 397 + 398 + // Convert to string 399 + let result = '1'.repeat(zeros) 400 + for (let i = digits.length - 1; i >= 0; i--) { 401 + result += ALPHABET[digits[i]] 402 + } 403 + 404 + return result 405 + } 406 + ``` 407 + 408 + **Step 3: Update main() to generate and display keys** 409 + 410 + ```javascript 411 + async function main() { 412 + const opts = parseArgs() 413 + 414 + console.log('PDS Federation Setup') 415 + console.log('====================') 416 + console.log(`Handle: ${opts.handle}`) 417 + console.log(`PDS: ${opts.pds}`) 418 + console.log('') 419 + 420 + // Step 1: Generate keypair 421 + console.log('Generating P-256 keypair...') 422 + const keyPair = await generateP256Keypair() 423 + const didKey = publicKeyToDidKey(keyPair.publicKey) 424 + console.log(` did:key: ${didKey}`) 425 + console.log(` Private key: ${bytesToHex(keyPair.privateKey)}`) 426 + console.log('') 427 + 428 + // TODO: Register DID:PLC 429 + // TODO: Initialize PDS 430 + // TODO: Notify relay 431 + } 432 + ``` 433 + 434 + **Step 4: Test key generation** 435 + 436 + Run: `node scripts/setup.js --handle alice --pds https://example.com` 437 + 438 + Expected: 439 + ``` 440 + Generating P-256 keypair... 441 + did:key: zDnae... 442 + Private key: abcd1234... 443 + ``` 444 + 445 + **Step 5: Commit** 446 + 447 + ```bash 448 + git add scripts/setup.js 449 + git commit -m "feat: add P-256 key generation to setup script" 450 + ``` 451 + 452 + --- 453 + 454 + ## Task 5: Add DID:PLC Operation Signing 455 + 456 + **Files:** 457 + - Modify: `scripts/setup.js` 458 + 459 + **Step 1: Add CBOR encoding (minimal, for PLC operations)** 460 + 461 + ```javascript 462 + // === CBOR ENCODING (minimal for PLC operations) === 463 + 464 + function cborEncode(value) { 465 + const parts = [] 466 + 467 + function encode(val) { 468 + if (val === null) { 469 + parts.push(0xf6) 470 + } else if (typeof val === 'string') { 471 + const bytes = new TextEncoder().encode(val) 472 + encodeHead(3, bytes.length) 473 + parts.push(...bytes) 474 + } else if (typeof val === 'number') { 475 + if (Number.isInteger(val) && val >= 0) { 476 + encodeHead(0, val) 477 + } 478 + } else if (val instanceof Uint8Array) { 479 + encodeHead(2, val.length) 480 + parts.push(...val) 481 + } else if (Array.isArray(val)) { 482 + encodeHead(4, val.length) 483 + for (const item of val) encode(item) 484 + } else if (typeof val === 'object') { 485 + const keys = Object.keys(val).sort() 486 + encodeHead(5, keys.length) 487 + for (const key of keys) { 488 + encode(key) 489 + encode(val[key]) 490 + } 491 + } 492 + } 493 + 494 + function encodeHead(majorType, length) { 495 + const mt = majorType << 5 496 + if (length < 24) { 497 + parts.push(mt | length) 498 + } else if (length < 256) { 499 + parts.push(mt | 24, length) 500 + } else if (length < 65536) { 501 + parts.push(mt | 25, length >> 8, length & 0xff) 502 + } 503 + } 504 + 505 + encode(value) 506 + return new Uint8Array(parts) 507 + } 508 + ``` 509 + 510 + **Step 2: Add SHA-256 hashing** 511 + 512 + ```javascript 513 + // === HASHING === 514 + 515 + async function sha256(data) { 516 + const hash = await webcrypto.subtle.digest('SHA-256', data) 517 + return new Uint8Array(hash) 518 + } 519 + ``` 520 + 521 + **Step 3: Add PLC operation signing** 522 + 523 + ```javascript 524 + // === PLC OPERATIONS === 525 + 526 + async function signPlcOperation(operation, privateKey) { 527 + // Encode operation without sig field 528 + const { sig, ...opWithoutSig } = operation 529 + const encoded = cborEncode(opWithoutSig) 530 + 531 + // Sign with P-256 532 + const signature = await webcrypto.subtle.sign( 533 + { name: 'ECDSA', hash: 'SHA-256' }, 534 + privateKey, 535 + encoded 536 + ) 537 + 538 + // Convert to low-S form and base64url encode 539 + const sigBytes = ensureLowS(new Uint8Array(signature)) 540 + return base64UrlEncode(sigBytes) 541 + } 542 + 543 + function ensureLowS(sig) { 544 + // P-256 order N 545 + const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') 546 + const halfN = N / 2n 547 + 548 + const r = sig.slice(0, 32) 549 + const s = sig.slice(32, 64) 550 + 551 + // Convert s to BigInt 552 + let sInt = BigInt('0x' + bytesToHex(s)) 553 + 554 + // If s > N/2, replace with N - s 555 + if (sInt > halfN) { 556 + sInt = N - sInt 557 + const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) 558 + const result = new Uint8Array(64) 559 + result.set(r) 560 + result.set(newS, 32) 561 + return result 562 + } 563 + 564 + return sig 565 + } 566 + 567 + function hexToBytes(hex) { 568 + const bytes = new Uint8Array(hex.length / 2) 569 + for (let i = 0; i < hex.length; i += 2) { 570 + bytes[i / 2] = parseInt(hex.substr(i, 2), 16) 571 + } 572 + return bytes 573 + } 574 + 575 + function base64UrlEncode(bytes) { 576 + const binary = String.fromCharCode(...bytes) 577 + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 578 + } 579 + 580 + async function createGenesisOperation(opts) { 581 + const { didKey, handle, pdsUrl, cryptoKey } = opts 582 + 583 + // Build the full handle 584 + const pdsHost = new URL(pdsUrl).host 585 + const fullHandle = `${handle}.${pdsHost}` 586 + 587 + const operation = { 588 + type: 'plc_operation', 589 + rotationKeys: [didKey], 590 + verificationMethods: { 591 + atproto: didKey 592 + }, 593 + alsoKnownAs: [`at://${fullHandle}`], 594 + services: { 595 + atproto_pds: { 596 + type: 'AtprotoPersonalDataServer', 597 + endpoint: pdsUrl 598 + } 599 + }, 600 + prev: null 601 + } 602 + 603 + // Sign the operation 604 + operation.sig = await signPlcOperation(operation, cryptoKey) 605 + 606 + return { operation, fullHandle } 607 + } 608 + 609 + async function deriveDidFromOperation(operation) { 610 + const { sig, ...opWithoutSig } = operation 611 + const encoded = cborEncode(opWithoutSig) 612 + const hash = await sha256(encoded) 613 + // DID is base32 of first 24 bytes of hash 614 + return 'did:plc:' + base32Encode(hash.slice(0, 24)) 615 + } 616 + 617 + function base32Encode(bytes) { 618 + const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' 619 + let result = '' 620 + let bits = 0 621 + let value = 0 622 + 623 + for (const byte of bytes) { 624 + value = (value << 8) | byte 625 + bits += 8 626 + while (bits >= 5) { 627 + bits -= 5 628 + result += alphabet[(value >> bits) & 31] 629 + } 630 + } 631 + 632 + if (bits > 0) { 633 + result += alphabet[(value << (5 - bits)) & 31] 634 + } 635 + 636 + return result 637 + } 638 + ``` 639 + 640 + **Step 4: Update main() to create operation** 641 + 642 + ```javascript 643 + async function main() { 644 + const opts = parseArgs() 645 + 646 + console.log('PDS Federation Setup') 647 + console.log('====================') 648 + console.log(`Handle: ${opts.handle}`) 649 + console.log(`PDS: ${opts.pds}`) 650 + console.log('') 651 + 652 + // Step 1: Generate keypair 653 + console.log('Generating P-256 keypair...') 654 + const keyPair = await generateP256Keypair() 655 + const didKey = publicKeyToDidKey(keyPair.publicKey) 656 + console.log(` did:key: ${didKey}`) 657 + console.log('') 658 + 659 + // Step 2: Create genesis operation 660 + console.log('Creating PLC genesis operation...') 661 + const { operation, fullHandle } = await createGenesisOperation({ 662 + didKey, 663 + handle: opts.handle, 664 + pdsUrl: opts.pds, 665 + cryptoKey: keyPair.cryptoKey 666 + }) 667 + const did = await deriveDidFromOperation(operation) 668 + console.log(` DID: ${did}`) 669 + console.log(` Handle: ${fullHandle}`) 670 + console.log('') 671 + 672 + // TODO: Register with plc.directory 673 + // TODO: Initialize PDS 674 + // TODO: Notify relay 675 + } 676 + ``` 677 + 678 + **Step 5: Test operation creation** 679 + 680 + Run: `node scripts/setup.js --handle alice --pds https://example.com` 681 + 682 + Expected: 683 + ``` 684 + Creating PLC genesis operation... 685 + DID: did:plc:... 686 + Handle: alice.example.com 687 + ``` 688 + 689 + **Step 6: Commit** 690 + 691 + ```bash 692 + git add scripts/setup.js 693 + git commit -m "feat: add PLC operation signing" 694 + ``` 695 + 696 + --- 697 + 698 + ## Task 6: Add PLC Directory Registration 699 + 700 + **Files:** 701 + - Modify: `scripts/setup.js` 702 + 703 + **Step 1: Add PLC registration function** 704 + 705 + ```javascript 706 + // === PLC DIRECTORY REGISTRATION === 707 + 708 + async function registerWithPlc(plcUrl, did, operation) { 709 + const url = `${plcUrl}/${encodeURIComponent(did)}` 710 + 711 + const response = await fetch(url, { 712 + method: 'POST', 713 + headers: { 714 + 'Content-Type': 'application/json' 715 + }, 716 + body: JSON.stringify(operation) 717 + }) 718 + 719 + if (!response.ok) { 720 + const text = await response.text() 721 + throw new Error(`PLC registration failed: ${response.status} ${text}`) 722 + } 723 + 724 + return true 725 + } 726 + ``` 727 + 728 + **Step 2: Update main() to register** 729 + 730 + Add after operation creation: 731 + 732 + ```javascript 733 + // Step 3: Register with PLC directory 734 + console.log(`Registering with ${opts.plcUrl}...`) 735 + await registerWithPlc(opts.plcUrl, did, operation) 736 + console.log(' Registered successfully!') 737 + console.log('') 738 + ``` 739 + 740 + **Step 3: Commit** 741 + 742 + ```bash 743 + git add scripts/setup.js 744 + git commit -m "feat: add PLC directory registration" 745 + ``` 746 + 747 + --- 748 + 749 + ## Task 7: Add PDS Initialization 750 + 751 + **Files:** 752 + - Modify: `scripts/setup.js` 753 + 754 + **Step 1: Add PDS initialization function** 755 + 756 + ```javascript 757 + // === PDS INITIALIZATION === 758 + 759 + async function initializePds(pdsUrl, did, privateKeyHex, handle) { 760 + const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}` 761 + 762 + const response = await fetch(url, { 763 + method: 'POST', 764 + headers: { 765 + 'Content-Type': 'application/json' 766 + }, 767 + body: JSON.stringify({ 768 + did, 769 + privateKey: privateKeyHex, 770 + handle 771 + }) 772 + }) 773 + 774 + if (!response.ok) { 775 + const text = await response.text() 776 + throw new Error(`PDS initialization failed: ${response.status} ${text}`) 777 + } 778 + 779 + return response.json() 780 + } 781 + ``` 782 + 783 + **Step 2: Update main() to initialize PDS** 784 + 785 + Add after PLC registration: 786 + 787 + ```javascript 788 + // Step 4: Initialize PDS 789 + console.log(`Initializing PDS at ${opts.pds}...`) 790 + const privateKeyHex = bytesToHex(keyPair.privateKey) 791 + await initializePds(opts.pds, did, privateKeyHex, fullHandle) 792 + console.log(' PDS initialized!') 793 + console.log('') 794 + ``` 795 + 796 + **Step 3: Commit** 797 + 798 + ```bash 799 + git add scripts/setup.js 800 + git commit -m "feat: add PDS initialization to setup script" 801 + ``` 802 + 803 + --- 804 + 805 + ## Task 8: Add Relay Notification 806 + 807 + **Files:** 808 + - Modify: `scripts/setup.js` 809 + 810 + **Step 1: Add relay notification function** 811 + 812 + ```javascript 813 + // === RELAY NOTIFICATION === 814 + 815 + async function notifyRelay(relayUrl, pdsHostname) { 816 + const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl` 817 + 818 + const response = await fetch(url, { 819 + method: 'POST', 820 + headers: { 821 + 'Content-Type': 'application/json' 822 + }, 823 + body: JSON.stringify({ 824 + hostname: pdsHostname 825 + }) 826 + }) 827 + 828 + // Relay might return 200 or 202, both are OK 829 + if (!response.ok && response.status !== 202) { 830 + const text = await response.text() 831 + console.warn(` Warning: Relay notification returned ${response.status}: ${text}`) 832 + return false 833 + } 834 + 835 + return true 836 + } 837 + ``` 838 + 839 + **Step 2: Update main() to notify relay** 840 + 841 + Add after PDS initialization: 842 + 843 + ```javascript 844 + // Step 5: Notify relay 845 + const pdsHostname = new URL(opts.pds).host 846 + console.log(`Notifying relay at ${opts.relayUrl}...`) 847 + const relayOk = await notifyRelay(opts.relayUrl, pdsHostname) 848 + if (relayOk) { 849 + console.log(' Relay notified!') 850 + } 851 + console.log('') 852 + ``` 853 + 854 + **Step 3: Commit** 855 + 856 + ```bash 857 + git add scripts/setup.js 858 + git commit -m "feat: add relay notification to setup script" 859 + ``` 860 + 861 + --- 862 + 863 + ## Task 9: Add Credentials File Output 864 + 865 + **Files:** 866 + - Modify: `scripts/setup.js` 867 + 868 + **Step 1: Add fs import and credentials saving** 869 + 870 + At the top of the file, add: 871 + 872 + ```javascript 873 + import { writeFileSync } from 'fs' 874 + ``` 875 + 876 + **Step 2: Add credentials saving function** 877 + 878 + ```javascript 879 + // === CREDENTIALS OUTPUT === 880 + 881 + function saveCredentials(filename, credentials) { 882 + writeFileSync(filename, JSON.stringify(credentials, null, 2)) 883 + } 884 + ``` 885 + 886 + **Step 3: Update main() with final output** 887 + 888 + Replace the end of main() with: 889 + 890 + ```javascript 891 + // Step 6: Save credentials 892 + const credentials = { 893 + handle: fullHandle, 894 + did, 895 + privateKeyHex: bytesToHex(keyPair.privateKey), 896 + didKey, 897 + pdsUrl: opts.pds, 898 + createdAt: new Date().toISOString() 899 + } 900 + 901 + const credentialsFile = `./credentials-${opts.handle}.json` 902 + saveCredentials(credentialsFile, credentials) 903 + 904 + // Final output 905 + console.log('Setup Complete!') 906 + console.log('===============') 907 + console.log(`Handle: ${fullHandle}`) 908 + console.log(`DID: ${did}`) 909 + console.log(`PDS: ${opts.pds}`) 910 + console.log('') 911 + console.log(`Credentials saved to: ${credentialsFile}`) 912 + console.log('Keep this file safe - it contains your private key!') 913 + } 914 + ``` 915 + 916 + **Step 4: Add .gitignore entry** 917 + 918 + Add to `.gitignore` (create if doesn't exist): 919 + 920 + ``` 921 + credentials-*.json 922 + ``` 923 + 924 + **Step 5: Commit** 925 + 926 + ```bash 927 + git add scripts/setup.js .gitignore 928 + git commit -m "feat: add credentials file output" 929 + ``` 930 + 931 + --- 932 + 933 + ## Task 10: Deploy and Test End-to-End 934 + 935 + **Files:** 936 + - None (testing only) 937 + 938 + **Step 1: Deploy updated PDS** 939 + 940 + ```bash 941 + source .env && npx wrangler deploy 942 + ``` 943 + 944 + **Step 2: Run full setup** 945 + 946 + ```bash 947 + node scripts/setup.js --handle testuser --pds https://atproto-pds.chad-53c.workers.dev 948 + ``` 949 + 950 + Expected output: 951 + ``` 952 + PDS Federation Setup 953 + ==================== 954 + Handle: testuser 955 + PDS: https://atproto-pds.chad-53c.workers.dev 956 + 957 + Generating P-256 keypair... 958 + did:key: zDnae... 959 + 960 + Creating PLC genesis operation... 961 + DID: did:plc:... 962 + Handle: testuser.atproto-pds.chad-53c.workers.dev 963 + 964 + Registering with https://plc.directory... 965 + Registered successfully! 966 + 967 + Initializing PDS at https://atproto-pds.chad-53c.workers.dev... 968 + PDS initialized! 969 + 970 + Notifying relay at https://bsky.network... 971 + Relay notified! 972 + 973 + Setup Complete! 974 + =============== 975 + Handle: testuser.atproto-pds.chad-53c.workers.dev 976 + DID: did:plc:... 977 + PDS: https://atproto-pds.chad-53c.workers.dev 978 + 979 + Credentials saved to: ./credentials-testuser.json 980 + ``` 981 + 982 + **Step 3: Verify handle resolution** 983 + 984 + ```bash 985 + curl "https://atproto-pds.chad-53c.workers.dev/.well-known/atproto-did?did=<your-did>" 986 + ``` 987 + 988 + Expected: Returns the DID as plain text 989 + 990 + **Step 4: Verify on plc.directory** 991 + 992 + ```bash 993 + curl "https://plc.directory/<your-did>" 994 + ``` 995 + 996 + Expected: Returns DID document with your PDS as the service endpoint 997 + 998 + **Step 5: Create a test post** 999 + 1000 + ```bash 1001 + curl -X POST "https://atproto-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.createRecord?did=<your-did>" \ 1002 + -H "Content-Type: application/json" \ 1003 + -d '{"collection":"app.bsky.feed.post","record":{"text":"Hello from my Cloudflare PDS!","createdAt":"2026-01-05T12:00:00.000Z"}}' 1004 + ``` 1005 + 1006 + **Step 6: Commit final state** 1007 + 1008 + ```bash 1009 + git add -A 1010 + git commit -m "chore: federation support complete" 1011 + ``` 1012 + 1013 + --- 1014 + 1015 + ## Summary 1016 + 1017 + **Files created/modified:** 1018 + - `src/pds.js` - Added handle storage and `/.well-known/atproto-did` endpoint 1019 + - `scripts/setup.js` - Complete setup script (~300 lines, zero dependencies) 1020 + - `package.json` - Added `setup` script 1021 + - `.gitignore` - Added credentials file pattern 1022 + 1023 + **What the setup script does:** 1024 + 1. Generates P-256 keypair 1025 + 2. Creates did:key from public key 1026 + 3. Builds and signs PLC genesis operation 1027 + 4. Derives DID from operation 1028 + 5. Registers with plc.directory 1029 + 6. Initializes PDS with identity 1030 + 7. Notifies relay 1031 + 8. Saves credentials to file 1032 + 1033 + **Usage:** 1034 + ```bash 1035 + npm run setup -- --handle yourname --pds https://your-pds.workers.dev 1036 + ```