# Federation Support Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Enable the Cloudflare PDS to federate with Bluesky by adding handle resolution, DID:PLC registration, and relay notification. **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. **Tech Stack:** Cloudflare Workers (existing), Node.js crypto (setup script), plc.directory API, bsky.network relay --- ## Task 1: Add Handle Storage to PDS **Files:** - Modify: `src/pds.js` **Step 1: Update /init endpoint to accept handle** In `src/pds.js`, modify the `/init` endpoint to also store the handle: ```javascript if (url.pathname === '/init') { const body = await request.json() if (!body.did || !body.privateKey) { return Response.json({ error: 'missing did or privateKey' }, { status: 400 }) } await this.initIdentity(body.did, body.privateKey, body.handle || null) return Response.json({ ok: true, did: body.did, handle: body.handle || null }) } ``` **Step 2: Update initIdentity method** Modify the `initIdentity` method to store handle: ```javascript async initIdentity(did, privateKeyHex, handle = null) { await this.state.storage.put('did', did) await this.state.storage.put('privateKey', privateKeyHex) if (handle) { await this.state.storage.put('handle', handle) } } ``` **Step 3: Add getHandle method** Add after `getDid()`: ```javascript async getHandle() { return this.state.storage.get('handle') } ``` **Step 4: Test locally** Run: `npx wrangler dev --port 8788` ```bash curl -X POST "http://localhost:8788/init?did=did:plc:test" \ -H "Content-Type: application/json" \ -d '{"did":"did:plc:test","privateKey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","handle":"alice.example.com"}' ``` Expected: `{"ok":true,"did":"did:plc:test","handle":"alice.example.com"}` **Step 5: Commit** ```bash git add src/pds.js git commit -m "feat: add handle storage to identity" ``` --- ## Task 2: Add Handle Resolution Endpoint **Files:** - Modify: `src/pds.js` **Step 1: Add /.well-known/atproto-did endpoint** 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: ```javascript async fetch(request) { const url = new URL(request.url) // Handle resolution - doesn't require ?did= param if (url.pathname === '/.well-known/atproto-did') { const did = await this.getDid() if (!did) { return new Response('User not found', { status: 404 }) } return new Response(did, { headers: { 'Content-Type': 'text/plain' } }) } // ... rest of existing fetch code ``` **Step 2: Update the default export to handle /.well-known without ?did=** The main router needs to route `/.well-known/atproto-did` requests differently. Modify the default export: ```javascript export default { async fetch(request, env) { const url = new URL(request.url) // For /.well-known/atproto-did, extract DID from subdomain // e.g., alice.atproto-pds.chad-53c.workers.dev -> look up "alice" if (url.pathname === '/.well-known/atproto-did') { const host = request.headers.get('Host') || '' // For now, use the first Durable Object (single-user PDS) // Extract handle from subdomain if present const did = url.searchParams.get('did') || 'default' const id = env.PDS.idFromName(did) const pds = env.PDS.get(id) return pds.fetch(request) } const did = url.searchParams.get('did') if (!did) { return new Response('missing did param', { status: 400 }) } const id = env.PDS.idFromName(did) const pds = env.PDS.get(id) return pds.fetch(request) } } ``` **Step 3: Test locally** Run: `npx wrangler dev --port 8788` First init: ```bash curl -X POST "http://localhost:8788/init?did=did:plc:testhandle" \ -H "Content-Type: application/json" \ -d '{"did":"did:plc:testhandle","privateKey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","handle":"alice"}' ``` Then test resolution: ```bash curl "http://localhost:8788/.well-known/atproto-did?did=did:plc:testhandle" ``` Expected: `did:plc:testhandle` **Step 4: Commit** ```bash git add src/pds.js git commit -m "feat: add handle resolution endpoint" ``` --- ## Task 3: Create Setup Script Skeleton **Files:** - Create: `scripts/setup.js` - Modify: `package.json` **Step 1: Create scripts directory and setup.js** Create `scripts/setup.js`: ```javascript #!/usr/bin/env node /** * PDS Setup Script * * Registers a did:plc, initializes the PDS, and notifies the relay. * Zero dependencies - uses Node.js built-ins only. * * Usage: node scripts/setup.js --handle alice --pds https://your-pds.workers.dev */ import { webcrypto } from 'crypto' // === ARGUMENT PARSING === function parseArgs() { const args = process.argv.slice(2) const opts = { handle: null, pds: null, plcUrl: 'https://plc.directory', relayUrl: 'https://bsky.network' } for (let i = 0; i < args.length; i++) { if (args[i] === '--handle' && args[i + 1]) { opts.handle = args[++i] } else if (args[i] === '--pds' && args[i + 1]) { opts.pds = args[++i] } else if (args[i] === '--plc-url' && args[i + 1]) { opts.plcUrl = args[++i] } else if (args[i] === '--relay-url' && args[i + 1]) { opts.relayUrl = args[++i] } } if (!opts.handle || !opts.pds) { console.error('Usage: node scripts/setup.js --handle --pds ') console.error('') console.error('Options:') console.error(' --handle Handle name (e.g., "alice")') console.error(' --pds PDS URL (e.g., "https://atproto-pds.chad-53c.workers.dev")') console.error(' --plc-url PLC directory URL (default: https://plc.directory)') console.error(' --relay-url Relay URL (default: https://bsky.network)') process.exit(1) } return opts } // === MAIN === async function main() { const opts = parseArgs() console.log('PDS Federation Setup') console.log('====================') console.log(`Handle: ${opts.handle}`) console.log(`PDS: ${opts.pds}`) console.log(`PLC: ${opts.plcUrl}`) console.log(`Relay: ${opts.relayUrl}`) console.log('') // TODO: Implement in subsequent tasks console.log('TODO: Generate keypair') console.log('TODO: Register DID:PLC') console.log('TODO: Initialize PDS') console.log('TODO: Notify relay') } main().catch(err => { console.error('Error:', err.message) process.exit(1) }) ``` **Step 2: Add npm script** Modify `package.json`: ```json { "name": "cloudflare-pds", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "test": "node --test test/*.test.js", "setup": "node scripts/setup.js" } } ``` **Step 3: Test the skeleton** Run: `node scripts/setup.js --handle alice --pds https://example.com` Expected: ``` PDS Federation Setup ==================== Handle: alice PDS: https://example.com ... ``` **Step 4: Commit** ```bash git add scripts/setup.js package.json git commit -m "feat: add setup script skeleton" ``` --- ## Task 4: Add P-256 Key Generation **Files:** - Modify: `scripts/setup.js` **Step 1: Add key generation utilities** Add after the argument parsing section: ```javascript // === KEY GENERATION === async function generateP256Keypair() { const keyPair = await webcrypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'] ) // Export private key as raw 32 bytes const privateJwk = await webcrypto.subtle.exportKey('jwk', keyPair.privateKey) const privateBytes = base64UrlDecode(privateJwk.d) // Export public key as uncompressed point (65 bytes) const publicRaw = await webcrypto.subtle.exportKey('raw', keyPair.publicKey) const publicBytes = new Uint8Array(publicRaw) // Compress public key to 33 bytes const compressedPublic = compressPublicKey(publicBytes) return { privateKey: privateBytes, publicKey: compressedPublic, cryptoKey: keyPair.privateKey } } function compressPublicKey(uncompressed) { // uncompressed is 65 bytes: 0x04 + x(32) + y(32) const x = uncompressed.slice(1, 33) const y = uncompressed.slice(33, 65) const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03 const compressed = new Uint8Array(33) compressed[0] = prefix compressed.set(x, 1) return compressed } function base64UrlDecode(str) { const base64 = str.replace(/-/g, '+').replace(/_/g, '/') const binary = atob(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i) } return bytes } function bytesToHex(bytes) { return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') } ``` **Step 2: Add did:key encoding for P-256** ```javascript // === DID:KEY ENCODING === // Multicodec prefix for P-256 public key (0x1200) const P256_MULTICODEC = new Uint8Array([0x80, 0x24]) function publicKeyToDidKey(compressedPublicKey) { // did:key format: "did:key:" + multibase(base58btc) of multicodec + key const keyWithCodec = new Uint8Array(P256_MULTICODEC.length + compressedPublicKey.length) keyWithCodec.set(P256_MULTICODEC) keyWithCodec.set(compressedPublicKey, P256_MULTICODEC.length) return 'did:key:z' + base58btcEncode(keyWithCodec) } function base58btcEncode(bytes) { const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' // Count leading zeros let zeros = 0 for (const b of bytes) { if (b === 0) zeros++ else break } // Convert to base58 const digits = [0] for (const byte of bytes) { let carry = byte for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8 digits[i] = carry % 58 carry = (carry / 58) | 0 } while (carry > 0) { digits.push(carry % 58) carry = (carry / 58) | 0 } } // Convert to string let result = '1'.repeat(zeros) for (let i = digits.length - 1; i >= 0; i--) { result += ALPHABET[digits[i]] } return result } ``` **Step 3: Update main() to generate and display keys** ```javascript async function main() { const opts = parseArgs() console.log('PDS Federation Setup') console.log('====================') console.log(`Handle: ${opts.handle}`) console.log(`PDS: ${opts.pds}`) console.log('') // Step 1: Generate keypair console.log('Generating P-256 keypair...') const keyPair = await generateP256Keypair() const didKey = publicKeyToDidKey(keyPair.publicKey) console.log(` did:key: ${didKey}`) console.log(` Private key: ${bytesToHex(keyPair.privateKey)}`) console.log('') // TODO: Register DID:PLC // TODO: Initialize PDS // TODO: Notify relay } ``` **Step 4: Test key generation** Run: `node scripts/setup.js --handle alice --pds https://example.com` Expected: ``` Generating P-256 keypair... did:key: zDnae... Private key: abcd1234... ``` **Step 5: Commit** ```bash git add scripts/setup.js git commit -m "feat: add P-256 key generation to setup script" ``` --- ## Task 5: Add DID:PLC Operation Signing **Files:** - Modify: `scripts/setup.js` **Step 1: Add CBOR encoding (minimal, for PLC operations)** ```javascript // === CBOR ENCODING (minimal for PLC operations) === function cborEncode(value) { const parts = [] function encode(val) { if (val === null) { parts.push(0xf6) } else if (typeof val === 'string') { const bytes = new TextEncoder().encode(val) encodeHead(3, bytes.length) parts.push(...bytes) } else if (typeof val === 'number') { if (Number.isInteger(val) && val >= 0) { encodeHead(0, val) } } else if (val instanceof Uint8Array) { encodeHead(2, val.length) parts.push(...val) } else if (Array.isArray(val)) { encodeHead(4, val.length) for (const item of val) encode(item) } else if (typeof val === 'object') { const keys = Object.keys(val).sort() encodeHead(5, keys.length) for (const key of keys) { encode(key) encode(val[key]) } } } function encodeHead(majorType, length) { const mt = majorType << 5 if (length < 24) { parts.push(mt | length) } else if (length < 256) { parts.push(mt | 24, length) } else if (length < 65536) { parts.push(mt | 25, length >> 8, length & 0xff) } } encode(value) return new Uint8Array(parts) } ``` **Step 2: Add SHA-256 hashing** ```javascript // === HASHING === async function sha256(data) { const hash = await webcrypto.subtle.digest('SHA-256', data) return new Uint8Array(hash) } ``` **Step 3: Add PLC operation signing** ```javascript // === PLC OPERATIONS === async function signPlcOperation(operation, privateKey) { // Encode operation without sig field const { sig, ...opWithoutSig } = operation const encoded = cborEncode(opWithoutSig) // Sign with P-256 const signature = await webcrypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, privateKey, encoded ) // Convert to low-S form and base64url encode const sigBytes = ensureLowS(new Uint8Array(signature)) return base64UrlEncode(sigBytes) } function ensureLowS(sig) { // P-256 order N const N = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551') const halfN = N / 2n const r = sig.slice(0, 32) const s = sig.slice(32, 64) // Convert s to BigInt let sInt = BigInt('0x' + bytesToHex(s)) // If s > N/2, replace with N - s if (sInt > halfN) { sInt = N - sInt const newS = hexToBytes(sInt.toString(16).padStart(64, '0')) const result = new Uint8Array(64) result.set(r) result.set(newS, 32) return result } return sig } function hexToBytes(hex) { const bytes = new Uint8Array(hex.length / 2) for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16) } return bytes } function base64UrlEncode(bytes) { const binary = String.fromCharCode(...bytes) return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } async function createGenesisOperation(opts) { const { didKey, handle, pdsUrl, cryptoKey } = opts // Build the full handle const pdsHost = new URL(pdsUrl).host const fullHandle = `${handle}.${pdsHost}` const operation = { type: 'plc_operation', rotationKeys: [didKey], verificationMethods: { atproto: didKey }, alsoKnownAs: [`at://${fullHandle}`], services: { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: pdsUrl } }, prev: null } // Sign the operation operation.sig = await signPlcOperation(operation, cryptoKey) return { operation, fullHandle } } async function deriveDidFromOperation(operation) { const { sig, ...opWithoutSig } = operation const encoded = cborEncode(opWithoutSig) const hash = await sha256(encoded) // DID is base32 of first 24 bytes of hash return 'did:plc:' + base32Encode(hash.slice(0, 24)) } function base32Encode(bytes) { const alphabet = 'abcdefghijklmnopqrstuvwxyz234567' let result = '' let bits = 0 let value = 0 for (const byte of bytes) { value = (value << 8) | byte bits += 8 while (bits >= 5) { bits -= 5 result += alphabet[(value >> bits) & 31] } } if (bits > 0) { result += alphabet[(value << (5 - bits)) & 31] } return result } ``` **Step 4: Update main() to create operation** ```javascript async function main() { const opts = parseArgs() console.log('PDS Federation Setup') console.log('====================') console.log(`Handle: ${opts.handle}`) console.log(`PDS: ${opts.pds}`) console.log('') // Step 1: Generate keypair console.log('Generating P-256 keypair...') const keyPair = await generateP256Keypair() const didKey = publicKeyToDidKey(keyPair.publicKey) console.log(` did:key: ${didKey}`) console.log('') // Step 2: Create genesis operation console.log('Creating PLC genesis operation...') const { operation, fullHandle } = await createGenesisOperation({ didKey, handle: opts.handle, pdsUrl: opts.pds, cryptoKey: keyPair.cryptoKey }) const did = await deriveDidFromOperation(operation) console.log(` DID: ${did}`) console.log(` Handle: ${fullHandle}`) console.log('') // TODO: Register with plc.directory // TODO: Initialize PDS // TODO: Notify relay } ``` **Step 5: Test operation creation** Run: `node scripts/setup.js --handle alice --pds https://example.com` Expected: ``` Creating PLC genesis operation... DID: did:plc:... Handle: alice.example.com ``` **Step 6: Commit** ```bash git add scripts/setup.js git commit -m "feat: add PLC operation signing" ``` --- ## Task 6: Add PLC Directory Registration **Files:** - Modify: `scripts/setup.js` **Step 1: Add PLC registration function** ```javascript // === PLC DIRECTORY REGISTRATION === async function registerWithPlc(plcUrl, did, operation) { const url = `${plcUrl}/${encodeURIComponent(did)}` const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(operation) }) if (!response.ok) { const text = await response.text() throw new Error(`PLC registration failed: ${response.status} ${text}`) } return true } ``` **Step 2: Update main() to register** Add after operation creation: ```javascript // Step 3: Register with PLC directory console.log(`Registering with ${opts.plcUrl}...`) await registerWithPlc(opts.plcUrl, did, operation) console.log(' Registered successfully!') console.log('') ``` **Step 3: Commit** ```bash git add scripts/setup.js git commit -m "feat: add PLC directory registration" ``` --- ## Task 7: Add PDS Initialization **Files:** - Modify: `scripts/setup.js` **Step 1: Add PDS initialization function** ```javascript // === PDS INITIALIZATION === async function initializePds(pdsUrl, did, privateKeyHex, handle) { const url = `${pdsUrl}/init?did=${encodeURIComponent(did)}` const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ did, privateKey: privateKeyHex, handle }) }) if (!response.ok) { const text = await response.text() throw new Error(`PDS initialization failed: ${response.status} ${text}`) } return response.json() } ``` **Step 2: Update main() to initialize PDS** Add after PLC registration: ```javascript // Step 4: Initialize PDS console.log(`Initializing PDS at ${opts.pds}...`) const privateKeyHex = bytesToHex(keyPair.privateKey) await initializePds(opts.pds, did, privateKeyHex, fullHandle) console.log(' PDS initialized!') console.log('') ``` **Step 3: Commit** ```bash git add scripts/setup.js git commit -m "feat: add PDS initialization to setup script" ``` --- ## Task 8: Add Relay Notification **Files:** - Modify: `scripts/setup.js` **Step 1: Add relay notification function** ```javascript // === RELAY NOTIFICATION === async function notifyRelay(relayUrl, pdsHostname) { const url = `${relayUrl}/xrpc/com.atproto.sync.requestCrawl` const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hostname: pdsHostname }) }) // Relay might return 200 or 202, both are OK if (!response.ok && response.status !== 202) { const text = await response.text() console.warn(` Warning: Relay notification returned ${response.status}: ${text}`) return false } return true } ``` **Step 2: Update main() to notify relay** Add after PDS initialization: ```javascript // Step 5: Notify relay const pdsHostname = new URL(opts.pds).host console.log(`Notifying relay at ${opts.relayUrl}...`) const relayOk = await notifyRelay(opts.relayUrl, pdsHostname) if (relayOk) { console.log(' Relay notified!') } console.log('') ``` **Step 3: Commit** ```bash git add scripts/setup.js git commit -m "feat: add relay notification to setup script" ``` --- ## Task 9: Add Credentials File Output **Files:** - Modify: `scripts/setup.js` **Step 1: Add fs import and credentials saving** At the top of the file, add: ```javascript import { writeFileSync } from 'fs' ``` **Step 2: Add credentials saving function** ```javascript // === CREDENTIALS OUTPUT === function saveCredentials(filename, credentials) { writeFileSync(filename, JSON.stringify(credentials, null, 2)) } ``` **Step 3: Update main() with final output** Replace the end of main() with: ```javascript // Step 6: Save credentials const credentials = { handle: fullHandle, did, privateKeyHex: bytesToHex(keyPair.privateKey), didKey, pdsUrl: opts.pds, createdAt: new Date().toISOString() } const credentialsFile = `./credentials-${opts.handle}.json` saveCredentials(credentialsFile, credentials) // Final output console.log('Setup Complete!') console.log('===============') console.log(`Handle: ${fullHandle}`) console.log(`DID: ${did}`) console.log(`PDS: ${opts.pds}`) console.log('') console.log(`Credentials saved to: ${credentialsFile}`) console.log('Keep this file safe - it contains your private key!') } ``` **Step 4: Add .gitignore entry** Add to `.gitignore` (create if doesn't exist): ``` credentials-*.json ``` **Step 5: Commit** ```bash git add scripts/setup.js .gitignore git commit -m "feat: add credentials file output" ``` --- ## Task 10: Deploy and Test End-to-End **Files:** - None (testing only) **Step 1: Deploy updated PDS** ```bash source .env && npx wrangler deploy ``` **Step 2: Run full setup** ```bash node scripts/setup.js --handle testuser --pds https://atproto-pds.chad-53c.workers.dev ``` Expected output: ``` PDS Federation Setup ==================== Handle: testuser PDS: https://atproto-pds.chad-53c.workers.dev Generating P-256 keypair... did:key: zDnae... Creating PLC genesis operation... DID: did:plc:... Handle: testuser.atproto-pds.chad-53c.workers.dev Registering with https://plc.directory... Registered successfully! Initializing PDS at https://atproto-pds.chad-53c.workers.dev... PDS initialized! Notifying relay at https://bsky.network... Relay notified! Setup Complete! =============== Handle: testuser.atproto-pds.chad-53c.workers.dev DID: did:plc:... PDS: https://atproto-pds.chad-53c.workers.dev Credentials saved to: ./credentials-testuser.json ``` **Step 3: Verify handle resolution** ```bash curl "https://atproto-pds.chad-53c.workers.dev/.well-known/atproto-did?did=" ``` Expected: Returns the DID as plain text **Step 4: Verify on plc.directory** ```bash curl "https://plc.directory/" ``` Expected: Returns DID document with your PDS as the service endpoint **Step 5: Create a test post** ```bash curl -X POST "https://atproto-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.createRecord?did=" \ -H "Content-Type: application/json" \ -d '{"collection":"app.bsky.feed.post","record":{"text":"Hello from my Cloudflare PDS!","createdAt":"2026-01-05T12:00:00.000Z"}}' ``` **Step 6: Commit final state** ```bash git add -A git commit -m "chore: federation support complete" ``` --- ## Summary **Files created/modified:** - `src/pds.js` - Added handle storage and `/.well-known/atproto-did` endpoint - `scripts/setup.js` - Complete setup script (~300 lines, zero dependencies) - `package.json` - Added `setup` script - `.gitignore` - Added credentials file pattern **What the setup script does:** 1. Generates P-256 keypair 2. Creates did:key from public key 3. Builds and signs PLC genesis operation 4. Derives DID from operation 5. Registers with plc.directory 6. Initializes PDS with identity 7. Notifies relay 8. Saves credentials to file **Usage:** ```bash npm run setup -- --handle yourname --pds https://your-pds.workers.dev ```