# Foreign DID Proxying Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Handle foreign DID requests by either (1) respecting `atproto-proxy` header, or (2) detecting foreign `repo` param and proxying to AppView. **Architecture:** (matches official PDS) 1. Check if `repo` is a local DID → handle locally (ignore atproto-proxy) 2. If foreign DID with `atproto-proxy` header → proxy to specified service 3. If foreign DID without header → proxy to AppView (default) **Tech Stack:** Cloudflare Workers, Durable Objects, ATProto --- ## Background When a client needs data from a foreign DID, it may: 1. Send `atproto-proxy: did:web:api.bsky.app#bsky_appview` header (explicit) 2. Just send `repo=did:plc:foreign...` without header (implicit) Our PDS should handle both cases. Currently it ignores the header and always tries to find records locally. --- ### Task 1: Add parseAtprotoProxyHeader Utility **Files:** - Modify: `src/pds.js` (after errorResponse function, around line 178) **Step 1: Add the utility function** ```javascript /** * Parse atproto-proxy header to get service DID and service ID * Format: "did:web:api.bsky.app#bsky_appview" * @param {string} header * @returns {{ did: string, serviceId: string } | null} */ function parseAtprotoProxyHeader(header) { if (!header) return null; const hashIndex = header.indexOf('#'); if (hashIndex === -1 || hashIndex === 0 || hashIndex === header.length - 1) { return null; } return { did: header.slice(0, hashIndex), serviceId: header.slice(hashIndex + 1), }; } ``` **Step 2: Commit** ```bash git add src/pds.js git commit -m "feat: add parseAtprotoProxyHeader utility" ``` --- ### Task 2: Add getKnownServiceUrl Utility **Files:** - Modify: `src/pds.js` (after parseAtprotoProxyHeader) **Step 1: Add utility to resolve service URLs** ```javascript /** * Get URL for a known service DID * @param {string} did - Service DID (e.g., "did:web:api.bsky.app") * @param {string} serviceId - Service ID (e.g., "bsky_appview") * @returns {string | null} */ function getKnownServiceUrl(did, serviceId) { // Known Bluesky services if (did === 'did:web:api.bsky.app' && serviceId === 'bsky_appview') { return 'https://api.bsky.app'; } // Add more known services as needed return null; } ``` **Step 2: Commit** ```bash git add src/pds.js git commit -m "feat: add getKnownServiceUrl utility" ``` --- ### Task 3: Add proxyToService Utility **Files:** - Modify: `src/pds.js` (after getKnownServiceUrl) **Step 1: Add the proxy utility function** ```javascript /** * Proxy a request to a service * @param {Request} request - Original request * @param {string} serviceUrl - Target service URL (e.g., "https://api.bsky.app") * @param {string} [authHeader] - Optional Authorization header * @returns {Promise} */ async function proxyToService(request, serviceUrl, authHeader) { const url = new URL(request.url); const targetUrl = new URL(url.pathname + url.search, serviceUrl); const headers = new Headers(); if (authHeader) { headers.set('Authorization', authHeader); } headers.set( 'Content-Type', request.headers.get('Content-Type') || 'application/json', ); const acceptHeader = request.headers.get('Accept'); if (acceptHeader) { headers.set('Accept', acceptHeader); } const acceptLangHeader = request.headers.get('Accept-Language'); if (acceptLangHeader) { headers.set('Accept-Language', acceptLangHeader); } // Forward atproto-specific headers const labelersHeader = request.headers.get('atproto-accept-labelers'); if (labelersHeader) { headers.set('atproto-accept-labelers', labelersHeader); } const topicsHeader = request.headers.get('x-bsky-topics'); if (topicsHeader) { headers.set('x-bsky-topics', topicsHeader); } try { const response = await fetch(targetUrl.toString(), { method: request.method, headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined, }); const responseHeaders = new Headers(response.headers); responseHeaders.set('Access-Control-Allow-Origin', '*'); return new Response(response.body, { status: response.status, statusText: response.statusText, headers: responseHeaders, }); } catch (err) { const message = err instanceof Error ? err.message : String(err); return errorResponse('UpstreamFailure', `Failed to reach service: ${message}`, 502); } } ``` **Step 2: Commit** ```bash git add src/pds.js git commit -m "feat: add proxyToService utility" ``` --- ### Task 4: Add isLocalDid Helper **Files:** - Modify: `src/pds.js` (after proxyToService) **Step 1: Add helper to check if DID is registered locally** ```javascript /** * Check if a DID is registered on this PDS * @param {Env} env * @param {string} did * @returns {Promise} */ async function isLocalDid(env, did) { const defaultPds = getDefaultPds(env); const res = await defaultPds.fetch( new Request('http://internal/get-registered-dids'), ); if (!res.ok) return false; const { dids } = await res.json(); return dids.includes(did); } ``` **Step 2: Commit** ```bash git add src/pds.js git commit -m "feat: add isLocalDid helper" ``` --- ### Task 5: Refactor handleAppViewProxy to Use proxyToService **Files:** - Modify: `src/pds.js:2725-2782` (handleAppViewProxy in PersonalDataServer class) **Step 1: Refactor the method** Replace with: ```javascript /** * @param {Request} request * @param {string} userDid */ async handleAppViewProxy(request, userDid) { const url = new URL(request.url); const lxm = url.pathname.replace('/xrpc/', ''); const serviceJwt = await this.createServiceAuthForAppView(userDid, lxm); return proxyToService(request, 'https://api.bsky.app', `Bearer ${serviceJwt}`); } ``` **Step 2: Run existing tests** ```bash npm test ``` Expected: All tests pass **Step 3: Commit** ```bash git add src/pds.js git commit -m "refactor: simplify handleAppViewProxy using proxyToService" ``` --- ### Task 6: Handle Foreign Repo with atproto-proxy Support in Worker Routing **Files:** - Modify: `src/pds.js` in `handleRequest` function (around line 5199) **Step 1: Update repo endpoints routing to match official PDS behavior** Find the repo endpoints routing block and REPLACE the entire block. Order of operations (matches official PDS): 1. Check if repo is local → return local data 2. If foreign → check atproto-proxy header for specific service 3. If no header → default to AppView ```javascript // Repo endpoints use ?repo= param instead of ?did= if ( url.pathname === '/xrpc/com.atproto.repo.describeRepo' || url.pathname === '/xrpc/com.atproto.repo.listRecords' || url.pathname === '/xrpc/com.atproto.repo.getRecord' ) { const repo = url.searchParams.get('repo'); if (!repo) { return errorResponse('InvalidRequest', 'missing repo param', 400); } // Check if this is a local DID - if so, handle locally const isLocal = await isLocalDid(env, repo); if (isLocal) { const id = env.PDS.idFromName(repo); const pds = env.PDS.get(id); return pds.fetch(request); } // Foreign DID - check for atproto-proxy header const proxyHeader = request.headers.get('atproto-proxy'); if (proxyHeader) { const parsed = parseAtprotoProxyHeader(proxyHeader); if (parsed) { const serviceUrl = getKnownServiceUrl(parsed.did, parsed.serviceId); if (serviceUrl) { return proxyToService(request, serviceUrl); } // Unknown service - could add DID resolution here in the future return errorResponse('InvalidRequest', `Unknown proxy service: ${proxyHeader}`, 400); } } // No header - default to AppView return proxyToService(request, 'https://api.bsky.app'); } ``` **Step 2: Run existing tests** ```bash npm test ``` Expected: All tests pass **Step 3: Commit** ```bash git add src/pds.js git commit -m "feat: handle atproto-proxy header and foreign repo proxying" ``` --- ### Task 7: Add E2E Tests **Files:** - Modify: `test/e2e.test.js` **Step 1: Add tests for proxy functionality** Add a new describe block: ```javascript describe('Foreign DID proxying', () => { it('proxies to AppView when atproto-proxy header present', async () => { // Use a known public post from Bluesky (bsky.app official account) const res = await fetch( `${BASE}/xrpc/com.atproto.repo.getRecord?repo=did:plc:z72i7hdynmk6r22z27h6tvur&collection=app.bsky.feed.post&rkey=3juzlwllznd24`, { headers: { 'atproto-proxy': 'did:web:api.bsky.app#bsky_appview', }, }, ); // Should get response from AppView, not local 404 assert.ok( res.status === 200 || res.status === 400, `Expected 200 or 400 from AppView, got ${res.status}`, ); }); it('proxies to AppView for foreign repo without header', async () => { // Foreign DID without atproto-proxy header - should still proxy const res = await fetch( `${BASE}/xrpc/com.atproto.repo.getRecord?repo=did:plc:z72i7hdynmk6r22z27h6tvur&collection=app.bsky.feed.post&rkey=3juzlwllznd24`, ); // Should get response from AppView, not local 404 assert.ok( res.status === 200 || res.status === 400, `Expected 200 or 400 from AppView, got ${res.status}`, ); }); it('returns error for unknown proxy service', async () => { const res = await fetch( `${BASE}/xrpc/com.atproto.repo.getRecord?repo=did:plc:test&collection=test&rkey=test`, { headers: { 'atproto-proxy': 'did:web:unknown.service#unknown', }, }, ); assert.strictEqual(res.status, 400); const data = await res.json(); assert.ok(data.message.includes('Unknown proxy service')); }); it('returns local record for local DID without proxy header', async () => { // Create a record first const { data: created } = await jsonPost( '/xrpc/com.atproto.repo.createRecord', { repo: DID, collection: 'app.bsky.feed.post', record: { $type: 'app.bsky.feed.post', text: 'Test post for local DID test', createdAt: new Date().toISOString(), }, }, { Authorization: `Bearer ${token}` }, ); // Fetch without proxy header - should get local record const rkey = created.uri.split('/').pop(); const res = await fetch( `${BASE}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=app.bsky.feed.post&rkey=${rkey}`, ); assert.strictEqual(res.status, 200); const data = await res.json(); assert.ok(data.value.text.includes('Test post for local DID test')); }); it('describeRepo proxies for foreign DID', async () => { const res = await fetch( `${BASE}/xrpc/com.atproto.repo.describeRepo?repo=did:plc:z72i7hdynmk6r22z27h6tvur`, ); // Should get response from AppView assert.ok(res.status === 200 || res.status === 400); }); it('listRecords proxies for foreign DID', async () => { const res = await fetch( `${BASE}/xrpc/com.atproto.repo.listRecords?repo=did:plc:z72i7hdynmk6r22z27h6tvur&collection=app.bsky.feed.post&limit=1`, ); // Should get response from AppView assert.ok(res.status === 200 || res.status === 400); }); }); ``` **Step 2: Run the tests** ```bash npm test ``` Expected: All tests pass **Step 3: Commit** ```bash git add test/e2e.test.js git commit -m "test: add e2e tests for foreign DID proxying" ``` --- ### Task 8: Manual Verification **Step 1: Deploy to dev** ```bash npx wrangler deploy ``` **Step 2: Test with the original failing curl (with header)** ```bash curl 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.getRecord?collection=app.bsky.feed.post&repo=did%3Aplc%3Abcgltzqazw5tb6k2g3ttenbj&rkey=3mbx6iyfqps2c' \ -H 'atproto-proxy: did:web:api.bsky.app#bsky_appview' ``` Expected: Returns post data from AppView **Step 3: Test without header (foreign repo detection)** ```bash curl 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.getRecord?collection=app.bsky.feed.post&repo=did%3Aplc%3Abcgltzqazw5tb6k2g3ttenbj&rkey=3mbx6iyfqps2c' ``` Expected: Also returns post data from AppView (detected as foreign DID) **Step 4: Test replying to a post in Bluesky client** Verify the original issue is fixed. --- ## Future Enhancements 1. **Service auth for proxied requests** - Add service JWT when proxying authenticated requests 2. **DID resolution** - Resolve unknown DIDs to find their service endpoints dynamically 3. **Caching** - Cache registered DIDs list to avoid repeated lookups --- ## Summary | Task | Description | |------|-------------| | 1 | Add `parseAtprotoProxyHeader` utility | | 2 | Add `getKnownServiceUrl` utility | | 3 | Add `proxyToService` utility | | 4 | Add `isLocalDid` helper | | 5 | Refactor `handleAppViewProxy` to use shared utility | | 6 | Handle `atproto-proxy` header AND foreign `repo` param | | 7 | Add e2e tests | | 8 | Manual verification |