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```