# Client Implementation Strategy ## Purpose This document outlines the design philosophy and implementation strategy for the ATP Keyserver client library. It serves as a roadmap for contributors and explains key architectural decisions. For protocol details, see [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md). For security guidelines, see [SECURITY.md](./SECURITY.md). For usage instructions, see [Client README](../packages/client/README.md). ## Design Philosophy ### Client-Side Encryption **Decision:** Cryptographic operations (encryption/decryption) happen on the client, not the server. **Rationale:** - **True end-to-end encryption**: Server never sees plaintext - **Better performance**: <1ms local crypto vs 100-500ms server round-trip - **Lower server load**: 50% less CPU, 97% less bandwidth - **Aligned with ATProto**: Decentralized architecture, user sovereignty - **Cost efficiency**: Scales with user devices, not server capacity **Trade-offs:** - Clients must handle cryptography correctly - Requires distributing crypto library (~52KB) - Key caching responsibility on client **Alternative considered:** Server-side encryption (rejected due to trust model and performance) ### Single Package Architecture **Decision:** One npm package `@atpkeyserver/client` with optional imports via subpath exports. **Rationale:** - Crypto library (@noble/ciphers) is ~52KB and represents 90% of bundle - High-level client adds only ~8KB on top of crypto functions - Maintenance overhead of separate packages not justified by small size difference - Subpath exports allow tree-shaking for minimal bundle impact **Package structure:** ``` @atpkeyserver/client ├── /crypto → Just crypto functions (52KB) ├── /client → Just KeyserverClient (60KB, includes crypto) └── / (main) → Both exports (60KB, tree-shaken) ``` **Alternative considered:** Separate `@atpkeyserver/client-crypto` and `@atpkeyserver/client` packages (rejected due to minimal size benefit) ### Service Auth Integration **Decision:** Client library abstracts service auth token management via callback pattern. **Rationale:** - PDS clients vary by platform (@atproto/api, custom implementations) - Callback pattern allows any PDS client integration - Client library handles token caching automatically - Separates concerns: client lib = crypto + keyserver API, user provides = PDS auth **Implementation:** ```typescript new KeyserverClient({ keyserverDid: 'did:web:keyserver.example.com', getServiceAuthToken: async (aud, lxm) => { // User provides: obtain token from their PDS client return await pdsClient.getServiceAuth({ aud, lxm }) } }) ``` See [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md#authentication-flow) for service auth details. ## Component Architecture ### Core Components **1. Crypto Module (`crypto.ts`)** - XChaCha20-Poly1305 encryption/decryption - Uses @noble/ciphers library - Stateless, pure functions - No network dependencies **2. KeyserverClient (`client.ts`)** - HTTP client for keyserver API - Service auth token management - Key caching with TTL - Error handling and retries **3. Cache Module (`cache.ts`)** - In-memory LRU cache - Separate TTLs for active/historical keys - Service auth token caching - Automatic expiry **4. Service Auth Module (`service-auth.ts`)** - Token cache management - Expiry checking with safety margin - Automatic refresh logic **5. Types Module (`types.ts`)** - TypeScript interfaces - Configuration types - Response types **6. Errors Module (`errors.ts`)** - Custom error classes - Structured error handling - HTTP status code mapping ### Dependency Graph ``` index.ts ├── crypto.ts │ └── @noble/ciphers ├── client.ts │ ├── crypto.ts │ ├── cache.ts │ ├── service-auth.ts │ ├── types.ts │ └── errors.ts ├── types.ts └── errors.ts ``` ## Key Features ### Automatic Caching **Key Caching:** - Active keys: 1 hour TTL (may change with rotation) - Historical keys: 24 hour TTL (immutable) - LRU eviction when max size reached **Service Auth Token Caching:** - 60 second TTL (or server-specified `exp`) - Refresh when <10 seconds remain - Memory-only storage **Benefits:** - Reduces keyserver load - Improves client performance - Handles token refresh automatically See [SECURITY.md](./SECURITY.md#token-caching) for security considerations. ### Request Deduplication Multiple simultaneous requests for the same key are collapsed into a single network request: ```typescript // Both calls trigger only one network request const [key1, key2] = await Promise.all([ client.getGroupKey('did:plc:abc#followers'), client.getGroupKey('did:plc:abc#followers') ]) ``` ### Automatic Retry Network failures retry with exponential backoff: - Retry on: 5xx errors, network timeouts - Don't retry on: 4xx errors (client errors are permanent) - Max retries: 3 attempts - Backoff: 100ms, 200ms, 400ms ### Type Safety Full TypeScript support: - Strict typing for all APIs - IntelliSense support - Compile-time error detection - Generated type definitions ## Bundle Size Analysis Tree-shaking eliminates unused code. Actual bundle sizes depend on what you import: ### Scenario 1: Crypto Functions Only ```typescript import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto' ``` **Bundled:** ~52KB (minified + gzipped ~15KB) - @noble/ciphers: 50KB - crypto.ts: 2KB **Use case:** You handle keyserver API calls manually, just need crypto. ### Scenario 2: Full Client ```typescript import { KeyserverClient } from '@atpkeyserver/client' ``` **Bundled:** ~60KB (minified + gzipped ~18KB) - @noble/ciphers: 50KB - Client code: 10KB (includes crypto, caching, HTTP, errors) **Use case:** Full-featured client with automatic caching and token management. ### Scenario 3: Both ```typescript import { KeyserverClient, encryptMessage } from '@atpkeyserver/client' ``` **Bundled:** ~60KB (same as scenario 2) - Client includes crypto, no duplication **Use case:** Mix high-level and low-level APIs as needed. ### Bundle Optimization **Automatic:** - Tree-shaking removes unused exports - Minification reduces code size - Gzip compresses for network transfer **Manual optimization:** - Use subpath imports (`/crypto`, `/client`) for smaller bundles - Import only what you need - Modern bundlers (webpack, vite, rollup) handle this automatically **Conclusion:** Crypto library dominates bundle size. Single package vs separate packages saves only ~8KB, not worth maintenance complexity. ## Implementation Phases ### Phase 1: Foundation (Complete) - [x] Crypto functions (encryptMessage, decryptMessage) - [x] Type definitions - [x] Package structure with subpath exports - [x] Build configuration (ESM + CJS) ### Phase 2: Client Library (Complete) - [x] KeyserverClient class - [x] Service auth token management - [x] Key caching with TTL - [x] HTTP client with retry logic - [x] Error handling - [x] Request deduplication ### Phase 3: Documentation (Complete) - [x] Protocol specification (ENCRYPTION_PROTOCOL.md) - [x] Security guidelines (SECURITY.md) - [x] API reference (API_REFERENCE.md) - [x] Client README with usage examples - [x] Minimal example implementation (basic-usage.ts) ### Phase 4: Testing - [ ] Unit tests for crypto functions - [ ] Integration tests for client - [ ] Mock keyserver for testing - [ ] E2E encryption flow test - [ ] Cache behavior tests - [ ] Service auth token refresh tests ### Phase 5: Polish - [ ] Performance benchmarks - [ ] Bundle size analysis - [ ] Documentation review - [ ] Example applications - [ ] npm package publication ## Success Metrics ### Performance Targets | Operation | Target | Measurement | |-----------|--------|-------------| | Encryption | <1ms | Local crypto operation | | Decryption (cached key) | <5ms | Cache lookup + crypto | | Decryption (cache miss) | <200ms | Network fetch + crypto | | Service auth (cached) | <1ms | Cache lookup | | Service auth (cache miss) | <100ms | PDS round-trip | ### Developer Experience - [ ] Complete TypeScript types - [ ] Comprehensive JSDoc comments - [ ] Working examples for 3+ platforms - [ ] <5 minutes from install to first encrypted message - [ ] Clear error messages with actionable guidance ### Security - [ ] Zero plaintext exposure to server - [ ] Keys cached in memory only - [ ] Service auth tokens cached for max 60 seconds - [ ] Proper cleanup on logout - [ ] No key or token leakage in logs - [ ] Audience binding prevents token misuse - [ ] Short-lived tokens limit attack window ### Code Quality - [ ] >80% test coverage - [ ] Zero runtime dependencies beyond @noble/ciphers - [ ] Tree-shakeable exports - [ ] CommonJS + ESM support - [ ] Works in Node.js, browser, React Native ## Testing Strategy ### Unit Tests **Crypto module:** - Encrypt/decrypt round-trip - Nonce uniqueness - AAD binding - Error handling **Cache module:** - TTL expiry - LRU eviction - Get/set operations - Clear operation **Service auth module:** - Token expiry checking - Refresh logic - Cache key generation ### Integration Tests **Client:** - Key fetch with caching - Service auth integration - Retry logic - Error handling **E2E:** - Full encryption flow (PDS → keyserver → encrypt → store) - Full decryption flow (fetch → keyserver → decrypt → display) - Key rotation handling ### Mock Server Build lightweight mock keyserver for testing: - Simulates auth verification - Returns mock keys - Configurable latency - Error injection ## Open Questions ### Rate Limiting **Question:** Should client enforce rate limits? **Options:** 1. No client-side limits (rely on server) 2. Simple debouncing (prevent rapid duplicate requests) 3. Full rate limit tracking (mirror server limits) **Current decision:** Option 2 (debouncing via request deduplication) ### Cache Persistence **Question:** Should keys be persisted to disk? **Options:** 1. Memory-only (current implementation) 2. Encrypted disk cache (requires key derivation) 3. Platform-specific secure storage (iOS Keychain, Android KeyStore) **Current decision:** Memory-only for security and simplicity **Rationale:** - Simpler implementation - Better security (no disk persistence) - Works across all platforms - User can implement custom cache if needed ### Error Recovery **Question:** How should clients handle persistent decryption failures? **Options:** 1. Fail silently (hide post) 2. Show error message 3. Retry with exponential backoff 4. Prompt user action (e.g., "Request access") **Current decision:** Show error message, no retry (decryption failures are permanent) ## Related Documentation - [Encryption Protocol](./ENCRYPTION_PROTOCOL.md) - Protocol specification - [Security Best Practices](./SECURITY.md) - Security guidelines - [API Reference](./API_REFERENCE.md) - Complete endpoint reference - [Architecture](./ARCHITECTURE.md) - Server architecture - [Client README](../packages/client/README.md) - Usage instructions ## Contributing Contributions welcome! Please: 1. Read existing documentation 2. Follow code style (Prettier) 3. Add tests for new features 4. Update documentation as needed 5. Submit PR with clear description For questions, open an issue in the [Codeberg repository](https://codeberg.org/juandjara/atp-keyserver).