···43434444## Documentation
45454646-- [Architecture](./docs/ARCHITECTURE.md) - Complete server architecture
4747-- [Client Implementation](./docs/CLIENT_IMPLEMENTATION.md) - Client library guide
4848-- API Reference - Coming soon
4949-- Encryption Protocol - Coming soon
5050-- Security Best Practices - Coming soon
4646+### Getting Started
4747+- [Client README](./packages/client/README.md) - Quick start for using the client library
4848+- [Server README](./packages/server/README.md) - Server deployment guide
4949+5050+### Technical Documentation
5151+- [Encryption Protocol](./docs/ENCRYPTION_PROTOCOL.md) - End-to-end encryption protocol specification
5252+- [API Reference](./docs/API_REFERENCE.md) - Complete API endpoint reference
5353+- [Architecture](./docs/ARCHITECTURE.md) - Server-side architecture and design
5454+- [Security Best Practices](./docs/SECURITY.md) - Security guidelines and threat model
5555+5656+### Implementation
5757+- [Client Implementation Strategy](./docs/CLIENT_IMPLEMENTATION.md) - Client library design decisions
51585259## Requirements
5360
+123-378
docs/ARCHITECTURE.md
···11# ATP Keyserver Architecture
2233+## Documentation Guide
44+55+This document focuses on **server-side architecture**. For other topics:
66+77+- **End-to-end encryption protocol**: See [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md)
88+- **Security best practices**: See [SECURITY.md](./SECURITY.md)
99+- **Complete API reference**: See [API_REFERENCE.md](./API_REFERENCE.md)
1010+- **Client library usage**: See [Client README](../packages/client/README.md)
1111+- **Implementation strategy**: See [CLIENT_IMPLEMENTATION.md](./CLIENT_IMPLEMENTATION.md)
1212+1313+314## Overview
415516A specialized AT Protocol (ATProto) service for secure storage and distribution of cryptographic keys. This keyserver manages both asymmetric keypairs (Ed25519) for public key cryptography and symmetric keys (XChaCha20-Poly1305) for group-based encrypted communication.
6171818+**Key principle:** The server manages keys but never performs encryption/decryption. All cryptographic operations happen client-side for true end-to-end encryption.
1919+2020+721## Technology Stack
822923### Runtime & Core
···2034- `@noble/ed25519` - Ed25519 asymmetric key operations
2135- `@noble/ciphers` - XChaCha20-Poly1305 symmetric encryption
22363737+2338## Architecture Components
24392540### 1. Entry Point (`main.ts`)
···33483449### 2. Database Layer (`lib/db.ts`)
35503636-SQLite database with three normalized tables:
5151+SQLite database with four tables supporting key versioning and access logging:
37523853```sql
5454+-- Asymmetric keypairs (Ed25519)
3955keys
4040-├── did (PRIMARY KEY)
4141-├── public_key (TEXT)
4242-└── private_key (TEXT)
5656+├── did (TEXT, NOT NULL)
5757+├── version (INTEGER, NOT NULL, DEFAULT 1)
5858+├── public_key (TEXT, NOT NULL)
5959+├── private_key (TEXT, NOT NULL)
6060+├── created_at (TEXT, NOT NULL)
6161+├── revoked_at (TEXT, nullable)
6262+├── status (TEXT, NOT NULL, DEFAULT 'active')
6363+├── status_comment (TEXT, nullable)
6464+└── PRIMARY KEY (did, version)
6565+ INDEX idx_keys_did_status ON (did, status)
43666767+-- Symmetric group keys (XChaCha20-Poly1305)
4468groups
4545-├── id (PRIMARY KEY)
4646-├── owner_did (TEXT)
4747-└── secret_key (TEXT)
6969+├── id (TEXT, NOT NULL)
7070+├── version (INTEGER, NOT NULL, DEFAULT 1)
7171+├── owner_did (TEXT, NOT NULL)
7272+├── secret_key (TEXT, NOT NULL)
7373+├── created_at (TEXT, NOT NULL)
7474+├── revoked_at (TEXT, nullable)
7575+├── status (TEXT, NOT NULL, DEFAULT 'active')
7676+├── status_comment (TEXT, nullable)
7777+└── PRIMARY KEY (id, version)
7878+ INDEX idx_groups_id_status ON (id, status)
48798080+-- Group membership
4981group_members
5050-├── group_id (FOREIGN KEY → groups.id)
5151-└── member_did (TEXT)
5252-```
8282+├── group_id (TEXT, NOT NULL)
8383+├── member_did (TEXT, NOT NULL)
8484+└── PRIMARY KEY (group_id, member_did)
8585+ FOREIGN KEY (group_id) REFERENCES groups(id)
53865454-**Features:**
5555-- WAL (Write-Ahead Logging) mode enabled for better concurrency
5656-- Type-safe schema definitions using TypeScript
5757-- Single database file (`keyserver.db`)
5858-- Exported DBSchema type for type-safe queries throughout the application
8787+-- Key access audit log
8888+key_access_log
8989+├── id (INTEGER, PRIMARY KEY AUTOINCREMENT)
9090+├── did (TEXT, NOT NULL)
9191+├── version (INTEGER, NOT NULL)
9292+├── accessed_at (TEXT, NOT NULL)
9393+├── ip (TEXT, nullable)
9494+└── user_agent (TEXT, nullable)
9595+```
59966097### 3. Authentication Middleware (`lib/authMiddleware.ts`)
6198···127164- `checkMembership(group_id, member_did)` - Verify member status
128165- `createGroup(group_id, owner_did)` - Create new group with secret key
129166167167+130168## API Endpoints
131169170170+The keyserver exposes the following endpoint categories:
171171+132172### Public Endpoints (No Authentication)
173173+- `GET /` - Service metadata
174174+- `GET /.well-known/did.json` - Service DID document
175175+- `GET /xrpc/dev.atpkeyserver.alpha.key.public` - Get any user's public key
133176134134-#### `GET /`
135135-Returns service metadata from package.json:
136136-```json
137137-{
138138- "name": "atp-keyserver",
139139- "version": "1.0.0"
140140-}
141141-```
177177+### Protected Endpoints (Require Service Auth Token)
142178143143-#### `GET /.well-known/did.json`
144144-Returns service DID document per AT Protocol specification:
145145-```json
146146-{
147147- "@context": ["https://www.w3.org/ns/did/v1"],
148148- "id": "{service-did}",
149149- "service": [
150150- {
151151- "id": "#atp_keyserver",
152152- "type": "AtpKeyserver",
153153- "serviceEndpoint": "{service-url}"
154154- }
155155- ]
156156-}
157157-```
179179+**Personal Key Management:**
180180+- `GET /xrpc/dev.atpkeyserver.alpha.key` - Get personal keypair
181181+- `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` - Rotate personal keypair
182182+- `GET /xrpc/dev.atpkeyserver.alpha.key.versions` - List key versions
183183+- `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` - Get access logs
158184159159-#### `GET /xrpc/dev.atpkeyserver.alpha.key.public?did={did}`
160160-Retrieve any user's public key without authentication.
185185+**Group Key Management:**
186186+- `GET /xrpc/dev.atpkeyserver.alpha.group.key` - Get group key
187187+- `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` - Rotate group key (owner only)
188188+- `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` - List group key versions
161189162162-**Query Parameters:**
163163-- `did` (required) - DID of user (must be `did:web:*` or `did:plc:*`)
190190+**Group Membership Management:**
191191+- `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` - Add member (owner only)
192192+- `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` - Remove member (owner only)
164193165165-**Response:**
166166-```json
167167-{
168168- "publicKey": "hex-encoded-public-key"
169169-}
170170-```
171171-172172-**Validation:**
173173-- DID format validation using `isDid()`
174174-- Only supports `did:web` and `did:plc` methods
175175-- URL-decodes the DID parameter
176176-177177-### Protected Endpoints (Require JWT Authentication)
178178-179179-#### `GET /xrpc/dev.atpkeyserver.alpha.key`
180180-Retrieve the authenticated user's complete keypair.
181181-182182-**Headers:**
183183-- `Authorization: Bearer {jwt}`
184184-185185-**Response:**
186186-```json
187187-{
188188- "publicKey": "hex-encoded-public-key",
189189- "privateKey": "hex-encoded-private-key"
190190-}
191191-```
192192-193193-**Implementation:** Calls `getKeypair(did)` from `lib/keys/asymmetric.ts` which returns or creates the user's Ed25519 keypair.
194194-195195-### Key Rotation & Revocation Endpoints
196196-197197-#### `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` (Authenticated)
198198-Rotate the user's asymmetric keypair, marking the current version as revoked and generating a new version.
199199-200200-**Headers:**
201201-- `Authorization: Bearer {jwt}`
202202-203203-**Body:**
204204-```json
205205-{
206206- "reason": "suspected_compromise" | "routine_rotation" | "user_requested"
207207-}
208208-```
209209-210210-**Response:**
211211-```json
212212-{
213213- "oldVersion": 1,
214214- "newVersion": 2,
215215- "rotatedAt": "2025-01-23T10:30:00.000Z"
216216-}
217217-```
218218-219219-#### `GET /xrpc/dev.atpkeyserver.alpha.key.versions` (Authenticated)
220220-List all versions of the user's keypair with their status.
221221-222222-**Headers:**
223223-- `Authorization: Bearer {jwt}`
224224-225225-**Response:**
226226-```json
227227-{
228228- "versions": [
229229- {
230230- "version": 2,
231231- "status": "active",
232232- "created_at": "2025-01-23T10:30:00.000Z",
233233- "revoked_at": null
234234- },
235235- {
236236- "version": 1,
237237- "status": "revoked",
238238- "created_at": "2025-01-20T08:00:00.000Z",
239239- "revoked_at": "2025-01-23T10:30:00.000Z"
240240- }
241241- ]
242242-}
243243-```
244244-245245-#### `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` (Authenticated)
246246-Retrieve access logs for the authenticated user's keys.
247247-248248-**Headers:**
249249-- `Authorization: Bearer {jwt}`
250250-251251-**Query Parameters:**
252252-- `limit` (optional) - Maximum number of log entries to return (default: 50)
253253-254254-**Response:**
255255-```json
256256-{
257257- "logs": [
258258- {
259259- "version": 2,
260260- "accessed_at": "2025-01-23T10:30:00.000Z",
261261- "ip": "192.0.2.1",
262262- "user_agent": "Mozilla/5.0..."
263263- },
264264- {
265265- "version": 1,
266266- "accessed_at": "2025-01-22T08:15:00.000Z",
267267- "ip": "192.0.2.1",
268268- "user_agent": "Mozilla/5.0..."
269269- }
270270- ]
271271-}
272272-```
273273-274274-**Note:** IP and user_agent fields may be null if not available from the request headers.
275275-276276-#### `GET /xrpc/dev.atpkeyserver.alpha.group.key` (Authenticated)
277277-Get a group's symmetric key (supports version parameter for historical keys).
278278-279279-**Headers:**
280280-- `Authorization: Bearer {jwt}`
281281-282282-**Query Parameters:**
283283-- `group_id` (required) - Group identifier (format: `{owner_did}#{group_name}`)
284284-- `version` (optional) - Specific version number
285285-286286-**Response:**
287287-```json
288288-{
289289- "groupId": "did:plc:abc123#followers",
290290- "secretKey": "hex-encoded-secret-key",
291291- "version": 1
292292-}
293293-```
294294-295295-#### `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` (Owner Only, Authenticated)
296296-Rotate a group's symmetric key (only the group owner can perform this action).
297297-298298-**Headers:**
299299-- `Authorization: Bearer {jwt}`
300300-301301-**Body:**
302302-```json
303303-{
304304- "group_id": "did:plc:abc123#followers",
305305- "reason": "suspected_compromise"
306306-}
307307-```
308308-309309-**Response:**
310310-```json
311311-{
312312- "groupId": "did:plc:abc123#followers",
313313- "oldVersion": 1,
314314- "newVersion": 2,
315315- "rotatedAt": "2025-01-23T10:30:00.000Z"
316316-}
317317-```
318318-319319-#### `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` (Authenticated)
320320-List all versions of a group key (owner and members can view).
321321-322322-**Headers:**
323323-- `Authorization: Bearer {jwt}`
324324-325325-**Query Parameters:**
326326-- `group_id` (required)
327327-328328-**Response:**
329329-```json
330330-{
331331- "groupId": "did:plc:abc123#followers",
332332- "versions": [
333333- {
334334- "version": 2,
335335- "status": "active",
336336- "created_at": "2025-01-23T10:30:00.000Z",
337337- "revoked_at": null
338338- },
339339- {
340340- "version": 1,
341341- "status": "revoked",
342342- "created_at": "2025-01-20T08:00:00.000Z",
343343- "revoked_at": "2025-01-23T10:30:00.000Z"
344344- }
345345- ]
346346-}
347347-```
348348-349349-#### `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` (Owner Only, Authenticated)
350350-Add a member to a group (only the group owner can perform this action).
351351-352352-**Headers:**
353353-- `Authorization: Bearer {jwt}`
354354-355355-**Body:**
356356-```json
357357-{
358358- "group_id": "did:plc:abc123#followers",
359359- "member_did": "did:plc:xyz789"
360360-}
361361-```
362362-363363-**Response:**
364364-```json
365365-{
366366- "groupId": "did:plc:abc123#followers",
367367- "memberDid": "did:plc:xyz789",
368368- "status": "added"
369369-}
370370-```
371371-372372-**Error Responses:**
373373-- `404` - Group not found
374374-- `403` - Only owner can add members
375375-- `409` - Member already in group
376376-377377-#### `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` (Owner Only, Authenticated)
378378-Remove a member from a group (only the group owner can perform this action).
379379-380380-**Headers:**
381381-- `Authorization: Bearer {jwt}`
382382-383383-**Body:**
384384-```json
385385-{
386386- "group_id": "did:plc:abc123#followers",
387387- "member_did": "did:plc:xyz789"
388388-}
389389-```
390390-391391-**Response:**
392392-```json
393393-{
394394- "groupId": "did:plc:abc123#followers",
395395- "memberDid": "did:plc:xyz789",
396396- "status": "removed"
397397-}
398398-```
399399-400400-**Error Responses:**
401401-- `404` - Group not found or member not found in group
402402-- `403` - Only owner can remove members
194194+**For complete endpoint documentation including request/response formats, parameters, error codes, and examples, see [API_REFERENCE.md](./API_REFERENCE.md).**
403195404196## Key Versioning & Revocation
405197···417209418210**Key rotation limits damage radius:** When a key leaks, rotation prevents new posts from being decrypted, while old posts remain readable to authorized parties.
419211420420-### Database Schema
421421-422422-Both `keys` and `groups` tables use versioning:
423423-424424-```sql
425425--- Asymmetric keys
426426-CREATE TABLE keys (
427427- did TEXT NOT NULL,
428428- version INTEGER NOT NULL DEFAULT 1,
429429- public_key TEXT NOT NULL,
430430- private_key TEXT NOT NULL,
431431- created_at TEXT NOT NULL,
432432- revoked_at TEXT,
433433- status TEXT NOT NULL DEFAULT 'active',
434434- PRIMARY KEY (did, version)
435435-);
436436-CREATE INDEX idx_keys_did_status ON keys(did, status);
437437-438438--- Symmetric group keys
439439-CREATE TABLE groups (
440440- id TEXT NOT NULL,
441441- version INTEGER NOT NULL DEFAULT 1,
442442- owner_did TEXT NOT NULL,
443443- secret_key TEXT NOT NULL,
444444- created_at TEXT NOT NULL,
445445- revoked_at TEXT,
446446- status TEXT NOT NULL DEFAULT 'active',
447447- PRIMARY KEY (id, version)
448448-);
449449-CREATE INDEX idx_groups_id_status ON groups(id, status);
450450-451451--- Access logging
452452-CREATE TABLE key_access_log (
453453- id INTEGER PRIMARY KEY AUTOINCREMENT,
454454- did TEXT NOT NULL,
455455- version INTEGER NOT NULL,
456456- accessed_at TEXT NOT NULL,
457457- ip TEXT,
458458- user_agent TEXT
459459-);
460460-```
461461-462212### Key Lifecycle
463213464214**Status Values:**
465215- `active` - Current version, used for encryption
466466-- `revoked` - Compromised or rotated out, still available for decryption
467467-- `rotated` - Replaced during routine rotation (reserved for future use)
216216+- `revoked` - No longer active (compromised or rotated out), still available for decryption
468217469218**Default Behavior:**
470219- New keys start as version 1 with status `active`
···537286- Audit compliance requirements
538287- Incident response investigation
539288540540-**API Access:**
289289+**API Access to logs:**
541290Users can retrieve access logs for their keys via the authenticated `/xrpc/dev.atpkeyserver.alpha.key.accessLog` endpoint. This allows users to:
542291- Monitor their own key usage
543292- Detect unauthorized access attempts
···564313- **Member Verification**: Key access validated against membership table
565314- **Namespace Isolation**: Group IDs scoped by owner DID
566315316316+For more insight on the security model, see [SECURITY.md](SECURITY.md)
317317+318318+567319## Configuration
568320569569-### Environment Variables
321321+There is not much to configure yet in this first version. The only thing would be the DID of the service and the port this service is running on, which are configured with environment variables as described here:
322322+570323- `DID` (required) - Service's own DID identifier
571324- `PORT` (optional) - HTTP server port (default: 4000)
572325573573-### Database
574574-- File: `keyserver.db`
575575-- Mode: WAL (Write-Ahead Logging)
576576-- Auto-created on first run
577326578327## Development Workflow
579328580580-### Scripts
581581-- `bun run dev` - Development mode with auto-reload (uses `DID=test`)
582582-- `bun start` - Production mode
583583-- `bun run format` - Format code with Prettier
329329+To create a new endpoint for the server, the development workflow would be something like this:
584330585585-### Dependencies
586586-- Uses Bun's native package management
587587-- Lock file: `bun.lock`
588588-- Separate dev dependencies for tooling
331331+1. Define the endpoint route in `main.ts` and decide whether it requires authentication or not.
332332+2. Implement the new endpoint inner logic in a file in the `lib` folder.
333333+3. Create a lexicon definition file for the new endpoint in the `lexicons` folder.
334334+4. Update the related documentation about API endpoints.
589335590336## Design Patterns
591337···607353- URL-safe without additional encoding
608354- Human-readable for debugging
609355610610-### Type-Safe Database Queries
611611-Database schema exported as TypeScript types:
612612-- Compile-time type checking for queries
613613-- IntelliSense support in IDEs
614614-- Reduces runtime errors
615356616357## Project Structure
617358618359```
619619-atp-keyserver/
620620-├── main.ts # Entry point and routing
621621-├── package.json # Dependencies and scripts
622622-├── bun.lock # Dependency lock file
623623-├── README.md # Project documentation
624624-├── CLAUDE.md # Architecture documentation (this file)
625625-├── keyserver.db # SQLite database (generated at runtime)
626626-├── lib/
627627-│ ├── db.ts # Database schema and connection
628628-│ ├── authMiddleware.ts # JWT authentication with DID resolution
629629-│ └── keys/
630630-│ ├── asymmetric.ts # Ed25519 keypair management with versioning
631631-│ ├── symmetric.ts # XChaCha20 encryption & group management with versioning
632632-│ └── access-log.ts # Key access logging for security auditing
633633-└── lexicons/
634634- └── dev/
635635- └── atpkeyserver/
636636- └── defs.json # XRPC lexicon definitions (placeholder)
360360+atp-keyserver/ # Monorepo root
361361+├── packages/
362362+│ ├── server/ # Keyserver service
363363+│ │ ├── main.ts # Entry point and routing
364364+│ │ ├── package.json # Server dependencies
365365+│ │ ├── tsconfig.json # Server TypeScript config
366366+│ │ ├── README.md # Server starting documentation
367367+│ │ ├── lib/
368368+│ │ │ ├── db.ts # Database schema and connection
369369+│ │ │ ├── authMiddleware.ts # JWT authentication with DID resolution
370370+│ │ │ └── keys/
371371+│ │ │ ├── asymmetric.ts # Ed25519 keypair management with versioning
372372+│ │ │ ├── symmetric.ts # XChaCha20 encryption & group management
373373+│ │ │ └── access-log.ts # Key access logging
374374+│ │ ├── lexicons/ # XRPC lexicon definitions
375375+│ │ │ └── ...
376376+│ │ └── keyserver.db # SQLite database (generated at runtime)
377377+│ └── client/ # Client library
378378+│ ├── src/
379379+│ │ ├── index.ts # Package entry point
380380+│ │ ├── crypto.ts # Encryption/decryption functions
381381+│ │ └── types.ts # TypeScript type definitions
382382+│ ├── dist/ # Compiled output (generated)
383383+│ ├── package.json # Client dependencies
384384+│ ├── tsconfig.json # ESM build config
385385+│ ├── tsconfig.cjs.json # CommonJS build config
386386+│ └── README.md # Client starting documentation
387387+├── docs/ # Centralized documentation
388388+│ ├── ARCHITECTURE.md # Server architecture (this file)
389389+│ ├── ENCRYPTION_PROTOCOL.md # E2E encryption protocol specification
390390+│ ├── SECURITY.md # Security best practices for new clients
391391+│ ├── API_REFERENCE.md # Complete API endpoint reference
392392+│ └── CLIENT_IMPLEMENTATION.md # Client library design decisions
393393+├── package.json # Workspace root configuration
394394+├── tsconfig.json # Base TypeScript configuration
395395+├── bun.lock # Dependency lock file
396396+├── README.md # Project overview
397397+└── LICENSE.md # License information
637398```
638399639400## Code Quality
···646407- Good use of AT Protocol standards
647408- Proper DID validation and method checking
648409- WAL mode enabled for SQLite (better concurrency)
649649-- Direct imports eliminate unnecessary module layers
650410651411### Considerations for Future Development
652412- Database error handling could be more comprehensive
···680440- **Group Key Rotation**: `/xrpc/dev.atpkeyserver.alpha.group.key.rotate` (owner only)
681441- **Version Listing**: View all key versions with status and timestamps
682442- **Specific Version Access**: Retrieve historical keys for decrypting old content
683683-- **Status Tracking**: Active, revoked, or rotated status per version
443443+- **Status Tracking**: Active or revoked status per version (with reason in metadata)
684444685445### Design Trade-offs
686446- **Backward Compatibility Over Forward Secrecy**: All versions retained to prevent data loss
···693453- Encrypted backup strategy for database
694454- Scheduled/automatic key rotation policies
695455- Enhanced anomaly detection based on access logs
696696-697697-## AT Protocol Integration
698698-699699-This keyserver implements a custom AT Protocol service type:
700700-701701-**Service Type**: `AtpKeyserver`
702702-**Lexicon Scope**: `dev.atpkeyserver.alpha`
703703-704704-The service follows AT Protocol conventions:
705705-- DID-based identity
706706-- JWT authentication with signing key verification
707707-- XRPC method naming (`/xrpc/{lexicon}.{method}`)
708708-- Service discovery via DID documents
709709-710710-This allows AT Protocol applications to discover and interact with the keyserver using standard DID resolution.
+293-663
docs/CLIENT_IMPLEMENTATION.md
···11-# Client-Side Encryption Implementation Plan
11+# Client Implementation Strategy
2233-## Overview
33+## Purpose
4455-This document outlines the implementation plan for client-side encryption/decryption of "followers-only" posts in the ATP keyserver ecosystem. The approach prioritizes end-to-end encryption, performance, and alignment with ATProto's decentralized philosophy.
55+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.
6677-## Recommended Solution: Client-Side with Key Caching
77+For protocol details, see [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md).
88+For security guidelines, see [SECURITY.md](./SECURITY.md).
99+For usage instructions, see [Client README](../packages/client/README.md).
81099-The keyserver's role is **key management and authorization**, not content processing. By keeping cryptographic operations client-side, we preserve the security model while maintaining better performance and scalability.
1111+## Design Philosophy
10121111-**Core Principle:** The server should only answer "Is this user authorized to decrypt this group's messages?" - it should never answer "What does this message say?"
1212-1313-## Key Benefits vs Server-Side Encryption
1313+### Client-Side Encryption
14141515-1. **Security**: True end-to-end encryption - server never sees plaintext
1616-2. **Performance**: Operations complete in <1ms vs 100-500ms for server round-trips
1717-3. **Scalability**: 50% less server load, 97% less bandwidth usage
1818-4. **Philosophy**: Aligns with ATProto's decentralized design principles
1919-5. **Cost**: Significantly lower server infrastructure costs
2020-6. **Privacy**: Users don't need to trust server operator with message content
1515+**Decision:** Cryptographic operations (encryption/decryption) happen on the client, not the server.
21162222-## Optimal Protocol
1717+**Rationale:**
1818+- **True end-to-end encryption**: Server never sees plaintext
1919+- **Better performance**: <1ms local crypto vs 100-500ms server round-trip
2020+- **Lower server load**: 50% less CPU, 97% less bandwidth
2121+- **Aligned with ATProto**: Decentralized architecture, user sovereignty
2222+- **Cost efficiency**: Scales with user devices, not server capacity
23232424-### Creating an Encrypted "Followers-Only" Post
2424+**Trade-offs:**
2525+- Clients must handle cryptography correctly
2626+- Requires distributing crypto library (~52KB)
2727+- Key caching responsibility on client
25282626-```typescript
2727-import { encryptMessage } from '@atpkeyserver/client/crypto'
2929+**Alternative considered:** Server-side encryption (rejected due to trust model and performance)
28302929-// 1. Define the group for followers-only visibility
3030-const groupId = `${authorDid}#followers`
3131+### Single Package Architecture
31323232-// 2. Fetch active group key from keyserver (with caching)
3333-let key = keyCache.get(groupId)
3434-if (!key) {
3535- const response = await fetch(
3636- `/xrpc/dev.atpkeyserver.alpha.group.key?group_id=${encodeURIComponent(groupId)}`,
3737- { headers: { Authorization: `Bearer ${jwt}` } }
3838- )
3939- const data = await response.json()
4040- key = { secretKey: data.secretKey, version: data.version }
4141- keyCache.set(groupId, key, { ttl: 3600 }) // Cache for 1 hour
4242-}
3333+**Decision:** One npm package `@atpkeyserver/client` with optional imports via subpath exports.
43344444-// 3. Encrypt the post content locally
4545-const post = { text: "Secret message for followers", createdAt: new Date() }
4646-const postUri = `at://${authorDid}/app.bsky.feed.post/${rkey}`
4747-const ciphertext = encryptMessage(postUri, key.secretKey, JSON.stringify(post))
3535+**Rationale:**
3636+- Crypto library (@noble/ciphers) is ~52KB and represents 90% of bundle
3737+- High-level client adds only ~8KB on top of crypto functions
3838+- Maintenance overhead of separate packages not justified by small size difference
3939+- Subpath exports allow tree-shaking for minimal bundle impact
48404949-// 4. Store encrypted content in PDS
5050-await pds.createRecord({
5151- collection: 'app.bsky.feed.post',
5252- record: {
5353- encrypted_content: ciphertext,
5454- key_version: key.version,
5555- encrypted_at: new Date().toISOString(),
5656- visibility: 'followers'
5757- }
5858-})
4141+**Package structure:**
5942```
6060-6161-### Decrypting a "Followers-Only" Post
4343+@atpkeyserver/client
4444+├── /crypto → Just crypto functions (52KB)
4545+├── /client → Just KeyserverClient (60KB, includes crypto)
4646+└── / (main) → Both exports (60KB, tree-shaken)
4747+```
62486363-```typescript
6464-import { decryptMessage } from '@atpkeyserver/client/crypto'
4949+**Alternative considered:** Separate `@atpkeyserver/client-crypto` and `@atpkeyserver/client` packages (rejected due to minimal size benefit)
65506666-// 1. Read encrypted post from feed
6767-const encryptedPost = await pds.getRecord(postUri)
6868-const { encrypted_content, key_version } = encryptedPost.value
6969-7070-// 2. Determine group ID from author
7171-const authorDid = postUri.split('/')[2]
7272-const groupId = `${authorDid}#followers`
5151+### Service Auth Integration
73527474-// 3. Fetch the specific key version (with caching)
7575-const cacheKey = `${groupId}:${key_version}`
7676-let key = keyCache.get(cacheKey)
5353+**Decision:** Client library abstracts service auth token management via callback pattern.
77547878-if (!key) {
7979- const response = await fetch(
8080- `/xrpc/dev.atpkeyserver.alpha.group.key?` +
8181- `group_id=${encodeURIComponent(groupId)}&version=${key_version}`,
8282- { headers: { Authorization: `Bearer ${followerJwt}` } }
8383- )
8484- const data = await response.json()
8585- key = data.secretKey
5555+**Rationale:**
5656+- PDS clients vary by platform (@atproto/api, custom implementations)
5757+- Callback pattern allows any PDS client integration
5858+- Client library handles token caching automatically
5959+- Separates concerns: client lib = crypto + keyserver API, user provides = PDS auth
86608787- // Cache historical keys for 24 hours (they never change)
8888- keyCache.set(cacheKey, key, { ttl: 86400 })
8989-}
9090-9191-// 4. Decrypt locally
9292-const plaintext = decryptMessage(postUri, key, encrypted_content)
9393-const post = JSON.parse(plaintext)
6161+**Implementation:**
6262+```typescript
6363+new KeyserverClient({
6464+ keyserverDid: 'did:web:keyserver.example.com',
6565+ getServiceAuthToken: async (aud, lxm) => {
6666+ // User provides: obtain token from their PDS client
6767+ return await pdsClient.getServiceAuth({ aud, lxm })
6868+ }
6969+})
9470```
95719696-## Authentication Flow
7272+See [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md#authentication-flow) for service auth details.
97739898-### Overview
7474+## Component Architecture
9975100100-The keyserver uses **ATProto service auth** - a secure, decentralized authentication mechanism where clients obtain short-lived JWT tokens from their PDS (Personal Data Server) to authenticate with the keyserver. This design ensures the keyserver never needs to store passwords or trust specific PDS instances.
7676+### Core Components
10177102102-**Key Properties:**
103103-- Tokens signed with user's ATProto signing key (from DID document)
104104-- Short-lived (default: 60 seconds, configurable up to server limits)
105105-- Audience-bound (only valid for specific keyserver DID)
106106-- Method-bound (optional: restrict to specific XRPC endpoint)
107107-- Direct client → keyserver communication (PDS only issues tokens)
7878+**1. Crypto Module (`crypto.ts`)**
7979+- XChaCha20-Poly1305 encryption/decryption
8080+- Uses @noble/ciphers library
8181+- Stateless, pure functions
8282+- No network dependencies
10883109109-### Complete Authentication Flow
8484+**2. KeyserverClient (`client.ts`)**
8585+- HTTP client for keyserver API
8686+- Service auth token management
8787+- Key caching with TTL
8888+- Error handling and retries
11089111111-```
112112-┌─────────┐ ┌─────────┐ ┌───────────┐
113113-│ Client │ │ PDS │ │ Keyserver │
114114-└────┬────┘ └────┬────┘ └─────┬─────┘
115115- │ │ │
116116- │ 1. Authenticate │ │
117117- │ ────────────────────────────>│ │
118118- │ (OAuth or createSession) │ │
119119- │ │ │
120120- │ 2. Return access token │ │
121121- │ <────────────────────────────│ │
122122- │ │ │
123123- │ 3. Request service auth │ │
124124- │ ────────────────────────────>│ │
125125- │ (getServiceAuth) │ │
126126- │ │ │
127127- │ 4. Sign & return JWT │ │
128128- │ <────────────────────────────│ │
129129- │ (signed with user's key) │ │
130130- │ │ │
131131- │ 5. Call keyserver with JWT │ │
132132- │ ────────────────────────────────────────────────────────────> │
133133- │ │ │
134134- │ │ 6. Verify JWT │
135135- │ │ - Check aud │
136136- │ │ - Resolve DID │
137137- │ │ - Verify permissions │
138138- │ │ │
139139- │ 7. Return encrypted key │ │
140140- │ <──────────────────────────────────────────────────────────── │
141141- │ │ │
142142-```
9090+**3. Cache Module (`cache.ts`)**
9191+- In-memory LRU cache
9292+- Separate TTLs for active/historical keys
9393+- Service auth token caching
9494+- Automatic expiry
14395144144-### Step 1: Authenticate to PDS
9696+**4. Service Auth Module (`service-auth.ts`)**
9797+- Token cache management
9898+- Expiry checking with safety margin
9999+- Automatic refresh logic
145100146146-Clients first authenticate with their PDS using OAuth (recommended) or legacy JWT authentication.
101101+**5. Types Module (`types.ts`)**
102102+- TypeScript interfaces
103103+- Configuration types
104104+- Response types
147105148148-**OAuth (Modern):**
149149-```typescript
150150-import { OAuthClient } from '@atproto/oauth-client'
106106+**6. Errors Module (`errors.ts`)**
107107+- Custom error classes
108108+- Structured error handling
109109+- HTTP status code mapping
151110152152-const oauthClient = new OAuthClient({
153153- clientId: 'your-app-client-id',
154154- // ... OAuth configuration
155155-})
111111+### Dependency Graph
156112157157-// Redirect user to PDS for authentication
158158-await oauthClient.authorize({
159159- scope: 'atproto transition:generic'
160160-})
161161-162162-// After OAuth callback:
163163-const { access_token, sub: userDid } = oauthClient.getTokens()
164113```
165165-166166-**Legacy JWT (com.atproto.server.createSession):**
167167-```typescript
168168-const { accessJwt, refreshJwt, did } = await fetch(
169169- 'https://bsky.social/xrpc/com.atproto.server.createSession',
170170- {
171171- method: 'POST',
172172- headers: { 'Content-Type': 'application/json' },
173173- body: JSON.stringify({
174174- identifier: 'user.bsky.social',
175175- password: 'app-password-here'
176176- })
177177- }
178178-).then(r => r.json())
114114+index.ts
115115+├── crypto.ts
116116+│ └── @noble/ciphers
117117+├── client.ts
118118+│ ├── crypto.ts
119119+│ ├── cache.ts
120120+│ ├── service-auth.ts
121121+│ ├── types.ts
122122+│ └── errors.ts
123123+├── types.ts
124124+└── errors.ts
179125```
180126181181-### Step 2: Request Service Auth Token
182182-183183-Call `com.atproto.server.getServiceAuth` on the user's PDS to obtain a service auth token for the keyserver.
184184-185185-```typescript
186186-// Get service auth token for keyserver
187187-const { token: serviceAuthToken } = await fetch(
188188- `https://bsky.social/xrpc/com.atproto.server.getServiceAuth?` +
189189- new URLSearchParams({
190190- aud: 'did:web:keyserver.example.com', // Keyserver DID (required)
191191- lxm: 'dev.atpkeyserver.alpha.key', // Optional: method binding
192192- exp: String(Math.floor(Date.now() / 1000) + 300) // Optional: 5 min expiry
193193- }),
194194- {
195195- headers: {
196196- // OAuth: Authorization: DPoP {access_token} + DPoP header
197197- // Legacy: Authorization: Bearer {accessJwt}
198198- Authorization: `Bearer ${accessJwt}`
199199- }
200200- }
201201-).then(r => r.json())
202202-```
127127+## Key Features
203128204204-**Parameters:**
205205-- `aud` (required): The DID of your keyserver (must match keyserver's serviceDid)
206206-- `lxm` (optional): XRPC method to bind token to (e.g., `dev.atpkeyserver.alpha.key`)
207207-- `exp` (optional): Expiration time in Unix epoch seconds (default: 60 seconds from now)
129129+### Automatic Caching
208130209209-**What the PDS does:**
210210-1. Verifies client's authentication (OAuth or JWT)
211211-2. Creates JWT with claims:
212212- - `iss`: User's DID
213213- - `aud`: Keyserver's DID
214214- - `exp`: Expiration timestamp
215215- - `iat`: Issued-at timestamp
216216- - `lxm`: Method binding (if specified)
217217-3. Signs JWT with user's ATProto signing key (from DID document)
218218-4. Returns `{ token: "eyJhbG..." }`
131131+**Key Caching:**
132132+- Active keys: 1 hour TTL (may change with rotation)
133133+- Historical keys: 24 hour TTL (immutable)
134134+- LRU eviction when max size reached
219135220220-### Step 3: Call Keyserver with Service Auth Token
136136+**Service Auth Token Caching:**
137137+- 60 second TTL (or server-specified `exp`)
138138+- Refresh when <10 seconds remain
139139+- Memory-only storage
221140222222-```typescript
223223-// Use service auth token to fetch group key
224224-const { secretKey, version } = await fetch(
225225- `https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.key?` +
226226- `group_id=${encodeURIComponent(`${userDid}#followers`)}`,
227227- {
228228- headers: {
229229- Authorization: `Bearer ${serviceAuthToken}`
230230- }
231231- }
232232-).then(r => r.json())
233233-```
141141+**Benefits:**
142142+- Reduces keyserver load
143143+- Improves client performance
144144+- Handles token refresh automatically
234145235235-**Keyserver verification (automatic in authMiddleware.ts):**
236236-1. Extracts JWT from Authorization header
237237-2. Parses JWT to get `iss` (user DID) and `aud` (keyserver DID)
238238-3. Checks `aud` matches keyserver's DID (prevents token misuse)
239239-4. Resolves user's DID to get signing key
240240-5. Verifies JWT signature with user's signing key
241241-6. Checks expiration timestamp
242242-7. Returns authenticated user's DID to request handler
146146+See [SECURITY.md](./SECURITY.md#token-caching) for security considerations.
243147244244-### Service Auth Token Caching
148148+### Request Deduplication
245149246246-Service auth tokens are short-lived (60 seconds default). Clients should cache tokens to avoid excessive PDS requests:
150150+Multiple simultaneous requests for the same key are collapsed into a single network request:
247151248152```typescript
249249-class ServiceAuthCache {
250250- private tokens = new Map<string, {
251251- token: string
252252- expiresAt: number
253253- }>()
153153+// Both calls trigger only one network request
154154+const [key1, key2] = await Promise.all([
155155+ client.getGroupKey('did:plc:abc#followers'),
156156+ client.getGroupKey('did:plc:abc#followers')
157157+])
158158+```
254159255255- async getToken(
256256- pdsClient: PdsClient,
257257- aud: string,
258258- lxm?: string
259259- ): Promise<string> {
260260- const cacheKey = `${aud}:${lxm || ''}`
261261- const cached = this.tokens.get(cacheKey)
160160+### Automatic Retry
262161263263- // Use cached token if valid for at least 10 more seconds
264264- const now = Math.floor(Date.now() / 1000)
265265- if (cached && cached.expiresAt > now + 10) {
266266- return cached.token
267267- }
162162+Network failures retry with exponential backoff:
163163+- Retry on: 5xx errors, network timeouts
164164+- Don't retry on: 4xx errors (client errors are permanent)
165165+- Max retries: 3 attempts
166166+- Backoff: 100ms, 200ms, 400ms
268167269269- // Request new token (60 second expiry)
270270- const exp = now + 60
271271- const { token } = await pdsClient.getServiceAuth({ aud, lxm, exp })
168168+### Type Safety
272169273273- this.tokens.set(cacheKey, { token, expiresAt: exp })
274274- return token
275275- }
170170+Full TypeScript support:
171171+- Strict typing for all APIs
172172+- IntelliSense support
173173+- Compile-time error detection
174174+- Generated type definitions
276175277277- clear(): void {
278278- this.tokens.clear()
279279- }
280280-}
281281-```
176176+## Bundle Size Analysis
282177283283-**Cache strategy:**
284284-- Keep tokens for up to 60 seconds (or specified `exp`)
285285-- Refresh when <10 seconds remain (safety margin)
286286-- Separate cache keys for different services (`aud`) and methods (`lxm`)
287287-- Clear cache on logout
178178+Tree-shaking eliminates unused code. Actual bundle sizes depend on what you import:
288179289289-### Complete End-to-End Example
180180+### Scenario 1: Crypto Functions Only
290181291182```typescript
292292-import { KeyserverClient } from '@atpkeyserver/client'
293293-import { encryptMessage } from '@atpkeyserver/client/crypto'
294294-295295-// 1. Authenticate to PDS (one-time or when session expires)
296296-const pdsClient = new AtpAgent({ service: 'https://bsky.social' })
297297-await pdsClient.login({
298298- identifier: 'user.bsky.social',
299299- password: 'app-password'
300300-})
301301-302302-// 2. Create keyserver client with service auth integration
303303-const keyserverClient = new KeyserverClient({
304304- keyserverDid: 'did:web:keyserver.example.com',
305305- getServiceAuthToken: async (aud, lxm) => {
306306- // Get service auth token from PDS
307307- const { token } = await pdsClient.com.atproto.server.getServiceAuth({
308308- aud,
309309- lxm,
310310- exp: Math.floor(Date.now() / 1000) + 60
311311- })
312312- return token
313313- }
314314-})
315315-316316-// 3. Encrypt and publish post (automatic token handling)
317317-const groupId = `${pdsClient.session.did}#followers`
318318-const { ciphertext, version } = await keyserverClient.encrypt(
319319- groupId,
320320- postUri,
321321- JSON.stringify({ text: "Secret message for followers" })
322322-)
323323-324324-await pdsClient.com.atproto.repo.createRecord({
325325- repo: pdsClient.session.did,
326326- collection: 'app.bsky.feed.post',
327327- record: {
328328- encrypted_content: ciphertext,
329329- key_version: version,
330330- encrypted_at: new Date().toISOString(),
331331- createdAt: new Date().toISOString()
332332- }
333333-})
183183+import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto'
334184```
335185336336-### Security Properties
186186+**Bundled:** ~52KB (minified + gzipped ~15KB)
187187+- @noble/ciphers: 50KB
188188+- crypto.ts: 2KB
337189338338-**Audience Binding:**
339339-- Tokens only valid for specific keyserver DID
340340-- Prevents token reuse across services
341341-- Keyserver rejects tokens with wrong `aud`
190190+**Use case:** You handle keyserver API calls manually, just need crypto.
342191343343-**Method Binding (Optional):**
344344-- `lxm` parameter restricts token to specific endpoint
345345-- Extra security for sensitive operations
346346-- Example: token only valid for `dev.atpkeyserver.alpha.key.rotate`
347347-348348-**Short-Lived Tokens:**
349349-- Default 60 second expiry limits attack window
350350-- Compromised token quickly becomes useless
351351-- No long-term credential storage on client
352352-353353-**Decentralized Verification:**
354354-- Keyserver verifies signature using public key from DID document
355355-- No shared secrets between PDS and keyserver
356356-- Works with any ATProto-compliant PDS
357357-358358-**User Key Signing:**
359359-- Token signed with user's signing key, not PDS key
360360-- User maintains cryptographic control over identity
361361-- PDS cannot forge tokens for users without their key
192192+### Scenario 2: Full Client
362193363363-## Implementation Plan
364364-365365-### 1. Create npm Package: `@atpkeyserver/client`
366366-367367-**Purpose:** Unified client library for keyserver integration, providing both low-level cryptographic functions and high-level API wrapper with built-in caching.
368368-369369-**Package Structure:**
370370-```
371371-@atpkeyserver/client/
372372-├── src/
373373-│ ├── index.ts # Main exports (KeyserverClient + crypto)
374374-│ ├── client.ts # KeyserverClient class
375375-│ ├── crypto.ts # Encryption/decryption functions
376376-│ ├── cache.ts # Key caching with TTL
377377-│ ├── service-auth.ts # Service auth token caching
378378-│ ├── types.ts # TypeScript types
379379-│ └── errors.ts # Custom error classes
380380-├── package.json # With subpath exports configured
381381-├── tsconfig.json
382382-├── README.md
383383-└── examples/
384384- ├── basic-usage.ts
385385- ├── react-hook.tsx
386386- └── cli-example.ts
387387-```
388388-389389-**Subpath Exports Configuration:**
390390-```json
391391-// package.json
392392-{
393393- "name": "@atpkeyserver/client",
394394- "version": "1.0.0",
395395- "exports": {
396396- ".": {
397397- "import": "./dist/index.js",
398398- "require": "./dist/index.cjs"
399399- },
400400- "./crypto": {
401401- "import": "./dist/crypto.js",
402402- "require": "./dist/crypto.cjs"
403403- },
404404- "./client": {
405405- "import": "./dist/client.js",
406406- "require": "./dist/client.cjs"
407407- }
408408- }
409409-}
194194+```typescript
195195+import { KeyserverClient } from '@atpkeyserver/client'
410196```
411197412412-**Import Patterns:**
413413-```typescript
414414-// Option 1: Import just crypto functions (tree-shaken, minimal bundle)
415415-import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto'
198198+**Bundled:** ~60KB (minified + gzipped ~18KB)
199199+- @noble/ciphers: 50KB
200200+- Client code: 10KB (includes crypto, caching, HTTP, errors)
416201417417-// Option 2: Import just the client
418418-import { KeyserverClient } from '@atpkeyserver/client/client'
202202+**Use case:** Full-featured client with automatic caching and token management.
419203420420-// Option 3: Import everything from main entry
421421-import { KeyserverClient, encryptMessage, decryptMessage } from '@atpkeyserver/client'
422422-```
204204+### Scenario 3: Both
423205424424-**Crypto Functions API:**
425206```typescript
426426-// Export from lib/keys/client.ts
427427-export function encryptMessage(
428428- id: string,
429429- key: string,
430430- plaintext: string
431431-): string
432432-433433-export function decryptMessage(
434434- id: string,
435435- key: string,
436436- ciphertext: string
437437-): string
438438-207207+import { KeyserverClient, encryptMessage } from '@atpkeyserver/client'
439208```
440209441441-**KeyserverClient API:**
442442-```typescript
443443-class KeyserverClient {
444444- constructor(config: {
445445- keyserverDid: string
446446- getServiceAuthToken: (aud: string, lxm?: string) => Promise<string>
447447- cache?: CacheOptions
448448- })
210210+**Bundled:** ~60KB (same as scenario 2)
211211+- Client includes crypto, no duplication
449212450450- // Get active group key (auto-cached)
451451- async getGroupKey(groupId: string): Promise<{
452452- secretKey: string
453453- version: number
454454- }>
213213+**Use case:** Mix high-level and low-level APIs as needed.
455214456456- // Get specific key version (auto-cached with longer TTL)
457457- async getGroupKeyVersion(
458458- groupId: string,
459459- version: number
460460- ): Promise<{ secretKey: string }>
215215+### Bundle Optimization
461216462462- // Encrypt message with auto key fetching
463463- async encrypt(
464464- groupId: string,
465465- messageId: string,
466466- plaintext: string
467467- ): Promise<{
468468- ciphertext: string
469469- version: number
470470- }>
217217+**Automatic:**
218218+- Tree-shaking removes unused exports
219219+- Minification reduces code size
220220+- Gzip compresses for network transfer
471221472472- // Decrypt message with auto key fetching
473473- async decrypt(
474474- groupId: string,
475475- messageId: string,
476476- ciphertext: string,
477477- version: number
478478- ): Promise<string>
222222+**Manual optimization:**
223223+- Use subpath imports (`/crypto`, `/client`) for smaller bundles
224224+- Import only what you need
225225+- Modern bundlers (webpack, vite, rollup) handle this automatically
479226480480- // Clear cache (useful for testing or logout)
481481- clearCache(): void
482482-}
483483-```
227227+**Conclusion:** Crypto library dominates bundle size. Single package vs separate packages saves only ~8KB, not worth maintenance complexity.
484228485485-**Caching Strategy:**
486486-```typescript
487487-interface CacheOptions {
488488- activeKeyTtl?: number // Default: 3600 (1 hour)
489489- historicalKeyTtl?: number // Default: 86400 (24 hours)
490490- maxSize?: number // Default: 1000 keys
491491-}
492492-```
229229+## Implementation Phases
493230494494-**Error Handling:**
495495-```typescript
496496-class KeyserverError extends Error {
497497- constructor(
498498- message: string,
499499- public statusCode: number,
500500- public code: string
501501- )
502502-}
231231+### Phase 1: Foundation (Complete)
503232504504-// Specific error types
505505-class UnauthorizedError extends KeyserverError
506506-class GroupNotFoundError extends KeyserverError
507507-class NetworkError extends KeyserverError
508508-class DecryptionError extends KeyserverError
509509-```
233233+- [x] Crypto functions (encryptMessage, decryptMessage)
234234+- [x] Type definitions
235235+- [x] Package structure with subpath exports
236236+- [x] Build configuration (ESM + CJS)
510237511511-**Key Features:**
512512-- TypeScript-first with full type safety
513513-- Tree-shakeable ESM exports (import only what you need)
514514-- CommonJS compatibility for Node.js
515515-- **Service auth token management with automatic caching**
516516-- **Integration with PDS via getServiceAuthToken callback**
517517-- Automatic retry logic with exponential backoff
518518-- Request deduplication (multiple simultaneous requests for same key)
519519-- Built-in cache invalidation strategies (keys + auth tokens)
520520-- Supports custom cache implementations
521521-- Comprehensive error handling
522522-- Request/response logging hooks
523523-- Zero runtime dependencies beyond @noble/ciphers
524524-- Comprehensive JSDoc documentation
238238+### Phase 2: Client Library (In Progress)
525239526526-**README Contents:**
527527-- Installation instructions
528528-- Quick start guide showing both high-level and low-level usage
529529-- **Service auth token integration with PDS**
530530-- Complete API documentation for KeyserverClient and crypto functions
531531-- Caching behavior explanation (keys + service auth tokens)
532532-- Error handling guide
533533-- Security considerations and best practices
534534-- Tree-shaking and bundle optimization tips
535535-- Usage examples for common scenarios:
536536- - Creating encrypted posts
537537- - Reading encrypted feeds
538538- - Key rotation handling
539539- - Group management
540540- - **PDS authentication and service auth token flow**
541541-- Integration guides for:
542542- - React/React Native apps
543543- - Node.js services
544544- - CLI tools
545545-- Browser and Node.js compatibility notes
240240+- [ ] KeyserverClient class
241241+- [ ] Service auth token management
242242+- [ ] Key caching with TTL
243243+- [ ] HTTP client with retry logic
244244+- [ ] Error handling
245245+- [ ] Request deduplication
546246547547----
247247+### Phase 3: Documentation (In Progress)
548248549549-### 2. Documentation
249249+- [x] Protocol specification (ENCRYPTION_PROTOCOL.md)
250250+- [x] Security guidelines (SECURITY.md)
251251+- [x] API reference (API_REFERENCE.md)
252252+- [ ] Client README with usage examples
253253+- [ ] Example implementations (React, Node.js, CLI)
550254551551-#### Protocol Specification Document
255255+### Phase 4: Testing
552256553553-Create `docs/ENCRYPTION_PROTOCOL.md` with:
257257+- [ ] Unit tests for crypto functions
258258+- [ ] Integration tests for client
259259+- [ ] Mock keyserver for testing
260260+- [ ] E2E encryption flow test
261261+- [ ] Cache behavior tests
262262+- [ ] Service auth token refresh tests
554263555555-**Overview:**
556556-- End-to-end encryption architecture
557557-- **Service auth token authentication flow**
558558-- Key versioning and rotation strategy
559559-- Group-based access control model
264264+### Phase 5: Polish
560265561561-**Message Format:**
562562-```typescript
563563-// Encrypted post record structure
564564-{
565565- encrypted_content: string, // hex(nonce) + hex(ciphertext)
566566- key_version: number, // Required for decryption
567567- encrypted_at: string, // ISO timestamp
568568- visibility: 'followers' | 'mentioned' | string
569569-}
570570-```
266266+- [ ] Performance benchmarks
267267+- [ ] Bundle size analysis
268268+- [ ] Documentation review
269269+- [ ] Example applications
270270+- [ ] npm package publication
571271572572-**Encryption Process:**
573573-1. Authenticate to PDS (OAuth or legacy JWT)
574574-2. Request service auth token from PDS for keyserver
575575-3. Determine group ID from visibility setting
576576-4. Fetch active group key from keyserver using service auth token
577577-5. Generate AT-URI as message ID
578578-6. Encrypt content with XChaCha20-Poly1305
579579-7. Store ciphertext with key version metadata
272272+## Success Metrics
580273581581-**Decryption Process:**
582582-1. Authenticate to PDS (OAuth or legacy JWT)
583583-2. Request service auth token from PDS for keyserver
584584-3. Read encrypted_content and key_version from record
585585-4. Determine group ID from author DID and visibility
586586-5. Fetch specific key version from keyserver using service auth token
587587-6. Decrypt using AT-URI as AAD (Additional Authenticated Data)
588588-7. Parse and display plaintext content
274274+### Performance Targets
589275590590-**Security Considerations:**
591591-- **Service auth tokens are short-lived (60 seconds default)**
592592-- **Cache service auth tokens to avoid PDS rate limits**
593593-- **Clear auth token cache on logout**
594594-- Never reuse nonces (automatically handled by random generation)
595595-- Always include AT-URI as AAD for binding
596596-- Cache keys appropriately (short TTL for active, long for historical)
597597-- Validate key_version exists before attempting decryption
598598-- Handle decryption failures gracefully (deleted posts, revoked access)
276276+| Operation | Target | Measurement |
277277+|-----------|--------|-------------|
278278+| Encryption | <1ms | Local crypto operation |
279279+| Decryption (cached key) | <5ms | Cache lookup + crypto |
280280+| Decryption (cache miss) | <200ms | Network fetch + crypto |
281281+| Service auth (cached) | <1ms | Cache lookup |
282282+| Service auth (cache miss) | <100ms | PDS round-trip |
599283600600-**Key Rotation Handling:**
601601-- Old posts encrypted with old key versions
602602-- New posts use current active key
603603-- Clients must support multi-version decryption
604604-- No re-encryption required for historical posts
284284+### Developer Experience
605285606606-**Error Scenarios:**
607607-- Missing key_version: Cannot decrypt (malformed post)
608608-- 403 Unauthorized: User lost group access
609609-- 404 Not Found: Group was deleted
610610-- Decryption failure: Corrupted data or wrong key
286286+- [ ] Complete TypeScript types
287287+- [ ] Comprehensive JSDoc comments
288288+- [ ] Working examples for 3+ platforms
289289+- [ ] <5 minutes from install to first encrypted message
290290+- [ ] Clear error messages with actionable guidance
611291612612-#### Security Best Practices Document
292292+### Security
613293614614-Create `docs/SECURITY.md` with:
294294+- [ ] Zero plaintext exposure to server
295295+- [ ] Keys cached in memory only
296296+- [ ] Service auth tokens cached for max 60 seconds
297297+- [ ] Proper cleanup on logout
298298+- [ ] No key or token leakage in logs
299299+- [ ] Audience binding prevents token misuse
300300+- [ ] Short-lived tokens limit attack window
615301616616-**Client Implementation Guidelines:**
617617-- Key material handling in memory
618618-- Service auth token and key cache security considerations
619619-- Error message sanitization (don't leak keys or tokens)
620620-- Secure key and token deletion on logout
621621-- Rate limiting recommendations
302302+### Code Quality
622303623623-**Service Auth Token Caching:**
624624-- **Short-lived tokens: 60 second TTL (default)**
625625-- **Refresh when <10 seconds remain (safety margin)**
626626-- **Memory-only cache (NEVER persist to disk)**
627627-- **Clear token cache on logout or session end**
628628-- **Separate cache keys per service (aud) and method (lxm)**
304304+- [ ] >80% test coverage
305305+- [ ] Zero runtime dependencies beyond @noble/ciphers
306306+- [ ] Tree-shakeable exports
307307+- [ ] CommonJS + ESM support
308308+- [ ] Works in Node.js, browser, React Native
629309630630-**Encryption Key Caching:**
631631-- Active keys: 1 hour TTL (balance freshness vs performance)
632632-- Historical keys: 24 hour TTL (never change, safe to cache longer)
633633-- Memory-only cache (never persist keys to disk without encryption)
634634-- Clear key cache on user logout
635635-- Cache size limits to prevent memory exhaustion
310310+## Testing Strategy
636311637637-**Common Pitfalls:**
638638-- Don't log decrypted content, keys, or auth tokens
639639-- Don't persist keys or tokens in localStorage without encryption
640640-- Don't skip version validation
641641-- Don't assume key fetch always succeeds
642642-- Don't retry decryption failures infinitely
643643-- **Don't reuse expired service auth tokens**
644644-- **Don't share service auth tokens between services (audience binding)**
312312+### Unit Tests
645313646646-#### Example Implementations
314314+**Crypto module:**
315315+- Encrypt/decrypt round-trip
316316+- Nonce uniqueness
317317+- AAD binding
318318+- Error handling
647319648648-**React Hook Example:**
649649-```typescript
650650-// useEncryptedPost.ts
651651-import { useCallback } from 'react'
652652-import { KeyserverClient } from '@atpkeyserver/client'
320320+**Cache module:**
321321+- TTL expiry
322322+- LRU eviction
323323+- Get/set operations
324324+- Clear operation
653325654654-export function useEncryptedPost(keyserverClient: KeyserverClient) {
655655- const createEncryptedPost = useCallback(async (
656656- visibility: 'followers',
657657- content: string
658658- ) => {
659659- const groupId = `${userDid}#${visibility}`
660660- const postUri = generatePostUri()
326326+**Service auth module:**
327327+- Token expiry checking
328328+- Refresh logic
329329+- Cache key generation
661330662662- const { ciphertext, version } = await keyserverClient.encrypt(
663663- groupId,
664664- postUri,
665665- JSON.stringify({ text: content, createdAt: new Date() })
666666- )
331331+### Integration Tests
667332668668- return {
669669- encrypted_content: ciphertext,
670670- key_version: version,
671671- encrypted_at: new Date().toISOString()
672672- }
673673- }, [keyserverClient, userDid])
333333+**Client:**
334334+- Key fetch with caching
335335+- Service auth integration
336336+- Retry logic
337337+- Error handling
674338675675- const decryptPost = useCallback(async (
676676- encryptedPost: EncryptedPost
677677- ) => {
678678- const { encrypted_content, key_version } = encryptedPost
679679- const authorDid = extractDidFromUri(encryptedPost.uri)
680680- const groupId = `${authorDid}#followers`
339339+**E2E:**
340340+- Full encryption flow (PDS → keyserver → encrypt → store)
341341+- Full decryption flow (fetch → keyserver → decrypt → display)
342342+- Key rotation handling
681343682682- const plaintext = await keyserverClient.decrypt(
683683- groupId,
684684- encryptedPost.uri,
685685- encrypted_content,
686686- key_version
687687- )
344344+### Mock Server
345345+346346+Build lightweight mock keyserver for testing:
347347+- Simulates auth verification
348348+- Returns mock keys
349349+- Configurable latency
350350+- Error injection
688351689689- return JSON.parse(plaintext)
690690- }, [keyserverClient])
352352+## Open Questions
691353692692- return { createEncryptedPost, decryptPost }
693693-}
694694-```
354354+### Rate Limiting
695355696696-**CLI Example:**
697697-```typescript
698698-// encrypt-cli.ts
699699-import { KeyserverClient } from '@atpkeyserver/client'
356356+**Question:** Should client enforce rate limits?
700357701701-const client = new KeyserverClient({
702702- baseUrl: process.env.KEYSERVER_URL,
703703- getAuthToken: async () => process.env.ATP_JWT
704704-})
358358+**Options:**
359359+1. No client-side limits (rely on server)
360360+2. Simple debouncing (prevent rapid duplicate requests)
361361+3. Full rate limit tracking (mirror server limits)
705362706706-// Encrypt a message
707707-const { ciphertext, version } = await client.encrypt(
708708- 'did:plc:abc123#followers',
709709- 'at://did:plc:abc123/app.bsky.feed.post/xyz789',
710710- 'Secret message for followers only'
711711-)
363363+**Current decision:** Option 2 (debouncing via request deduplication)
712364713713-console.log(`Encrypted (v${version}):`, ciphertext)
365365+### Cache Persistence
714366715715-// Decrypt a message
716716-const plaintext = await client.decrypt(
717717- 'did:plc:abc123#followers',
718718- 'at://did:plc:abc123/app.bsky.feed.post/xyz789',
719719- ciphertext,
720720- version
721721-)
367367+**Question:** Should keys be persisted to disk?
722368723723-console.log('Decrypted:', plaintext)
724724-```
369369+**Options:**
370370+1. Memory-only (current implementation)
371371+2. Encrypted disk cache (requires key derivation)
372372+3. Platform-specific secure storage (iOS Keychain, Android KeyStore)
725373726726----
374374+**Current decision:** Memory-only for security and simplicity
727375728728-## Bundle Size & Tree-Shaking
376376+**Rationale:**
377377+- Simpler implementation
378378+- Better security (no disk persistence)
379379+- Works across all platforms
380380+- User can implement custom cache if needed
729381730730-Modern bundlers (webpack, vite, rollup) automatically eliminate unused code through tree-shaking. This means developers can choose their level of abstraction without worrying about bundle size:
382382+### Error Recovery
731383732732-**Scenario 1: Just Crypto Functions**
733733-```typescript
734734-import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto'
735735-// Bundled: ~52KB (@noble/ciphers + crypto.ts)
736736-// KeyserverClient, cache, and HTTP client code eliminated
737737-```
384384+**Question:** How should clients handle persistent decryption failures?
738385739739-**Scenario 2: Full Client with Caching**
740740-```typescript
741741-import { KeyserverClient } from '@atpkeyserver/client'
742742-// Bundled: ~60KB (includes everything)
743743-// Crypto functions included since client depends on them
744744-```
386386+**Options:**
387387+1. Fail silently (hide post)
388388+2. Show error message
389389+3. Retry with exponential backoff
390390+4. Prompt user action (e.g., "Request access")
745391746746-**Scenario 3: Both (Advanced)**
747747-```typescript
748748-import { KeyserverClient, encryptMessage } from '@atpkeyserver/client'
749749-// Bundled: ~60KB (same as scenario 2)
750750-// Manual crypto calls available alongside client methods
751751-```
392392+**Current decision:** Show error message, no retry (decryption failures are permanent)
752393753753-**Key Insight:** The crypto library (@noble/ciphers) represents 90%+ of the total bundle size. Whether you import crypto functions alone or the full client, the bundle size difference is only ~8-10KB. This validates the single-package approach - the maintenance complexity of separate packages isn't justified by such a small size difference.
394394+## Related Documentation
754395755755----
396396+- [Encryption Protocol](./ENCRYPTION_PROTOCOL.md) - Protocol specification
397397+- [Security Best Practices](./SECURITY.md) - Security guidelines
398398+- [API Reference](./API_REFERENCE.md) - Complete endpoint reference
399399+- [Architecture](./ARCHITECTURE.md) - Server architecture
400400+- [Client README](../packages/client/README.md) - Usage instructions
756401757757-## Success Metrics
402402+## Contributing
758403759759-**Performance Targets:**
760760-- Encryption operation: <1ms
761761-- Decryption operation: <5ms (including cache lookup)
762762-- Key fetch with cache hit: <1ms
763763-- Key fetch with cache miss: <200ms
764764-- **Service auth token fetch: <100ms (PDS round-trip)**
765765-- **Service auth token cache hit: <1ms**
404404+Contributions welcome! Please:
766405767767-**Developer Experience:**
768768-- Complete TypeScript types
769769-- Comprehensive documentation
770770-- Working examples for 3+ platforms
771771-- <5 minutes from npm install to first encrypted message
772772-- **Clear service auth integration patterns**
773773-- **Automatic token refresh handling**
406406+1. Read existing documentation
407407+2. Follow code style (Prettier)
408408+3. Add tests for new features
409409+4. Update documentation as needed
410410+5. Submit PR with clear description
774411775775-**Security:**
776776-- Zero plaintext exposure to server
777777-- All keys cached in memory only
778778-- **Service auth tokens cached for max 60 seconds**
779779-- Proper cleanup on logout (keys + tokens)
780780-- No key or token leakage in logs or errors
781781-- **Audience binding prevents token misuse**
782782-- **Short-lived tokens limit attack window**
412412+For questions, open an issue in the [Codeberg repository](https://codeberg.org/juandjara/atp-keyserver).
+409-17
packages/client/README.md
···11# @atpkeyserver/client
2233-Client library for ATP keyserver with end-to-end encryption support.
33+TypeScript client library for ATP Keyserver with end-to-end encryption.
4455## Status
6677-🚧 **Under Development** - This package is currently being built.
77+🚧 **Under Development** - Core crypto functions implemented, KeyserverClient in progress.
8899-## Overview
99+## Features
10101111-This client library provides:
1212-- End-to-end encryption functions (XChaCha20-Poly1305)
1313-- Type-safe keyserver API client
1414-- Automatic service auth token management
1515-- Built-in key caching
1111+- **End-to-end encryption** with XChaCha20-Poly1305
1212+- **Service auth integration** with ATProto PDS
1313+- **Automatic key caching** for performance
1414+- **TypeScript-first** with full type safety
1515+- **Tree-shakeable** ESM and CommonJS support
1616+- **Zero runtime dependencies** (except @noble/ciphers)
16171718## Installation
18191920```bash
2021npm install @atpkeyserver/client
2222+# or
2323+bun add @atpkeyserver/client
2124```
22252326## Quick Start
24272828+### Using Crypto Functions Only
2929+3030+If you want to handle key management manually:
3131+2532```typescript
2633import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto'
27342835// Encrypt a message
3636+const messageId = 'at://did:plc:abc123/app.bsky.feed.post/xyz789'
3737+const secretKey = '0123456789abcdef...' // 64 hex chars (32 bytes)
3838+const plaintext = JSON.stringify({ text: 'Secret message' })
3939+4040+const ciphertext = encryptMessage(messageId, secretKey, plaintext)
4141+// Returns: hex(nonce) + hex(ciphertext)
4242+4343+// Decrypt a message
4444+const decrypted = decryptMessage(messageId, secretKey, ciphertext)
4545+const post = JSON.parse(decrypted)
4646+// Returns: { text: 'Secret message' }
4747+```
4848+4949+### Using KeyserverClient (Coming Soon)
5050+5151+Full-featured client with automatic caching and service auth:
5252+5353+```typescript
5454+import { KeyserverClient } from '@atpkeyserver/client'
5555+import { AtpAgent } from '@atproto/api'
5656+5757+// 1. Set up PDS client
5858+const agent = new AtpAgent({ service: 'https://bsky.social' })
5959+await agent.login({
6060+ identifier: 'user.bsky.social',
6161+ password: 'app-password'
6262+})
6363+6464+// 2. Create keyserver client
6565+const keyserver = new KeyserverClient({
6666+ keyserverDid: 'did:web:keyserver.example.com',
6767+ getServiceAuthToken: async (aud, lxm) => {
6868+ const { token } = await agent.com.atproto.server.getServiceAuth({
6969+ aud,
7070+ lxm,
7171+ exp: Math.floor(Date.now() / 1000) + 60
7272+ })
7373+ return token
7474+ }
7575+})
7676+7777+// 3. Encrypt a post
7878+const groupId = `${agent.session.did}#followers`
7979+const postUri = 'at://did:plc:abc123/app.bsky.feed.post/xyz789'
8080+const { ciphertext, version } = await keyserver.encrypt(
8181+ groupId,
8282+ postUri,
8383+ JSON.stringify({ text: 'Secret message for followers' })
8484+)
8585+8686+// 4. Store encrypted post in PDS
8787+await agent.com.atproto.repo.createRecord({
8888+ repo: agent.session.did,
8989+ collection: 'app.bsky.feed.post',
9090+ record: {
9191+ encrypted_content: ciphertext,
9292+ key_version: version,
9393+ encrypted_at: new Date().toISOString(),
9494+ createdAt: new Date().toISOString()
9595+ }
9696+})
9797+9898+// 5. Decrypt a post from feed
9999+const encryptedPost = await agent.getPost(postUri)
100100+const plaintext = await keyserver.decrypt(
101101+ groupId,
102102+ postUri,
103103+ encryptedPost.encrypted_content,
104104+ encryptedPost.key_version
105105+)
106106+const post = JSON.parse(plaintext)
107107+```
108108+109109+## API Reference
110110+111111+### Crypto Functions
112112+113113+#### `encryptMessage(id, key, plaintext)`
114114+115115+Encrypts plaintext using XChaCha20-Poly1305 with additional authenticated data.
116116+117117+**Parameters:**
118118+- `id` (string): Message ID (AT-URI) used as AAD
119119+- `key` (string): Hex-encoded 32-byte secret key (64 hex characters)
120120+- `plaintext` (string): UTF-8 plaintext to encrypt
121121+122122+**Returns:** `string` - Hex-encoded nonce (48 chars) + ciphertext
123123+124124+**Example:**
125125+```typescript
29126const ciphertext = encryptMessage(
3030- 'at://did:plc:abc123/app.bsky.feed.post/xyz',
3131- secretKey,
127127+ 'at://did:plc:abc123/app.bsky.feed.post/xyz789',
128128+ '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
32129 'Secret message'
33130)
131131+```
341323535-// Decrypt a message
133133+#### `decryptMessage(id, key, ciphertext)`
134134+135135+Decrypts ciphertext using XChaCha20-Poly1305.
136136+137137+**Parameters:**
138138+- `id` (string): Message ID (AT-URI) used as AAD (must match encryption)
139139+- `key` (string): Hex-encoded 32-byte secret key (64 hex characters)
140140+- `ciphertext` (string): Hex-encoded nonce + ciphertext from `encryptMessage`
141141+142142+**Returns:** `string` - UTF-8 plaintext
143143+144144+**Throws:** Error if:
145145+- AAD doesn't match (wrong message ID)
146146+- Key is incorrect
147147+- Ciphertext is corrupted
148148+- Authentication tag verification fails
149149+150150+**Example:**
151151+```typescript
36152const plaintext = decryptMessage(
3737- 'at://did:plc:abc123/app.bsky.feed.post/xyz',
3838- secretKey,
3939- ciphertext
153153+ 'at://did:plc:abc123/app.bsky.feed.post/xyz789',
154154+ '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
155155+ '48a3f2c1...9b8d7e6f'
40156)
41157```
421584343-## Documentation
159159+### KeyserverClient (Coming Soon)
441604545-See the [Client Implementation Guide](../../docs/CLIENT_IMPLEMENTATION.md) for complete documentation.
161161+#### Constructor
162162+163163+```typescript
164164+new KeyserverClient(config: KeyserverClientConfig)
165165+```
166166+167167+**Config Options:**
168168+```typescript
169169+interface KeyserverClientConfig {
170170+ keyserverDid: string // DID of keyserver
171171+ getServiceAuthToken: (aud: string, lxm?: string) => Promise<string> // Token provider
172172+ cache?: CacheOptions // Optional cache config
173173+}
174174+175175+interface CacheOptions {
176176+ activeKeyTtl?: number // Active key TTL in seconds (default: 3600)
177177+ historicalKeyTtl?: number // Historical key TTL in seconds (default: 86400)
178178+ maxSize?: number // Max cache entries (default: 1000)
179179+}
180180+```
181181+182182+#### `getGroupKey(groupId: string)`
183183+184184+Fetch the active group key with automatic caching.
185185+186186+**Parameters:**
187187+- `groupId` (string): Group ID in format `{owner_did}#{group_name}`
188188+189189+**Returns:** `Promise<{ secretKey: string, version: number }>`
190190+191191+**Throws:**
192192+- `UnauthorizedError` - Invalid or expired service auth token
193193+- `ForbiddenError` - User not a member of group
194194+- `NotFoundError` - Group doesn't exist
195195+- `NetworkError` - Network or server error
196196+197197+#### `getGroupKeyVersion(groupId: string, version: number)`
198198+199199+Fetch a specific historical key version with automatic caching.
200200+201201+**Parameters:**
202202+- `groupId` (string): Group ID
203203+- `version` (number): Key version number
204204+205205+**Returns:** `Promise<{ secretKey: string }>`
206206+207207+#### `encrypt(groupId: string, messageId: string, plaintext: string)`
208208+209209+Encrypt message with automatic key fetching.
210210+211211+**Parameters:**
212212+- `groupId` (string): Group ID
213213+- `messageId` (string): Message ID (AT-URI)
214214+- `plaintext` (string): UTF-8 plaintext
215215+216216+**Returns:** `Promise<{ ciphertext: string, version: number }>`
217217+218218+#### `decrypt(groupId: string, messageId: string, ciphertext: string, version: number)`
219219+220220+Decrypt message with automatic key fetching.
221221+222222+**Parameters:**
223223+- `groupId` (string): Group ID
224224+- `messageId` (string): Message ID (AT-URI)
225225+- `ciphertext` (string): Hex-encoded nonce + ciphertext
226226+- `version` (number): Key version from encrypted record
227227+228228+**Returns:** `Promise<string>` - Decrypted plaintext
229229+230230+#### `clearCache()`
231231+232232+Clear all cached keys and service auth tokens. Call on logout.
233233+234234+```typescript
235235+keyserver.clearCache()
236236+```
237237+238238+## Service Auth Integration
239239+240240+The keyserver uses ATProto service auth for authentication. You need to provide a `getServiceAuthToken` callback that obtains tokens from the user's PDS.
241241+242242+### With @atproto/api
243243+244244+```typescript
245245+import { AtpAgent } from '@atproto/api'
246246+import { KeyserverClient } from '@atpkeyserver/client'
247247+248248+const agent = new AtpAgent({ service: 'https://bsky.social' })
249249+await agent.login({ identifier: 'user.bsky.social', password: 'app-password' })
250250+251251+const keyserver = new KeyserverClient({
252252+ keyserverDid: 'did:web:keyserver.example.com',
253253+ getServiceAuthToken: async (aud, lxm) => {
254254+ const { token } = await agent.com.atproto.server.getServiceAuth({
255255+ aud,
256256+ lxm,
257257+ exp: Math.floor(Date.now() / 1000) + 60
258258+ })
259259+ return token
260260+ }
261261+})
262262+```
263263+264264+### With OAuth
265265+266266+```typescript
267267+import { OAuthClient } from '@atproto/oauth-client'
268268+import { KeyserverClient } from '@atpkeyserver/client'
269269+270270+const oauthClient = new OAuthClient({ /* config */ })
271271+await oauthClient.authorize({ scope: 'atproto transition:generic' })
272272+273273+const keyserver = new KeyserverClient({
274274+ keyserverDid: 'did:web:keyserver.example.com',
275275+ getServiceAuthToken: async (aud, lxm) => {
276276+ // Implement service auth token fetch with OAuth
277277+ // See ENCRYPTION_PROTOCOL.md for details
278278+ }
279279+})
280280+```
281281+282282+See [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md#authentication-flow) for complete service auth flow.
283283+284284+## Caching Behavior
285285+286286+### Key Caching
287287+288288+The client automatically caches keys in memory for performance:
289289+290290+- **Active keys:** 1 hour TTL (may change with rotation)
291291+- **Historical keys:** 24 hour TTL (immutable)
292292+- **LRU eviction:** When max size reached (default 1000 entries)
293293+294294+Cache keys are formatted as `{groupId}:{version}` for precise version tracking.
295295+296296+### Service Auth Token Caching
297297+298298+Service auth tokens are cached with short TTL:
299299+300300+- **TTL:** 60 seconds (or server-specified `exp`)
301301+- **Refresh:** Automatic when <10 seconds remain
302302+- **Memory-only:** Never persisted to disk
303303+304304+### Cache Security
305305+306306+- All caches are **memory-only** (never persisted)
307307+- Cleared automatically on logout (call `clearCache()`)
308308+- No sensitive data in cache keys
309309+310310+See [Security Best Practices](../../docs/SECURITY.md#token-caching) for details.
311311+312312+## Error Handling
313313+314314+### Error Types
315315+316316+```typescript
317317+import {
318318+ UnauthorizedError, // 401 - Invalid/expired auth token
319319+ ForbiddenError, // 403 - Not authorized for resource
320320+ NotFoundError, // 404 - Resource doesn't exist
321321+ NetworkError, // Network or server error
322322+ DecryptionError // Decryption failed
323323+} from '@atpkeyserver/client'
324324+```
325325+326326+### Handling Errors
327327+328328+```typescript
329329+try {
330330+ const plaintext = await keyserver.decrypt(groupId, messageId, ciphertext, version)
331331+ return JSON.parse(plaintext)
332332+} catch (error) {
333333+ if (error instanceof ForbiddenError) {
334334+ // User lost access to group
335335+ return { error: 'You no longer have access to this content' }
336336+ } else if (error instanceof NotFoundError) {
337337+ // Group was deleted
338338+ return { error: 'This group no longer exists' }
339339+ } else if (error instanceof DecryptionError) {
340340+ // Corrupted data or wrong key
341341+ return { error: 'Cannot decrypt this message' }
342342+ } else if (error instanceof NetworkError) {
343343+ // Temporary network issue - could retry
344344+ throw error
345345+ } else {
346346+ // Unknown error
347347+ throw error
348348+ }
349349+}
350350+```
351351+352352+### Automatic Retry
353353+354354+Network errors (5xx, timeouts) are automatically retried with exponential backoff:
355355+- Max retries: 3 attempts
356356+- Backoff: 100ms, 200ms, 400ms
357357+358358+Client errors (4xx) are NOT retried as they indicate permanent issues.
359359+360360+## Platform Compatibility
361361+362362+### Node.js
363363+364364+Requires Node.js 22+ with native crypto support.
365365+366366+```typescript
367367+import { encryptMessage } from '@atpkeyserver/client/crypto'
368368+```
369369+370370+### Browser
371371+372372+Works in all modern browsers with Web Crypto API:
373373+374374+```typescript
375375+import { KeyserverClient } from '@atpkeyserver/client'
376376+```
377377+378378+### React Native
379379+380380+Requires crypto polyfills. See examples/ directory for setup.
381381+382382+## Bundle Size
383383+384384+Tree-shaking automatically eliminates unused code:
385385+386386+| Import | Bundle Size (minified + gzipped) |
387387+|--------|----------------------------------|
388388+| `@atpkeyserver/client/crypto` | ~15KB |
389389+| `@atpkeyserver/client` | ~18KB |
390390+391391+The crypto library (@noble/ciphers) represents 90% of bundle size.
392392+393393+## Examples
394394+395395+Check the `examples/` directory for complete implementations:
396396+397397+- **basic-usage.ts** - Minimal crypto-only example
398398+- **react-hook.tsx** - React hooks for encrypted posts
399399+- **cli-example.ts** - Command-line encryption tool
400400+401401+## Security
402402+403403+### Best Practices
404404+405405+- Never log keys or tokens to console/files
406406+- Never persist keys to localStorage without encryption
407407+- Always use HTTPS for keyserver communication
408408+- Clear cache on logout with `clearCache()`
409409+- Use AT-URI as message ID for AAD binding
410410+411411+See [Security Best Practices](../../docs/SECURITY.md) for comprehensive guidelines.
412412+413413+### Threat Model
414414+415415+- **Client-side encryption**: Server never sees plaintext
416416+- **Short-lived tokens**: 60 second expiry limits attack window
417417+- **Audience binding**: Tokens valid only for specific keyserver
418418+- **No forward secrecy**: Old keys retained for compatibility (by design)
419419+420420+See [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md#security-considerations) for details.
421421+422422+## Contributing
423423+424424+Contributions welcome! Please:
425425+426426+1. Read [Client Implementation Strategy](../../docs/CLIENT_IMPLEMENTATION.md)
427427+2. Follow TypeScript and Prettier conventions
428428+3. Add tests for new features
429429+4. Update documentation as needed
4643047431## License
484324949-MIT
433433+See [LICENSE.md](../../LICENSE.md)
434434+435435+## Resources
436436+437437+- [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md) - Protocol specification
438438+- [Security Best Practices](../../docs/SECURITY.md) - Security guidelines
439439+- [API Reference](../../docs/API_REFERENCE.md) - Complete endpoint reference
440440+- [Client Implementation](../../docs/CLIENT_IMPLEMENTATION.md) - Design decisions
441441+- [Server Architecture](../../docs/ARCHITECTURE.md) - Server architecture
-36
packages/server/README.md
···54544. **SSL/TLS**: Terminate SSL at reverse proxy.
55555. **Monitoring**: Monitor database size and access logs regularly.
56565757-## Database
5858-5959-SQLite database (`keyserver.db`) is created automatically on first run with:
6060-- Automatic schema migrations
6161-- WAL mode enabled for better concurrency
6262-6363-**Note:** Ensure the database file is included in your backup strategy.
6464-6565-## API Endpoints
6666-6767-See [Architecture Documentation](../../docs/ARCHITECTURE.md#api-endpoints) for complete API reference.
6868-6969-### Public Endpoints
7070-- `GET /` - Service metadata
7171-- `GET /.well-known/did.json` - Service DID document
7272-- `GET /xrpc/dev.atpkeyserver.alpha.key.public` - Get public key (no auth)
7373-7474-### Protected Endpoints (require service auth token)
7575-- `GET /xrpc/dev.atpkeyserver.alpha.key` - Get keypair
7676-- `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` - Rotate keypair
7777-- `GET /xrpc/dev.atpkeyserver.alpha.key.versions` - List key versions
7878-- `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` - Get key access logs
7979-- `GET /xrpc/dev.atpkeyserver.alpha.group.key` - Get group key
8080-- `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` - Rotate group key
8181-- `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` - List group key versions
8282-- `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` - Add group member
8383-- `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` - Remove group member
8484-8585-## Security Considerations
8686-8787-- Server never performs encryption or decryption
8888-- All keys are stored in SQLite with proper access controls
8989-- JWT tokens are verified via ATProto signing keys
9090-- Basic access logging allows for security monitoring
9191-- Key versioning prevents data loss
9292-9357## License
94589559See [LICENSE.md](../../LICENSE.md)