Skip to content

feat: implement XRPC service proxying per spec#28

Merged
ascorbic merged 12 commits into
mainfrom
claude/fix-xrpc-proxying-J507e
Dec 30, 2025
Merged

feat: implement XRPC service proxying per spec#28
ascorbic merged 12 commits into
mainfrom
claude/fix-xrpc-proxying-J507e

Conversation

@ascorbic
Copy link
Copy Markdown
Owner

Implement proper XRPC service proxying according to the atproto spec:
https://atproto.com/specs/xrpc#service-proxying

Changes:

  • Add DID resolution utilities for did:web and did:plc
  • Parse atproto-proxy header (format: "did:web:example.com#service_id")
  • Resolve DID documents and extract service endpoints
  • Route requests to the specified service endpoint
  • Maintain backward compatibility with hardcoded Bluesky routing

The implementation:

  1. Checks for atproto-proxy header
  2. Resolves the DID document from the specified DID
  3. Extracts the service endpoint for the specified service ID
  4. Forwards the request to that endpoint with a service JWT
  5. Falls back to api.bsky.app/api.bsky.chat if no header present

Tests:

  • 11 unit tests for DID resolver utilities
  • 7 integration tests for proxy behavior
  • All existing 126 tests continue to pass (137 total)

Implement proper XRPC service proxying according to the atproto spec:
https://atproto.com/specs/xrpc#service-proxying

Changes:
- Add DID resolution utilities for did:web and did:plc
- Parse atproto-proxy header (format: "did:web:example.com#service_id")
- Resolve DID documents and extract service endpoints
- Route requests to the specified service endpoint
- Maintain backward compatibility with hardcoded Bluesky routing

The implementation:
1. Checks for atproto-proxy header
2. Resolves the DID document from the specified DID
3. Extracts the service endpoint for the specified service ID
4. Forwards the request to that endpoint with a service JWT
5. Falls back to api.bsky.app/api.bsky.chat if no header present

Tests:
- 11 unit tests for DID resolver utilities
- 7 integration tests for proxy behavior
- All existing 126 tests continue to pass (137 total)
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 29, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
atproto-pds 4341334 Dec 30 2025, 08:10 AM

claude and others added 11 commits December 29, 2025 20:30
Update proxy tests to handle different network conditions:
- Local test environment: DNS lookups fail, fetch returns 500
- GitHub Actions: DNS lookups succeed, may get 401 or other responses

Tests now verify the core behavior (proxying works) rather than
specific status codes that vary by environment.
Replace environment-dependent network tests with deterministic mocked tests:
- Mock DID document resolution
- Mock proxied service responses
- Verify service JWT creation and forwarding
- Add comprehensive test for successful proxy flow

Benefits:
- Tests work consistently in all environments (local, CI, etc.)
- No network dependencies
- Faster test execution
- More thorough validation of proxy behavior

Test count: 138 total (added 1 new test for valid proxy flow)
Security and performance improvements based on @bluesky-social/atproto patterns:

**DID Document Caching**
- Add 1-hour TTL cache for DID document lookups
- Limit cache size to 1000 entries with LRU eviction
- Reduces network overhead for repeated proxying to same services

**URL Construction & Validation**
- Use URL constructor for safe URL building (prevent injection)
- Simplified URL validation matching @atproto/common-web approach
- Allow both HTTP and HTTPS (for local development)
- Validate URLs can be parsed

**Service Endpoint Lookup**
- Align extractServiceEndpoint with @atproto/common-web/did-doc.ts
- Optimize service ID matching (hot path optimization)
- Support both #fragment and did#fragment formats

**Special-case Main Services**
- Cache api.bsky.app and api.bsky.chat endpoints
- Avoid DID lookups for known Bluesky services
- Still validate service IDs exist

**Header Security**
- Strip sensitive headers before proxying:
  - authorization (replaced with service JWT)
  - cookie (privacy - don't leak cookies)
  - x-forwarded-for/x-real-ip (don't leak client IP)
  - atproto-proxy (internal routing)
  - connection, host (connection-specific)

**Path Validation**
- Prevent path traversal in XRPC method names
- Reject .. and // in lexicon method paths

**Tests**
- Add 9 URL validation tests
- Update existing tests for new return types
- All 147 tests passing

Based on patterns from: @bluesky-social/atproto/packages/pds/src/pipethrough.ts
and @bluesky-social/atproto/packages/common-web/src/did-doc.ts
Replace custom extractServiceEndpoint and validateServiceUrl
implementations with official @atproto/common-web getServiceEndpoint.

- Import getServiceEndpoint from @atproto/common-web
- Remove custom implementation from src/did-resolver.ts
- Re-export official DidDocument type
- Update all tests to use official API
- All 147 tests passing

This ensures we stay aligned with the official atproto implementation
and benefit from their tested and proven code.
Replace custom DID resolution implementation with the official
@atproto/identity package for better reliability and maintenance.

Changes:
- Add @atproto/identity dependency (^0.4.10)
- Create WorkersDidCache using Cloudflare Cache API
- Replace custom resolveDidDocument with DidResolver
- Remove redundant DID resolution code from did-resolver.ts
- Update tests to handle URL objects in fetch mocks
- Import DidDocument type from @atproto/common-web

Benefits:
- Battle-tested official implementation
- Proper caching with stale/fresh semantics
- Timeout protection (3s default)
- Aligned with Bluesky's proven patterns
- Persistent caching via Cloudflare Cache API

All 147 tests passing.
Extract proxy handling from index.ts into a clean, focused module.

Changes:
- Create src/xrpc-proxy.ts with handleXrpcProxy function
- Move parseProxyHeader to xrpc-proxy.ts (better location)
- Delete src/did-resolver.ts (no longer needed)
- Update index.ts to use handleXrpcProxy handler
- Update tests to import from xrpc-proxy.ts

Benefits:
- Cleaner separation of concerns
- index.ts is now 265 lines (was 447)
- Proxy logic is self-contained and testable
- parseProxyHeader lives with proxy code where it belongs

All 147 tests passing.
- Add null check for payload.sub in xrpc-proxy
- Improve parseProxyHeader validation with optional chaining
- Fix array destructuring type inference in service-auth
@atproto/identity uses `redirect: "error"` which Cloudflare Workers
doesn't support (only "follow" or "manual"). Created custom DidResolver
that uses `redirect: "manual"` and checks for redirect status codes.

Uses official @atproto/common-web schema validation for DID documents.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Import waitUntil from cloudflare:workers for background cache refresh
- Validate cached DID documents using schema check on retrieval
- Clear invalid cache entries automatically

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ascorbic ascorbic merged commit e34a6e4 into main Dec 30, 2025
3 checks passed
@ascorbic ascorbic deleted the claude/fix-xrpc-proxying-J507e branch December 30, 2025 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants