this repo has no description
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 20In `src/pds.js`, modify the `/init` endpoint to also store the handle: 21 22```javascript 23if (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 35Modify the `initIdentity` method to store handle: 36 37```javascript 38async 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 49Add after `getDid()`: 50 51```javascript 52async getHandle() { 53 return this.state.storage.get('handle') 54} 55``` 56 57**Step 4: Test locally** 58 59Run: `npx wrangler dev --port 8788` 60 61```bash 62curl -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 67Expected: `{"ok":true,"did":"did:plc:test","handle":"alice.example.com"}` 68 69**Step 5: Commit** 70 71```bash 72git add src/pds.js 73git 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 85Add 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 88async 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 107The main router needs to route `/.well-known/atproto-did` requests differently. Modify the default export: 108 109```javascript 110export 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 140Run: `npx wrangler dev --port 8788` 141 142First init: 143```bash 144curl -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 149Then test resolution: 150```bash 151curl "http://localhost:8788/.well-known/atproto-did?did=did:plc:testhandle" 152``` 153 154Expected: `did:plc:testhandle` 155 156**Step 4: Commit** 157 158```bash 159git add src/pds.js 160git 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 173Create `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 187import { webcrypto } from 'crypto' 188 189// === ARGUMENT PARSING === 190 191function 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 228async 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 246main().catch(err => { 247 console.error('Error:', err.message) 248 process.exit(1) 249}) 250``` 251 252**Step 2: Add npm script** 253 254Modify `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 273Run: `node scripts/setup.js --handle alice --pds https://example.com` 274 275Expected: 276``` 277PDS Federation Setup 278==================== 279Handle: alice 280PDS: https://example.com 281... 282``` 283 284**Step 4: Commit** 285 286```bash 287git add scripts/setup.js package.json 288git 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 300Add after the argument parsing section: 301 302```javascript 303// === KEY GENERATION === 304 305async 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 330function 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 341function 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 351function 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) 362const P256_MULTICODEC = new Uint8Array([0x80, 0x24]) 363 364function 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 373function 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 411async 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 436Run: `node scripts/setup.js --handle alice --pds https://example.com` 437 438Expected: 439``` 440Generating P-256 keypair... 441 did:key: zDnae... 442 Private key: abcd1234... 443``` 444 445**Step 5: Commit** 446 447```bash 448git add scripts/setup.js 449git 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 464function 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 515async 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 526async 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 543function 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 567function 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 575function base64UrlEncode(bytes) { 576 const binary = String.fromCharCode(...bytes) 577 return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 578} 579 580async 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 609async 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 617function 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 643async 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 680Run: `node scripts/setup.js --handle alice --pds https://example.com` 681 682Expected: 683``` 684Creating PLC genesis operation... 685 DID: did:plc:... 686 Handle: alice.example.com 687``` 688 689**Step 6: Commit** 690 691```bash 692git add scripts/setup.js 693git 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 708async 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 730Add 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 743git add scripts/setup.js 744git 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 759async 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 785Add 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 799git add scripts/setup.js 800git 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 815async 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 841Add 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 857git add scripts/setup.js 858git 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 870At the top of the file, add: 871 872```javascript 873import { writeFileSync } from 'fs' 874``` 875 876**Step 2: Add credentials saving function** 877 878```javascript 879// === CREDENTIALS OUTPUT === 880 881function saveCredentials(filename, credentials) { 882 writeFileSync(filename, JSON.stringify(credentials, null, 2)) 883} 884``` 885 886**Step 3: Update main() with final output** 887 888Replace 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 918Add to `.gitignore` (create if doesn't exist): 919 920``` 921credentials-*.json 922``` 923 924**Step 5: Commit** 925 926```bash 927git add scripts/setup.js .gitignore 928git 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 941source .env && npx wrangler deploy 942``` 943 944**Step 2: Run full setup** 945 946```bash 947node scripts/setup.js --handle testuser --pds https://atproto-pds.chad-53c.workers.dev 948``` 949 950Expected output: 951``` 952PDS Federation Setup 953==================== 954Handle: testuser 955PDS: https://atproto-pds.chad-53c.workers.dev 956 957Generating P-256 keypair... 958 did:key: zDnae... 959 960Creating PLC genesis operation... 961 DID: did:plc:... 962 Handle: testuser.atproto-pds.chad-53c.workers.dev 963 964Registering with https://plc.directory... 965 Registered successfully! 966 967Initializing PDS at https://atproto-pds.chad-53c.workers.dev... 968 PDS initialized! 969 970Notifying relay at https://bsky.network... 971 Relay notified! 972 973Setup Complete! 974=============== 975Handle: testuser.atproto-pds.chad-53c.workers.dev 976DID: did:plc:... 977PDS: https://atproto-pds.chad-53c.workers.dev 978 979Credentials saved to: ./credentials-testuser.json 980``` 981 982**Step 3: Verify handle resolution** 983 984```bash 985curl "https://atproto-pds.chad-53c.workers.dev/.well-known/atproto-did?did=<your-did>" 986``` 987 988Expected: Returns the DID as plain text 989 990**Step 4: Verify on plc.directory** 991 992```bash 993curl "https://plc.directory/<your-did>" 994``` 995 996Expected: Returns DID document with your PDS as the service endpoint 997 998**Step 5: Create a test post** 999 1000```bash 1001curl -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 1009git add -A 1010git 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:** 10241. Generates P-256 keypair 10252. Creates did:key from public key 10263. Builds and signs PLC genesis operation 10274. Derives DID from operation 10285. Registers with plc.directory 10296. Initializes PDS with identity 10307. Notifies relay 10318. Saves credentials to file 1032 1033**Usage:** 1034```bash 1035npm run setup -- --handle yourname --pds https://your-pds.workers.dev 1036```