QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
···11+# QuickDID Environment Configuration Template
22+# Copy this file to .env and customize for your deployment
33+#
44+# IMPORTANT: Never commit .env files with real SERVICE_KEY values
55+66+# ============================================================================
77+# REQUIRED CONFIGURATION
88+# ============================================================================
99+1010+# External hostname for service endpoints (REQUIRED)
1111+# Examples:
1212+# - quickdid.example.com
1313+# - quickdid.example.com:8080
1414+# - localhost:3007
1515+HTTP_EXTERNAL=quickdid.example.com
1616+1717+# Private key for service identity (REQUIRED)
1818+# SECURITY: Generate a new key for each environment
1919+# NEVER commit real keys to version control
2020+SERVICE_KEY=did:key:YOUR_PRIVATE_KEY_HERE
2121+2222+# ============================================================================
2323+# NETWORK CONFIGURATION
2424+# ============================================================================
2525+2626+# HTTP server port (default: 8080)
2727+HTTP_PORT=8080
2828+2929+# PLC directory hostname (default: plc.directory)
3030+# Use "plc.directory" for production
3131+PLC_HOSTNAME=plc.directory
3232+3333+# HTTP User-Agent header (optional)
3434+# Default: quickdid/{version} (+https://github.com/smokesignal.events/quickdid)
3535+# USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com)
3636+3737+# Custom DNS nameservers (optional, comma-separated)
3838+# Examples: 8.8.8.8,8.8.4.4 or 1.1.1.1,1.0.0.1
3939+# DNS_NAMESERVERS=
4040+4141+# Additional CA certificates (optional, comma-separated paths)
4242+# CERTIFICATE_BUNDLES=
4343+4444+# ============================================================================
4545+# CACHING CONFIGURATION
4646+# ============================================================================
4747+4848+# Redis URL for caching (optional but recommended for production)
4949+# Examples:
5050+# - redis://localhost:6379/0
5151+# - redis://user:pass@redis.example.com:6379/0
5252+# - rediss://secure-redis.example.com:6380/0
5353+# REDIS_URL=redis://localhost:6379/0
5454+5555+# TTL for in-memory cache in seconds (default: 600 = 10 minutes)
5656+# Lower = fresher data, higher = better performance
5757+# Range: 60-3600 recommended
5858+CACHE_TTL_MEMORY=600
5959+6060+# TTL for Redis cache in seconds (default: 7776000 = 90 days)
6161+# Recommendations:
6262+# - 86400 (1 day) for frequently changing data
6363+# - 604800 (1 week) for balanced performance
6464+# - 7776000 (90 days) for stable data
6565+CACHE_TTL_REDIS=86400
6666+6767+# ============================================================================
6868+# QUEUE CONFIGURATION
6969+# ============================================================================
7070+7171+# Queue adapter type (default: mpsc)
7272+# Options:
7373+# - mpsc: In-memory queue (single instance)
7474+# - redis: Distributed queue (multi-instance)
7575+# - noop: Disable queue (testing only)
7676+QUEUE_ADAPTER=mpsc
7777+7878+# Redis URL for queue operations (optional)
7979+# Falls back to REDIS_URL if not specified
8080+# Use when separating cache and queue Redis instances
8181+# QUEUE_REDIS_URL=
8282+8383+# Redis key prefix for queues (default: queue:handleresolver:)
8484+# Useful for namespacing when sharing Redis
8585+QUEUE_REDIS_PREFIX=queue:handleresolver:
8686+8787+# Redis blocking timeout in seconds (default: 5)
8888+# Lower = more responsive, higher = less polling
8989+QUEUE_REDIS_TIMEOUT=5
9090+9191+# Worker ID for queue operations (optional)
9292+# Default: auto-generated UUID
9393+# Examples: worker-001, prod-us-east-1, $(hostname)
9494+# QUEUE_WORKER_ID=
9595+9696+# Buffer size for MPSC queue (default: 1000)
9797+# Increase for high-traffic deployments
9898+QUEUE_BUFFER_SIZE=1000
9999+100100+# ============================================================================
101101+# LOGGING
102102+# ============================================================================
103103+104104+# Rust log level
105105+# Options: trace, debug, info, warn, error
106106+# Production: info or warn
107107+# Development: debug
108108+RUST_LOG=info
109109+110110+# ============================================================================
111111+# DEVELOPMENT OVERRIDES (uncomment for local development)
112112+# ============================================================================
113113+114114+# HTTP_EXTERNAL=localhost:3007
115115+# SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
116116+# RUST_LOG=debug
117117+# CACHE_TTL_MEMORY=60
118118+# CACHE_TTL_REDIS=300
+542
docs/configuration-reference.md
···11+# QuickDID Configuration Reference
22+33+This document provides a comprehensive reference for all configuration options available in QuickDID.
44+55+## Table of Contents
66+77+- [Required Configuration](#required-configuration)
88+- [Network Configuration](#network-configuration)
99+- [Caching Configuration](#caching-configuration)
1010+- [Queue Configuration](#queue-configuration)
1111+- [Security Configuration](#security-configuration)
1212+- [Advanced Configuration](#advanced-configuration)
1313+- [Configuration Examples](#configuration-examples)
1414+- [Validation Rules](#validation-rules)
1515+1616+## Required Configuration
1717+1818+These environment variables MUST be set for QuickDID to start.
1919+2020+### `HTTP_EXTERNAL`
2121+2222+**Required**: Yes
2323+**Type**: String
2424+**Format**: Hostname with optional port
2525+2626+The external hostname where this service will be accessible. This is used to generate the service DID and for AT Protocol identity resolution.
2727+2828+**Examples**:
2929+```bash
3030+# Production domain
3131+HTTP_EXTERNAL=quickdid.example.com
3232+3333+# With non-standard port
3434+HTTP_EXTERNAL=quickdid.example.com:8080
3535+3636+# Development/testing
3737+HTTP_EXTERNAL=localhost:3007
3838+```
3939+4040+**Constraints**:
4141+- Must be a valid hostname or hostname:port combination
4242+- Port (if specified) must be between 1-65535
4343+- Used to generate service DID (did:web:{HTTP_EXTERNAL})
4444+4545+### `SERVICE_KEY`
4646+4747+**Required**: Yes
4848+**Type**: String
4949+**Format**: DID private key
5050+**Security**: SENSITIVE - Never commit to version control
5151+5252+The private key for the service's AT Protocol identity. This key is used to sign responses and authenticate the service.
5353+5454+**Examples**:
5555+```bash
5656+# did:key format (Ed25519)
5757+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
5858+5959+# did:plc format
6060+SERVICE_KEY=did:plc:xyz123abc456def789
6161+```
6262+6363+**Constraints**:
6464+- Must be a valid DID format
6565+- Must include the private key component
6666+- Should be stored securely (e.g., secrets manager, encrypted storage)
6767+6868+## Network Configuration
6969+7070+### `HTTP_PORT`
7171+7272+**Required**: No
7373+**Type**: String
7474+**Default**: `8080`
7575+**Range**: 1-65535
7676+7777+The port number for the HTTP server to bind to.
7878+7979+**Examples**:
8080+```bash
8181+HTTP_PORT=8080 # Default
8282+HTTP_PORT=3000 # Common alternative
8383+HTTP_PORT=80 # Standard HTTP (requires root/privileges)
8484+```
8585+8686+### `PLC_HOSTNAME`
8787+8888+**Required**: No
8989+**Type**: String
9090+**Default**: `plc.directory`
9191+9292+The hostname of the PLC directory service for DID resolution.
9393+9494+**Examples**:
9595+```bash
9696+PLC_HOSTNAME=plc.directory # Production (default)
9797+PLC_HOSTNAME=test.plc.directory # Testing environment
9898+PLC_HOSTNAME=localhost:2582 # Local PLC server
9999+```
100100+101101+### `DNS_NAMESERVERS`
102102+103103+**Required**: No
104104+**Type**: String (comma-separated IP addresses)
105105+**Default**: System DNS
106106+107107+Custom DNS nameservers for handle resolution via TXT records.
108108+109109+**Examples**:
110110+```bash
111111+# Google DNS
112112+DNS_NAMESERVERS=8.8.8.8,8.8.4.4
113113+114114+# Cloudflare DNS
115115+DNS_NAMESERVERS=1.1.1.1,1.0.0.1
116116+117117+# Multiple providers
118118+DNS_NAMESERVERS=8.8.8.8,1.1.1.1
119119+120120+# Local DNS
121121+DNS_NAMESERVERS=192.168.1.1
122122+```
123123+124124+### `USER_AGENT`
125125+126126+**Required**: No
127127+**Type**: String
128128+**Default**: `quickdid/{version} (+https://github.com/smokesignal.events/quickdid)`
129129+130130+HTTP User-Agent header for outgoing requests.
131131+132132+**Examples**:
133133+```bash
134134+# Custom agent
135135+USER_AGENT="MyService/1.0.0 (+https://myservice.com)"
136136+137137+# With contact info
138138+USER_AGENT="quickdid/1.0.0 (+https://quickdid.example.com; admin@example.com)"
139139+```
140140+141141+### `CERTIFICATE_BUNDLES`
142142+143143+**Required**: No
144144+**Type**: String (comma-separated file paths)
145145+**Default**: System CA certificates
146146+147147+Additional CA certificate bundles for TLS connections.
148148+149149+**Examples**:
150150+```bash
151151+# Single certificate
152152+CERTIFICATE_BUNDLES=/etc/ssl/certs/custom-ca.pem
153153+154154+# Multiple certificates
155155+CERTIFICATE_BUNDLES=/certs/ca1.pem,/certs/ca2.pem
156156+157157+# Corporate CA
158158+CERTIFICATE_BUNDLES=/usr/local/share/ca-certificates/corporate-ca.crt
159159+```
160160+161161+## Caching Configuration
162162+163163+### `REDIS_URL`
164164+165165+**Required**: No (but highly recommended for production)
166166+**Type**: String
167167+**Format**: Redis connection URL
168168+169169+Redis connection URL for persistent caching. Enables distributed caching and better performance.
170170+171171+**Examples**:
172172+```bash
173173+# Local Redis (no auth)
174174+REDIS_URL=redis://localhost:6379/0
175175+176176+# With authentication
177177+REDIS_URL=redis://user:password@redis.example.com:6379/0
178178+179179+# Using database 1
180180+REDIS_URL=redis://localhost:6379/1
181181+182182+# Redis Sentinel
183183+REDIS_URL=redis-sentinel://sentinel1:26379,sentinel2:26379/mymaster/0
184184+185185+# TLS connection
186186+REDIS_URL=rediss://secure-redis.example.com:6380/0
187187+```
188188+189189+### `CACHE_TTL_MEMORY`
190190+191191+**Required**: No
192192+**Type**: Integer (seconds)
193193+**Default**: `600` (10 minutes)
194194+**Range**: 60-3600 (recommended)
195195+**Constraints**: Must be > 0
196196+197197+Time-to-live for in-memory cache entries in seconds. Used when Redis is not available.
198198+199199+**Examples**:
200200+```bash
201201+CACHE_TTL_MEMORY=300 # 5 minutes (aggressive refresh)
202202+CACHE_TTL_MEMORY=600 # 10 minutes (default, balanced)
203203+CACHE_TTL_MEMORY=1800 # 30 minutes (less frequent updates)
204204+CACHE_TTL_MEMORY=3600 # 1 hour (stable data)
205205+```
206206+207207+**Recommendations**:
208208+- Lower values: Fresher data, more DNS/HTTP lookups, higher load
209209+- Higher values: Better performance, potentially stale data
210210+- Production with Redis: Can use lower values (300-600)
211211+- Production without Redis: Use higher values (1800-3600)
212212+213213+### `CACHE_TTL_REDIS`
214214+215215+**Required**: No
216216+**Type**: Integer (seconds)
217217+**Default**: `7776000` (90 days)
218218+**Range**: 3600-31536000 (1 hour to 1 year)
219219+**Constraints**: Must be > 0
220220+221221+Time-to-live for Redis cache entries in seconds.
222222+223223+**Examples**:
224224+```bash
225225+CACHE_TTL_REDIS=3600 # 1 hour (frequently changing data)
226226+CACHE_TTL_REDIS=86400 # 1 day (recommended for active handles)
227227+CACHE_TTL_REDIS=604800 # 1 week (balanced)
228228+CACHE_TTL_REDIS=2592000 # 30 days (stable handles)
229229+CACHE_TTL_REDIS=7776000 # 90 days (default, maximum stability)
230230+```
231231+232232+**Recommendations**:
233233+- Social media handles: 1-7 days
234234+- Corporate/stable handles: 30-90 days
235235+- Test environments: 1 hour
236236+237237+## Queue Configuration
238238+239239+### `QUEUE_ADAPTER`
240240+241241+**Required**: No
242242+**Type**: String
243243+**Default**: `mpsc`
244244+**Values**: `mpsc`, `redis`, `noop`
245245+246246+The type of queue adapter for background handle resolution.
247247+248248+**Options**:
249249+- `mpsc`: In-memory multi-producer single-consumer queue (default)
250250+- `redis`: Redis-backed distributed queue
251251+- `noop`: Disable queue processing (testing only)
252252+253253+**Examples**:
254254+```bash
255255+# Single instance deployment
256256+QUEUE_ADAPTER=mpsc
257257+258258+# Multi-instance or high availability
259259+QUEUE_ADAPTER=redis
260260+261261+# Testing without background processing
262262+QUEUE_ADAPTER=noop
263263+```
264264+265265+### `QUEUE_REDIS_URL`
266266+267267+**Required**: No
268268+**Type**: String
269269+**Default**: Falls back to `REDIS_URL`
270270+271271+Dedicated Redis URL for queue operations. Use when separating cache and queue Redis instances.
272272+273273+**Examples**:
274274+```bash
275275+# Separate Redis for queues
276276+QUEUE_REDIS_URL=redis://queue-redis:6379/2
277277+278278+# With different credentials
279279+QUEUE_REDIS_URL=redis://queue_user:queue_pass@redis.example.com:6379/1
280280+```
281281+282282+### `QUEUE_REDIS_PREFIX`
283283+284284+**Required**: No
285285+**Type**: String
286286+**Default**: `queue:handleresolver:`
287287+288288+Redis key prefix for queue operations. Use to namespace queues when sharing Redis.
289289+290290+**Examples**:
291291+```bash
292292+# Default
293293+QUEUE_REDIS_PREFIX=queue:handleresolver:
294294+295295+# Environment-specific
296296+QUEUE_REDIS_PREFIX=prod:queue:hr:
297297+QUEUE_REDIS_PREFIX=staging:queue:hr:
298298+299299+# Version-specific
300300+QUEUE_REDIS_PREFIX=quickdid:v1:queue:
301301+302302+# Instance-specific
303303+QUEUE_REDIS_PREFIX=us-east-1:queue:hr:
304304+```
305305+306306+### `QUEUE_REDIS_TIMEOUT`
307307+308308+**Required**: No
309309+**Type**: Integer (seconds)
310310+**Default**: `5`
311311+**Range**: 1-60 (recommended)
312312+**Constraints**: Must be > 0
313313+314314+Redis blocking timeout for queue operations in seconds. Controls how long to wait for new items.
315315+316316+**Examples**:
317317+```bash
318318+QUEUE_REDIS_TIMEOUT=1 # Very responsive, more polling
319319+QUEUE_REDIS_TIMEOUT=5 # Default, balanced
320320+QUEUE_REDIS_TIMEOUT=10 # Less polling, slower shutdown
321321+QUEUE_REDIS_TIMEOUT=30 # Minimal polling, slow shutdown
322322+```
323323+324324+### `QUEUE_WORKER_ID`
325325+326326+**Required**: No
327327+**Type**: String
328328+**Default**: Auto-generated UUID
329329+330330+Worker identifier for queue operations. Used in logs and monitoring.
331331+332332+**Examples**:
333333+```bash
334334+# Simple numbering
335335+QUEUE_WORKER_ID=worker-001
336336+337337+# Environment-based
338338+QUEUE_WORKER_ID=prod-us-east-1
339339+QUEUE_WORKER_ID=staging-worker-2
340340+341341+# Hostname-based
342342+QUEUE_WORKER_ID=$(hostname)
343343+344344+# Pod name in Kubernetes
345345+QUEUE_WORKER_ID=$HOSTNAME
346346+```
347347+348348+### `QUEUE_BUFFER_SIZE`
349349+350350+**Required**: No
351351+**Type**: Integer
352352+**Default**: `1000`
353353+**Range**: 100-100000 (recommended)
354354+355355+Buffer size for the MPSC queue adapter. Only used when `QUEUE_ADAPTER=mpsc`.
356356+357357+**Examples**:
358358+```bash
359359+QUEUE_BUFFER_SIZE=100 # Minimal memory, may block
360360+QUEUE_BUFFER_SIZE=1000 # Default, balanced
361361+QUEUE_BUFFER_SIZE=5000 # High traffic
362362+QUEUE_BUFFER_SIZE=10000 # Very high traffic
363363+```
364364+365365+## Configuration Examples
366366+367367+### Minimal Development Configuration
368368+369369+```bash
370370+# .env.development
371371+HTTP_EXTERNAL=localhost:3007
372372+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
373373+RUST_LOG=debug
374374+```
375375+376376+### Standard Production Configuration
377377+378378+```bash
379379+# .env.production
380380+# Required
381381+HTTP_EXTERNAL=quickdid.example.com
382382+SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager
383383+384384+# Network
385385+HTTP_PORT=8080
386386+USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com)
387387+388388+# Caching
389389+REDIS_URL=redis://redis:6379/0
390390+CACHE_TTL_MEMORY=600
391391+CACHE_TTL_REDIS=86400 # 1 day
392392+393393+# Queue
394394+QUEUE_ADAPTER=redis
395395+QUEUE_REDIS_TIMEOUT=5
396396+QUEUE_BUFFER_SIZE=5000
397397+398398+# Logging
399399+RUST_LOG=info
400400+```
401401+402402+### High-Availability Configuration
403403+404404+```bash
405405+# .env.ha
406406+# Required
407407+HTTP_EXTERNAL=quickdid.example.com
408408+SERVICE_KEY=${SECRET_SERVICE_KEY}
409409+410410+# Network
411411+HTTP_PORT=8080
412412+DNS_NAMESERVERS=8.8.8.8,8.8.4.4,1.1.1.1,1.0.0.1
413413+414414+# Caching (separate Redis instances)
415415+REDIS_URL=redis://cache-redis:6379/0
416416+CACHE_TTL_MEMORY=300
417417+CACHE_TTL_REDIS=3600
418418+419419+# Queue (dedicated Redis)
420420+QUEUE_ADAPTER=redis
421421+QUEUE_REDIS_URL=redis://queue-redis:6379/0
422422+QUEUE_REDIS_PREFIX=prod:queue:
423423+QUEUE_WORKER_ID=${HOSTNAME}
424424+QUEUE_REDIS_TIMEOUT=10
425425+426426+# Performance
427427+QUEUE_BUFFER_SIZE=10000
428428+429429+# Logging
430430+RUST_LOG=warn
431431+```
432432+433433+### Docker Compose Configuration
434434+435435+```yaml
436436+version: '3.8'
437437+438438+services:
439439+ quickdid:
440440+ image: quickdid:latest
441441+ environment:
442442+ HTTP_EXTERNAL: quickdid.example.com
443443+ SERVICE_KEY: ${SERVICE_KEY}
444444+ HTTP_PORT: 8080
445445+ REDIS_URL: redis://redis:6379/0
446446+ CACHE_TTL_MEMORY: 600
447447+ CACHE_TTL_REDIS: 86400
448448+ QUEUE_ADAPTER: redis
449449+ QUEUE_REDIS_TIMEOUT: 5
450450+ RUST_LOG: info
451451+ ports:
452452+ - "8080:8080"
453453+ depends_on:
454454+ - redis
455455+456456+ redis:
457457+ image: redis:7-alpine
458458+ command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
459459+```
460460+461461+## Validation Rules
462462+463463+QuickDID validates configuration at startup. The following rules are enforced:
464464+465465+### Required Fields
466466+467467+1. **HTTP_EXTERNAL**: Must be provided
468468+2. **SERVICE_KEY**: Must be provided
469469+470470+### Value Constraints
471471+472472+1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`):
473473+ - Must be positive integers (> 0)
474474+ - Recommended minimum: 60 seconds
475475+476476+2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`):
477477+ - Must be positive integers (> 0)
478478+ - Recommended range: 1-60 seconds
479479+480480+3. **Queue Adapter** (`QUEUE_ADAPTER`):
481481+ - Must be one of: `mpsc`, `redis`, `noop`, `none`
482482+ - Case-sensitive
483483+484484+4. **Port** (`HTTP_PORT`):
485485+ - Must be valid port number (1-65535)
486486+ - Ports < 1024 require elevated privileges
487487+488488+### Validation Errors
489489+490490+If validation fails, QuickDID will exit with one of these error codes:
491491+492492+- `error-quickdid-config-1`: Missing required environment variable
493493+- `error-quickdid-config-2`: Invalid configuration value
494494+- `error-quickdid-config-3`: Invalid TTL value (must be positive)
495495+- `error-quickdid-config-4`: Invalid timeout value (must be positive)
496496+497497+### Testing Configuration
498498+499499+Test your configuration without starting the service:
500500+501501+```bash
502502+# Validate configuration
503503+HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help
504504+505505+# Test with specific values
506506+CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation
507507+508508+# Check parsed configuration (with debug logging)
509509+RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid
510510+```
511511+512512+## Best Practices
513513+514514+### Security
515515+516516+1. **Never commit SERVICE_KEY** to version control
517517+2. Use environment-specific key management (Vault, AWS Secrets, etc.)
518518+3. Rotate SERVICE_KEY regularly
519519+4. Use TLS for Redis connections in production (`rediss://`)
520520+5. Implement network segmentation for Redis access
521521+522522+### Performance
523523+524524+1. **With Redis**: Use lower memory cache TTL (300-600s)
525525+2. **Without Redis**: Use higher memory cache TTL (1800-3600s)
526526+3. **High traffic**: Increase QUEUE_BUFFER_SIZE (5000-10000)
527527+4. **Multi-region**: Use region-specific QUEUE_WORKER_ID
528528+529529+### Monitoring
530530+531531+1. Set descriptive QUEUE_WORKER_ID for log correlation
532532+2. Use structured logging with appropriate RUST_LOG levels
533533+3. Monitor Redis memory usage and adjust TTLs accordingly
534534+4. Track cache hit rates to optimize TTL values
535535+536536+### Deployment
537537+538538+1. Use `.env` files for local development
539539+2. Use secrets management for production SERVICE_KEY
540540+3. Set resource limits in container orchestration
541541+4. Use health checks to monitor service availability
542542+5. Implement gradual rollouts with feature flags
+706
docs/production-deployment.md
···11+# QuickDID Production Deployment Guide
22+33+This guide provides comprehensive instructions for deploying QuickDID in a production environment using Docker.
44+55+## Table of Contents
66+77+- [Prerequisites](#prerequisites)
88+- [Environment Configuration](#environment-configuration)
99+- [Docker Deployment](#docker-deployment)
1010+- [Docker Compose Setup](#docker-compose-setup)
1111+- [Health Monitoring](#health-monitoring)
1212+- [Security Considerations](#security-considerations)
1313+- [Troubleshooting](#troubleshooting)
1414+1515+## Prerequisites
1616+1717+- Docker 20.10.0 or higher
1818+- Docker Compose 2.0.0 or higher (optional, for multi-container setup)
1919+- Redis 6.0 or higher (optional, for persistent caching and queue management)
2020+- Valid SSL certificates for HTTPS (recommended for production)
2121+- Domain name configured with appropriate DNS records
2222+2323+## Environment Configuration
2424+2525+Create a `.env` file in your deployment directory with the following configuration:
2626+2727+```bash
2828+# ============================================================================
2929+# QuickDID Production Environment Configuration
3030+# ============================================================================
3131+3232+# ----------------------------------------------------------------------------
3333+# REQUIRED CONFIGURATION
3434+# ----------------------------------------------------------------------------
3535+3636+# External hostname for service endpoints
3737+# This should be your public domain name with port if non-standard
3838+# Examples:
3939+# - quickdid.example.com
4040+# - quickdid.example.com:8080
4141+# - localhost:3007 (for testing only)
4242+HTTP_EXTERNAL=quickdid.example.com
4343+4444+# Private key for service identity (DID format)
4545+# Generate a new key for production using atproto-identity tools
4646+# SECURITY: Keep this key secure and never commit to version control
4747+# Example formats:
4848+# - did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
4949+# - did:plc:xyz123abc456
5050+SERVICE_KEY=did:key:YOUR_PRODUCTION_KEY_HERE
5151+5252+# ----------------------------------------------------------------------------
5353+# NETWORK CONFIGURATION
5454+# ----------------------------------------------------------------------------
5555+5656+# HTTP server port (default: 8080)
5757+# This is the port the service will bind to inside the container
5858+# Map this to your desired external port in docker-compose.yml
5959+HTTP_PORT=8080
6060+6161+# PLC directory hostname (default: plc.directory)
6262+# Change this if using a custom PLC directory or testing environment
6363+PLC_HOSTNAME=plc.directory
6464+6565+# ----------------------------------------------------------------------------
6666+# CACHING CONFIGURATION
6767+# ----------------------------------------------------------------------------
6868+6969+# Redis connection URL for caching (highly recommended for production)
7070+# Format: redis://[username:password@]host:port/database
7171+# Examples:
7272+# - redis://localhost:6379/0 (local Redis, no auth)
7373+# - redis://user:pass@redis.example.com:6379/0 (remote with auth)
7474+# - redis://redis:6379/0 (Docker network)
7575+# - rediss://secure-redis.example.com:6380/0 (TLS)
7676+# Benefits: Persistent cache, distributed caching, better performance
7777+REDIS_URL=redis://redis:6379/0
7878+7979+# TTL for in-memory cache in seconds (default: 600 = 10 minutes)
8080+# Range: 60-3600 recommended
8181+# Lower = fresher data, more DNS/HTTP lookups
8282+# Higher = better performance, potentially stale data
8383+CACHE_TTL_MEMORY=600
8484+8585+# TTL for Redis cache in seconds (default: 7776000 = 90 days)
8686+# Range: 3600-31536000 (1 hour to 1 year)
8787+# Recommendations:
8888+# - 86400 (1 day) for frequently changing data
8989+# - 604800 (1 week) for balanced performance
9090+# - 7776000 (90 days) for stable data
9191+CACHE_TTL_REDIS=86400
9292+9393+# ----------------------------------------------------------------------------
9494+# QUEUE CONFIGURATION
9595+# ----------------------------------------------------------------------------
9696+9797+# Queue adapter type: 'mpsc', 'redis', or 'noop' (default: mpsc)
9898+# - 'mpsc': In-memory queue for single-instance deployments
9999+# - 'redis': Distributed queue for multi-instance or HA deployments
100100+# - 'noop': Disable queue processing (testing only)
101101+QUEUE_ADAPTER=redis
102102+103103+# Redis URL for queue adapter (uses REDIS_URL if not set)
104104+# Set this if you want to use a separate Redis instance for queuing
105105+# QUEUE_REDIS_URL=redis://queue-redis:6379/1
106106+107107+# Redis key prefix for queues (default: queue:handleresolver:)
108108+# Useful when sharing Redis instance with other services
109109+QUEUE_REDIS_PREFIX=queue:quickdid:prod:
110110+111111+# Redis blocking timeout for queue operations in seconds (default: 5)
112112+# Range: 1-60 recommended
113113+# Lower = more responsive to shutdown, more polling
114114+# Higher = less polling overhead, slower shutdown
115115+QUEUE_REDIS_TIMEOUT=5
116116+117117+# Worker ID for Redis queue (auto-generated UUID if not set)
118118+# Set this for predictable worker identification in multi-instance deployments
119119+# Examples: worker-001, prod-us-east-1, $(hostname)
120120+# QUEUE_WORKER_ID=worker-001
121121+122122+# Buffer size for MPSC queue (default: 1000)
123123+# Range: 100-100000
124124+# Increase for high-traffic deployments using MPSC adapter
125125+QUEUE_BUFFER_SIZE=5000
126126+127127+# ----------------------------------------------------------------------------
128128+# HTTP CLIENT CONFIGURATION
129129+# ----------------------------------------------------------------------------
130130+131131+# HTTP User-Agent header
132132+# Identifies your service to other AT Protocol services
133133+# Default: Auto-generated with current version from Cargo.toml
134134+# Format: quickdid/{version} (+https://github.com/smokesignal.events/quickdid)
135135+USER_AGENT=quickdid/1.0.0-rc.1 (+https://quickdid.example.com)
136136+137137+# Custom DNS nameservers (comma-separated)
138138+# Use for custom DNS resolution or to bypass local DNS
139139+# Examples:
140140+# - 8.8.8.8,8.8.4.4 (Google DNS)
141141+# - 1.1.1.1,1.0.0.1 (Cloudflare DNS)
142142+# DNS_NAMESERVERS=1.1.1.1,1.0.0.1
143143+144144+# Additional CA certificates (comma-separated file paths)
145145+# Use when connecting to services with custom CA certificates
146146+# CERTIFICATE_BUNDLES=/certs/custom-ca.pem,/certs/internal-ca.pem
147147+148148+# ----------------------------------------------------------------------------
149149+# LOGGING AND MONITORING
150150+# ----------------------------------------------------------------------------
151151+152152+# Logging level (debug, info, warn, error)
153153+# Use 'info' for production, 'debug' for troubleshooting
154154+RUST_LOG=info
155155+156156+# Structured logging format (optional)
157157+# Set to 'json' for machine-readable logs
158158+# RUST_LOG_FORMAT=json
159159+160160+# ----------------------------------------------------------------------------
161161+# PERFORMANCE TUNING
162162+# ----------------------------------------------------------------------------
163163+164164+# Tokio runtime worker threads (defaults to CPU count)
165165+# Adjust based on your container's CPU allocation
166166+# TOKIO_WORKER_THREADS=4
167167+168168+# Maximum concurrent connections (optional)
169169+# Helps prevent resource exhaustion
170170+# MAX_CONNECTIONS=10000
171171+172172+# ----------------------------------------------------------------------------
173173+# DOCKER-SPECIFIC CONFIGURATION
174174+# ----------------------------------------------------------------------------
175175+176176+# Container restart policy (for docker-compose)
177177+# Options: no, always, on-failure, unless-stopped
178178+RESTART_POLICY=unless-stopped
179179+180180+# Resource limits (for docker-compose)
181181+# Adjust based on your available resources
182182+MEMORY_LIMIT=512M
183183+CPU_LIMIT=1.0
184184+```
185185+186186+## Docker Deployment
187187+188188+### Building the Docker Image
189189+190190+Create a `Dockerfile` in your project root:
191191+192192+```dockerfile
193193+# Build stage
194194+FROM rust:1.75-slim AS builder
195195+196196+# Install build dependencies
197197+RUN apt-get update && apt-get install -y \
198198+ pkg-config \
199199+ libssl-dev \
200200+ && rm -rf /var/lib/apt/lists/*
201201+202202+# Create app directory
203203+WORKDIR /app
204204+205205+# Copy source files
206206+COPY Cargo.toml Cargo.lock ./
207207+COPY src ./src
208208+209209+# Build the application
210210+RUN cargo build --release
211211+212212+# Runtime stage
213213+FROM debian:bookworm-slim
214214+215215+# Install runtime dependencies
216216+RUN apt-get update && apt-get install -y \
217217+ ca-certificates \
218218+ libssl3 \
219219+ curl \
220220+ && rm -rf /var/lib/apt/lists/*
221221+222222+# Create non-root user
223223+RUN useradd -m -u 1000 quickdid
224224+225225+# Copy binary from builder
226226+COPY --from=builder /app/target/release/quickdid /usr/local/bin/quickdid
227227+228228+# Set ownership and permissions
229229+RUN chown quickdid:quickdid /usr/local/bin/quickdid
230230+231231+# Switch to non-root user
232232+USER quickdid
233233+234234+# Expose default port
235235+EXPOSE 8080
236236+237237+# Health check
238238+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
239239+ CMD curl -f http://localhost:8080/health || exit 1
240240+241241+# Run the application
242242+ENTRYPOINT ["quickdid"]
243243+```
244244+245245+Build the image:
246246+247247+```bash
248248+docker build -t quickdid:latest .
249249+```
250250+251251+### Running a Single Instance
252252+253253+```bash
254254+# Run with environment file
255255+docker run -d \
256256+ --name quickdid \
257257+ --env-file .env \
258258+ -p 8080:8080 \
259259+ --restart unless-stopped \
260260+ quickdid:latest
261261+```
262262+263263+## Docker Compose Setup
264264+265265+Create a `docker-compose.yml` file for a complete production setup:
266266+267267+```yaml
268268+version: '3.8'
269269+270270+services:
271271+ quickdid:
272272+ image: quickdid:latest
273273+ container_name: quickdid
274274+ env_file: .env
275275+ ports:
276276+ - "8080:8080"
277277+ depends_on:
278278+ redis:
279279+ condition: service_healthy
280280+ networks:
281281+ - quickdid-network
282282+ restart: ${RESTART_POLICY:-unless-stopped}
283283+ deploy:
284284+ resources:
285285+ limits:
286286+ memory: ${MEMORY_LIMIT:-512M}
287287+ cpus: ${CPU_LIMIT:-1.0}
288288+ reservations:
289289+ memory: 256M
290290+ cpus: '0.5'
291291+ healthcheck:
292292+ test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
293293+ interval: 30s
294294+ timeout: 3s
295295+ retries: 3
296296+ start_period: 10s
297297+ logging:
298298+ driver: "json-file"
299299+ options:
300300+ max-size: "10m"
301301+ max-file: "3"
302302+303303+ redis:
304304+ image: redis:7-alpine
305305+ container_name: quickdid-redis
306306+ command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
307307+ volumes:
308308+ - redis-data:/data
309309+ networks:
310310+ - quickdid-network
311311+ restart: unless-stopped
312312+ healthcheck:
313313+ test: ["CMD", "redis-cli", "ping"]
314314+ interval: 10s
315315+ timeout: 3s
316316+ retries: 3
317317+ logging:
318318+ driver: "json-file"
319319+ options:
320320+ max-size: "10m"
321321+ max-file: "3"
322322+323323+ # Optional: Nginx reverse proxy with SSL
324324+ nginx:
325325+ image: nginx:alpine
326326+ container_name: quickdid-nginx
327327+ ports:
328328+ - "80:80"
329329+ - "443:443"
330330+ volumes:
331331+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
332332+ - ./certs:/etc/nginx/certs:ro
333333+ - ./acme-challenge:/var/www/acme:ro
334334+ depends_on:
335335+ - quickdid
336336+ networks:
337337+ - quickdid-network
338338+ restart: unless-stopped
339339+ logging:
340340+ driver: "json-file"
341341+ options:
342342+ max-size: "10m"
343343+ max-file: "3"
344344+345345+networks:
346346+ quickdid-network:
347347+ driver: bridge
348348+349349+volumes:
350350+ redis-data:
351351+ driver: local
352352+```
353353+354354+### Nginx Configuration (nginx.conf)
355355+356356+```nginx
357357+events {
358358+ worker_connections 1024;
359359+}
360360+361361+http {
362362+ upstream quickdid {
363363+ server quickdid:8080;
364364+ }
365365+366366+ server {
367367+ listen 80;
368368+ server_name quickdid.example.com;
369369+370370+ # ACME challenge for Let's Encrypt
371371+ location /.well-known/acme-challenge/ {
372372+ root /var/www/acme;
373373+ }
374374+375375+ # Redirect HTTP to HTTPS
376376+ location / {
377377+ return 301 https://$server_name$request_uri;
378378+ }
379379+ }
380380+381381+ server {
382382+ listen 443 ssl http2;
383383+ server_name quickdid.example.com;
384384+385385+ ssl_certificate /etc/nginx/certs/fullchain.pem;
386386+ ssl_certificate_key /etc/nginx/certs/privkey.pem;
387387+ ssl_protocols TLSv1.2 TLSv1.3;
388388+ ssl_ciphers HIGH:!aNULL:!MD5;
389389+390390+ location / {
391391+ proxy_pass http://quickdid;
392392+ proxy_set_header Host $host;
393393+ proxy_set_header X-Real-IP $remote_addr;
394394+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
395395+ proxy_set_header X-Forwarded-Proto $scheme;
396396+397397+ # WebSocket support (if needed)
398398+ proxy_http_version 1.1;
399399+ proxy_set_header Upgrade $http_upgrade;
400400+ proxy_set_header Connection "upgrade";
401401+402402+ # Timeouts
403403+ proxy_connect_timeout 60s;
404404+ proxy_send_timeout 60s;
405405+ proxy_read_timeout 60s;
406406+ }
407407+408408+ # Health check endpoint
409409+ location /health {
410410+ proxy_pass http://quickdid/health;
411411+ access_log off;
412412+ }
413413+ }
414414+}
415415+```
416416+417417+### Starting the Stack
418418+419419+```bash
420420+# Start all services
421421+docker-compose up -d
422422+423423+# View logs
424424+docker-compose logs -f
425425+426426+# Check service status
427427+docker-compose ps
428428+429429+# Stop all services
430430+docker-compose down
431431+```
432432+433433+## Health Monitoring
434434+435435+QuickDID provides health check endpoints for monitoring:
436436+437437+### Basic Health Check
438438+439439+```bash
440440+curl http://quickdid.example.com/health
441441+```
442442+443443+Expected response:
444444+```json
445445+{
446446+ "status": "healthy",
447447+ "version": "1.0.0",
448448+ "uptime_seconds": 3600
449449+}
450450+```
451451+452452+### Monitoring with Prometheus (Optional)
453453+454454+Add to your `docker-compose.yml`:
455455+456456+```yaml
457457+ prometheus:
458458+ image: prom/prometheus:latest
459459+ container_name: quickdid-prometheus
460460+ volumes:
461461+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
462462+ - prometheus-data:/prometheus
463463+ command:
464464+ - '--config.file=/etc/prometheus/prometheus.yml'
465465+ - '--storage.tsdb.path=/prometheus'
466466+ ports:
467467+ - "9090:9090"
468468+ networks:
469469+ - quickdid-network
470470+ restart: unless-stopped
471471+472472+volumes:
473473+ prometheus-data:
474474+ driver: local
475475+```
476476+477477+## Security Considerations
478478+479479+### 1. Service Key Protection
480480+481481+- **Never commit** the `SERVICE_KEY` to version control
482482+- Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager)
483483+- Rotate keys regularly
484484+- Use different keys for different environments
485485+486486+### 2. Network Security
487487+488488+- Use HTTPS in production with valid SSL certificates
489489+- Implement rate limiting at the reverse proxy level
490490+- Use firewall rules to restrict access to Redis
491491+- Enable Redis authentication in production
492492+493493+### 3. Container Security
494494+495495+- Run containers as non-root user (already configured in Dockerfile)
496496+- Keep base images updated
497497+- Scan images for vulnerabilities regularly
498498+- Use read-only filesystems where possible
499499+500500+### 4. Redis Security
501501+502502+```bash
503503+# Add to Redis configuration for production
504504+requirepass your_strong_password_here
505505+maxclients 10000
506506+timeout 300
507507+```
508508+509509+### 5. Environment Variables
510510+511511+- Use Docker secrets or external secret management
512512+- Avoid logging sensitive environment variables
513513+- Implement proper access controls
514514+515515+## Troubleshooting
516516+517517+### Common Issues and Solutions
518518+519519+#### 1. Container Won't Start
520520+521521+```bash
522522+# Check logs
523523+docker logs quickdid
524524+525525+# Verify environment variables
526526+docker exec quickdid env | grep -E "HTTP_EXTERNAL|SERVICE_KEY"
527527+528528+# Test Redis connectivity
529529+docker exec quickdid redis-cli -h redis ping
530530+```
531531+532532+#### 2. Handle Resolution Failures
533533+534534+```bash
535535+# Enable debug logging
536536+docker exec quickdid sh -c "export RUST_LOG=debug"
537537+538538+# Check DNS resolution
539539+docker exec quickdid nslookup plc.directory
540540+541541+# Verify Redis cache
542542+docker exec -it quickdid-redis redis-cli
543543+> KEYS handle:*
544544+> TTL handle:example_key
545545+```
546546+547547+#### 3. Performance Issues
548548+549549+```bash
550550+# Monitor Redis memory usage
551551+docker exec quickdid-redis redis-cli INFO memory
552552+553553+# Check container resource usage
554554+docker stats quickdid
555555+556556+# Analyze slow queries (with debug logging)
557557+docker logs quickdid | grep "resolution took"
558558+```
559559+560560+#### 4. Health Check Failures
561561+562562+```bash
563563+# Manual health check
564564+docker exec quickdid curl -v http://localhost:8080/health
565565+566566+# Check service binding
567567+docker exec quickdid netstat -tlnp | grep 8080
568568+```
569569+570570+### Debugging Commands
571571+572572+```bash
573573+# Interactive shell in container
574574+docker exec -it quickdid /bin/bash
575575+576576+# Test handle resolution
577577+curl "http://localhost:8080/xrpc/com.atproto.identity.resolveHandle?handle=example.bsky.social"
578578+579579+# Check Redis keys
580580+docker exec quickdid-redis redis-cli --scan --pattern "handle:*" | head -20
581581+582582+# Monitor real-time logs
583583+docker-compose logs -f quickdid | grep -E "ERROR|WARN"
584584+```
585585+586586+## Maintenance
587587+588588+### Backup and Restore
589589+590590+```bash
591591+# Backup Redis data
592592+docker exec quickdid-redis redis-cli BGSAVE
593593+docker cp quickdid-redis:/data/dump.rdb ./backups/redis-$(date +%Y%m%d).rdb
594594+595595+# Restore Redis data
596596+docker cp ./backups/redis-backup.rdb quickdid-redis:/data/dump.rdb
597597+docker restart quickdid-redis
598598+```
599599+600600+### Updates and Rollbacks
601601+602602+```bash
603603+# Update to new version
604604+docker pull quickdid:new-version
605605+docker-compose down
606606+docker-compose up -d
607607+608608+# Rollback if needed
609609+docker-compose down
610610+docker tag quickdid:previous quickdid:latest
611611+docker-compose up -d
612612+```
613613+614614+### Log Rotation
615615+616616+Configure Docker's built-in log rotation in `/etc/docker/daemon.json`:
617617+618618+```json
619619+{
620620+ "log-driver": "json-file",
621621+ "log-opts": {
622622+ "max-size": "10m",
623623+ "max-file": "3"
624624+ }
625625+}
626626+```
627627+628628+## Performance Optimization
629629+630630+### Redis Optimization
631631+632632+```redis
633633+# Add to redis.conf or pass as command arguments
634634+maxmemory 2gb
635635+maxmemory-policy allkeys-lru
636636+save "" # Disable persistence for cache-only usage
637637+tcp-keepalive 300
638638+timeout 0
639639+```
640640+641641+### System Tuning
642642+643643+```bash
644644+# Add to host system's /etc/sysctl.conf
645645+net.core.somaxconn = 1024
646646+net.ipv4.tcp_tw_reuse = 1
647647+net.ipv4.ip_local_port_range = 10000 65000
648648+fs.file-max = 100000
649649+```
650650+651651+## Configuration Validation
652652+653653+QuickDID validates all configuration at startup. The following rules are enforced:
654654+655655+### Required Fields
656656+657657+- **HTTP_EXTERNAL**: Must be provided
658658+- **SERVICE_KEY**: Must be provided
659659+660660+### Value Constraints
661661+662662+1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`):
663663+ - Must be positive integers (> 0)
664664+ - Recommended minimum: 60 seconds
665665+666666+2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`):
667667+ - Must be positive integers (> 0)
668668+ - Recommended range: 1-60 seconds
669669+670670+3. **Queue Adapter** (`QUEUE_ADAPTER`):
671671+ - Must be one of: `mpsc`, `redis`, `noop`, `none`
672672+ - Case-sensitive
673673+674674+### Validation Errors
675675+676676+If validation fails, QuickDID will exit with one of these error codes:
677677+678678+- `error-quickdid-config-1`: Missing required environment variable
679679+- `error-quickdid-config-2`: Invalid configuration value
680680+- `error-quickdid-config-3`: Invalid TTL value (must be positive)
681681+- `error-quickdid-config-4`: Invalid timeout value (must be positive)
682682+683683+### Testing Configuration
684684+685685+```bash
686686+# Validate configuration without starting service
687687+HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help
688688+689689+# Test with specific values (will fail validation)
690690+CACHE_TTL_MEMORY=0 quickdid --help
691691+692692+# Debug configuration parsing
693693+RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid
694694+```
695695+696696+## Support and Resources
697697+698698+- **Documentation**: [QuickDID GitHub Repository](https://github.com/smokesignal.events/quickdid)
699699+- **Configuration Reference**: See [configuration-reference.md](./configuration-reference.md) for detailed documentation of all options
700700+- **AT Protocol Specs**: [atproto.com](https://atproto.com)
701701+- **Issues**: Report bugs via GitHub Issues
702702+- **Community**: Join the AT Protocol Discord server
703703+704704+## License
705705+706706+QuickDID is licensed under the MIT License. See LICENSE file for details.
+27-7
src/bin/quickdid.rs
···22use async_trait::async_trait;
33use atproto_identity::{
44 config::{CertificateBundles, DnsNameservers},
55- key::{identify_key, to_public, KeyData, KeyProvider},
55+ key::{KeyData, KeyProvider, identify_key, to_public},
66 resolve::HickoryDnsResolver,
77};
88use clap::Parser;
···5858 let args = Args::parse();
5959 let config = Config::from_args(args)?;
60606161+ // Validate configuration
6262+ config.validate()?;
6363+6164 tracing::info!("Starting QuickDID service on port {}", config.http_port);
6265 tracing::info!("Service DID: {}", config.service_did);
6666+ tracing::info!(
6767+ "Cache TTL - Memory: {}s, Redis: {}s",
6868+ config.cache_ttl_memory,
6969+ config.cache_ttl_redis
7070+ );
63716472 // Parse certificate bundles if provided
6573 let certificate_bundles: CertificateBundles = config
···135143 // Create handle resolver with Redis caching if available, otherwise use in-memory caching
136144 let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> =
137145 if let Some(pool) = redis_pool {
138138- tracing::info!("Using Redis-backed handle resolver with 90-day cache TTL");
139139- Arc::new(RedisHandleResolver::new(base_handle_resolver, pool))
146146+ tracing::info!(
147147+ "Using Redis-backed handle resolver with {}-second cache TTL",
148148+ config.cache_ttl_redis
149149+ );
150150+ Arc::new(RedisHandleResolver::with_ttl(
151151+ base_handle_resolver,
152152+ pool,
153153+ config.cache_ttl_redis,
154154+ ))
140155 } else {
141141- tracing::info!("Using in-memory handle resolver with 10-minute cache TTL");
156156+ tracing::info!(
157157+ "Using in-memory handle resolver with {}-second cache TTL",
158158+ config.cache_ttl_memory
159159+ );
142160 Arc::new(CachingHandleResolver::new(
143161 base_handle_resolver,
144144- 600, // 10 minutes TTL for in-memory cache
162162+ config.cache_ttl_memory,
145163 ))
146164 };
147165···174192 pool,
175193 config.queue_worker_id.clone(),
176194 config.queue_redis_prefix.clone(),
177177- 5, // 5 second timeout for blocking operations
195195+ config.queue_redis_timeout, // Configurable timeout for blocking operations
178196 ))
179197 }
180198 Err(e) => {
···192210 }
193211 },
194212 None => {
195195- tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter");
213213+ tracing::warn!(
214214+ "Redis queue adapter requested but no Redis URL configured, using no-op adapter"
215215+ );
196216 Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
197217 }
198218 }
+18-3
src/cache.rs
···11//! Redis cache utilities for QuickDID
2233-use anyhow::Result;
43use deadpool_redis::{Config, Pool, Runtime};
44+use thiserror::Error;
55+66+/// Cache-specific errors following the QuickDID error format
77+#[derive(Debug, Error)]
88+pub enum CacheError {
99+ #[error("error-quickdid-cache-1 Redis pool creation failed: {0}")]
1010+ PoolCreationFailed(String),
1111+1212+ #[error("error-quickdid-cache-2 Invalid Redis URL: {0}")]
1313+ InvalidRedisUrl(String),
1414+1515+ #[error("error-quickdid-cache-3 Redis connection failed: {0}")]
1616+ ConnectionFailed(String),
1717+}
518619/// Create a Redis connection pool from a Redis URL
77-pub fn create_redis_pool(redis_url: &str) -> Result<Pool> {
2020+pub fn create_redis_pool(redis_url: &str) -> Result<Pool, CacheError> {
821 let config = Config::from_url(redis_url);
99- let pool = config.create_pool(Some(Runtime::Tokio1))?;
2222+ let pool = config
2323+ .create_pool(Some(Runtime::Tokio1))
2424+ .map_err(|e| CacheError::PoolCreationFailed(e.to_string()))?;
1025 Ok(pool)
1126}
+328-11
src/config.rs
···11-use anyhow::Result;
11+//! Configuration management for QuickDID service
22+//!
33+//! This module handles all configuration parsing, validation, and error handling
44+//! for the QuickDID AT Protocol identity resolution service.
55+//!
66+//! ## Configuration Sources
77+//!
88+//! Configuration can be provided through:
99+//! - Environment variables (highest priority)
1010+//! - Command-line arguments
1111+//! - Default values (lowest priority)
1212+//!
1313+//! ## Example
1414+//!
1515+//! ```bash
1616+//! # Minimal configuration
1717+//! HTTP_EXTERNAL=quickdid.example.com \
1818+//! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
1919+//! quickdid
2020+//!
2121+//! # Full configuration with Redis and custom settings
2222+//! HTTP_EXTERNAL=quickdid.example.com \
2323+//! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
2424+//! HTTP_PORT=3000 \
2525+//! REDIS_URL=redis://localhost:6379 \
2626+//! CACHE_TTL_MEMORY=300 \
2727+//! CACHE_TTL_REDIS=86400 \
2828+//! QUEUE_ADAPTER=redis \
2929+//! QUEUE_REDIS_TIMEOUT=10 \
3030+//! quickdid
3131+//! ```
3232+233use atproto_identity::config::optional_env;
334use clap::Parser;
3535+use thiserror::Error;
3636+3737+/// Configuration-specific errors following the QuickDID error format
3838+///
3939+/// All errors follow the pattern: `error-quickdid-config-{number} {message}: {details}`
4040+#[derive(Debug, Error)]
4141+pub enum ConfigError {
4242+ /// Missing required environment variable or command-line argument
4343+ ///
4444+ /// Example: When SERVICE_KEY or HTTP_EXTERNAL are not provided
4545+ #[error("error-quickdid-config-1 Missing required environment variable: {0}")]
4646+ MissingRequired(String),
4747+4848+ /// Invalid configuration value that doesn't meet expected format or constraints
4949+ ///
5050+ /// Example: Invalid QUEUE_ADAPTER value (must be 'mpsc', 'redis', or 'noop')
5151+ #[error("error-quickdid-config-2 Invalid configuration value: {0}")]
5252+ InvalidValue(String),
5353+5454+ /// Invalid TTL (Time To Live) value
5555+ ///
5656+ /// TTL values must be positive integers representing seconds
5757+ #[error("error-quickdid-config-3 Invalid TTL value (must be positive): {0}")]
5858+ InvalidTtl(String),
5959+6060+ /// Invalid timeout value
6161+ ///
6262+ /// Timeout values must be positive integers representing seconds
6363+ #[error("error-quickdid-config-4 Invalid timeout value (must be positive): {0}")]
6464+ InvalidTimeout(String),
6565+}
466567#[derive(Parser, Clone)]
668#[command(
···2284 HTTP_EXTERNAL External hostname for service endpoints (required)
2385 HTTP_PORT HTTP server port (default: 8080)
2486 PLC_HOSTNAME PLC directory hostname (default: plc.directory)
2525- USER_AGENT HTTP User-Agent header (auto-generated)
2626- DNS_NAMESERVERS Custom DNS nameservers (optional)
2727- CERTIFICATE_BUNDLES Additional CA certificates (optional)
8787+ USER_AGENT HTTP User-Agent header (auto-generated with version)
8888+ DNS_NAMESERVERS Custom DNS nameservers (comma-separated IPs)
8989+ CERTIFICATE_BUNDLES Additional CA certificates (comma-separated paths)
9090+9191+ CACHING:
2892 REDIS_URL Redis URL for handle resolution caching (optional)
2929- QUEUE_ADAPTER Queue adapter type: 'mpsc' or 'redis' (default: mpsc)
9393+ CACHE_TTL_MEMORY TTL for in-memory cache in seconds (default: 600)
9494+ CACHE_TTL_REDIS TTL for Redis cache in seconds (default: 7776000 = 90 days)
9595+9696+ QUEUE CONFIGURATION:
9797+ QUEUE_ADAPTER Queue adapter: 'mpsc', 'redis', 'noop' (default: mpsc)
3098 QUEUE_REDIS_URL Redis URL for queue adapter (uses REDIS_URL if not set)
3199 QUEUE_REDIS_PREFIX Redis key prefix for queues (default: queue:handleresolver:)
3232- QUEUE_WORKER_ID Worker ID for Redis queue (random UUID if not set)
100100+ QUEUE_REDIS_TIMEOUT Queue blocking timeout in seconds (default: 5)
101101+ QUEUE_WORKER_ID Worker ID for Redis queue (auto-generated UUID if not set)
33102 QUEUE_BUFFER_SIZE Buffer size for MPSC queue (default: 1000)
34103"
35104)]
105105+/// Command-line arguments and environment variables configuration
36106pub struct Args {
107107+ /// HTTP server port to bind to
108108+ ///
109109+ /// Examples: "8080", "3000", "80"
110110+ /// Constraints: Must be a valid port number (1-65535)
37111 #[arg(long, env = "HTTP_PORT", default_value = "8080")]
38112 pub http_port: String,
39113114114+ /// PLC directory hostname for DID resolution
115115+ ///
116116+ /// Examples: "plc.directory", "test.plc.directory"
117117+ /// Use "plc.directory" for production
40118 #[arg(long, env = "PLC_HOSTNAME", default_value = "plc.directory")]
41119 pub plc_hostname: String,
42120121121+ /// External hostname for service endpoints (REQUIRED)
122122+ ///
123123+ /// Examples:
124124+ /// - "quickdid.example.com" (standard)
125125+ /// - "quickdid.example.com:8080" (with port)
126126+ /// - "localhost:3007" (development)
43127 #[arg(long, env = "HTTP_EXTERNAL")]
44128 pub http_external: Option<String>,
45129130130+ /// Private key for service identity in DID format (REQUIRED)
131131+ ///
132132+ /// Examples:
133133+ /// - "did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK"
134134+ /// - "did:plc:xyz123abc456"
135135+ ///
136136+ /// SECURITY: Keep this key secure and never commit to version control
46137 #[arg(long, env = "SERVICE_KEY")]
47138 pub service_key: Option<String>,
48139140140+ /// HTTP User-Agent header for outgoing requests
141141+ ///
142142+ /// Example: `quickdid/1.0.0 (+https://quickdid.example.com)`
143143+ /// Default: Auto-generated with current version
49144 #[arg(long, env = "USER_AGENT")]
50145 pub user_agent: Option<String>,
51146147147+ /// Custom DNS nameservers (comma-separated IP addresses)
148148+ ///
149149+ /// Examples:
150150+ /// - "8.8.8.8,8.8.4.4" (Google DNS)
151151+ /// - "1.1.1.1,1.0.0.1" (Cloudflare DNS)
152152+ /// - "192.168.1.1" (Local DNS)
52153 #[arg(long, env = "DNS_NAMESERVERS")]
53154 pub dns_nameservers: Option<String>,
54155156156+ /// Additional CA certificates (comma-separated file paths)
157157+ ///
158158+ /// Examples:
159159+ /// - "/etc/ssl/certs/custom-ca.pem"
160160+ /// - "/certs/ca1.pem,/certs/ca2.pem"
161161+ ///
162162+ /// Use for custom or internal certificate authorities
55163 #[arg(long, env = "CERTIFICATE_BUNDLES")]
56164 pub certificate_bundles: Option<String>,
57165166166+ /// Redis connection URL for caching
167167+ ///
168168+ /// Examples:
169169+ /// - "redis://localhost:6379/0" (local, no auth)
170170+ /// - "redis://user:pass@redis.example.com:6379/0" (with auth)
171171+ /// - "rediss://secure-redis.example.com:6380/0" (TLS)
172172+ ///
173173+ /// Benefits: Persistent cache, distributed caching, better performance
58174 #[arg(long, env = "REDIS_URL")]
59175 pub redis_url: Option<String>,
60176177177+ /// Queue adapter type for background processing
178178+ ///
179179+ /// Values:
180180+ /// - "mpsc": In-memory multi-producer single-consumer queue
181181+ /// - "redis": Redis-backed distributed queue
182182+ /// - "noop": Disable queue processing (for testing)
183183+ ///
184184+ /// Default: "mpsc" for single-instance deployments
61185 #[arg(long, env = "QUEUE_ADAPTER", default_value = "mpsc")]
62186 pub queue_adapter: String,
63187188188+ /// Redis URL specifically for queue operations
189189+ ///
190190+ /// Falls back to REDIS_URL if not specified
191191+ /// Use when separating cache and queue Redis instances
64192 #[arg(long, env = "QUEUE_REDIS_URL")]
65193 pub queue_redis_url: Option<String>,
66194195195+ /// Redis key prefix for queue operations
196196+ ///
197197+ /// Examples:
198198+ /// - "queue:handleresolver:" (default)
199199+ /// - "prod:queue:hr:" (environment-specific)
200200+ /// - "quickdid:v1:queue:" (version-specific)
201201+ ///
202202+ /// Use to namespace queues when sharing Redis
67203 #[arg(
68204 long,
69205 env = "QUEUE_REDIS_PREFIX",
···71207 )]
72208 pub queue_redis_prefix: String,
73209210210+ /// Worker ID for Redis queue operations
211211+ ///
212212+ /// Examples: "worker-001", "prod-us-east-1", "quickdid-1"
213213+ /// Default: Auto-generated UUID
214214+ ///
215215+ /// Use for identifying specific workers in logs
74216 #[arg(long, env = "QUEUE_WORKER_ID")]
75217 pub queue_worker_id: Option<String>,
76218219219+ /// Buffer size for MPSC queue
220220+ ///
221221+ /// Range: 100-100000 (recommended)
222222+ /// Default: 1000
223223+ ///
224224+ /// Increase for high-traffic deployments
77225 #[arg(long, env = "QUEUE_BUFFER_SIZE", default_value = "1000")]
78226 pub queue_buffer_size: usize,
227227+228228+ /// TTL for in-memory cache in seconds
229229+ ///
230230+ /// Range: 60-3600 (recommended)
231231+ /// Default: 600 (10 minutes)
232232+ ///
233233+ /// Lower values = fresher data, more resolution requests
234234+ /// Higher values = better performance, potentially stale data
235235+ #[arg(long, env = "CACHE_TTL_MEMORY", default_value = "600")]
236236+ pub cache_ttl_memory: u64,
237237+238238+ /// TTL for Redis cache in seconds
239239+ ///
240240+ /// Range: 3600-31536000 (1 hour to 1 year)
241241+ /// Default: 7776000 (90 days)
242242+ ///
243243+ /// Recommendation: 86400 (1 day) for frequently changing data
244244+ #[arg(long, env = "CACHE_TTL_REDIS", default_value = "7776000")]
245245+ pub cache_ttl_redis: u64,
246246+247247+ /// Redis blocking timeout for queue operations in seconds
248248+ ///
249249+ /// Range: 1-60 (recommended)
250250+ /// Default: 5
251251+ ///
252252+ /// Lower values = more responsive to shutdown
253253+ /// Higher values = less Redis polling overhead
254254+ #[arg(long, env = "QUEUE_REDIS_TIMEOUT", default_value = "5")]
255255+ pub queue_redis_timeout: u64,
79256}
80257258258+/// Validated configuration for QuickDID service
259259+///
260260+/// This struct contains all configuration after validation and processing.
261261+/// Use `Config::from_args()` to create from command-line arguments and environment variables.
262262+///
263263+/// ## Example
264264+///
265265+/// ```rust,no_run
266266+/// use quickdid::config::{Args, Config};
267267+/// use clap::Parser;
268268+///
269269+/// let args = Args::parse();
270270+/// let config = Config::from_args(args)?;
271271+/// config.validate()?;
272272+///
273273+/// println!("Service running at: {}", config.http_external);
274274+/// println!("Service DID: {}", config.service_did);
275275+/// ```
81276#[derive(Clone)]
82277pub struct Config {
278278+ /// HTTP server port (e.g., "8080", "3000")
83279 pub http_port: String,
280280+281281+ /// PLC directory hostname (e.g., "plc.directory")
84282 pub plc_hostname: String,
283283+284284+ /// External hostname for service endpoints (e.g., "quickdid.example.com")
85285 pub http_external: String,
286286+287287+ /// Private key for service identity (e.g., "did:key:z42tm...")
86288 pub service_key: String,
289289+290290+ /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)")
87291 pub user_agent: String,
292292+293293+ /// Derived service DID (e.g., "did:web:quickdid.example.com")
294294+ /// Automatically generated from http_external with proper encoding
88295 pub service_did: String,
296296+297297+ /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4")
89298 pub dns_nameservers: Option<String>,
299299+300300+ /// Additional CA certificate bundles, comma-separated paths
90301 pub certificate_bundles: Option<String>,
302302+303303+ /// Redis URL for caching (e.g., "redis://localhost:6379/0")
91304 pub redis_url: Option<String>,
305305+306306+ /// Queue adapter type: "mpsc", "redis", or "noop"
92307 pub queue_adapter: String,
308308+309309+ /// Redis URL for queue operations (falls back to redis_url)
93310 pub queue_redis_url: Option<String>,
311311+312312+ /// Redis key prefix for queues (e.g., "queue:handleresolver:")
94313 pub queue_redis_prefix: String,
314314+315315+ /// Worker ID for queue operations (auto-generated if not set)
95316 pub queue_worker_id: Option<String>,
317317+318318+ /// Buffer size for MPSC queue (e.g., 1000)
96319 pub queue_buffer_size: usize,
320320+321321+ /// TTL for in-memory cache in seconds (e.g., 600 = 10 minutes)
322322+ pub cache_ttl_memory: u64,
323323+324324+ /// TTL for Redis cache in seconds (e.g., 7776000 = 90 days)
325325+ pub cache_ttl_redis: u64,
326326+327327+ /// Redis blocking timeout for queue operations in seconds (e.g., 5)
328328+ pub queue_redis_timeout: u64,
97329}
9833099331impl Config {
100100- pub fn from_args(args: Args) -> Result<Self> {
332332+ /// Create a validated Config from command-line arguments and environment variables
333333+ ///
334334+ /// This method:
335335+ /// 1. Processes command-line arguments with environment variable fallbacks
336336+ /// 2. Validates required fields (HTTP_EXTERNAL and SERVICE_KEY)
337337+ /// 3. Generates derived values (service_did from http_external)
338338+ /// 4. Applies defaults where appropriate
339339+ ///
340340+ /// ## Priority Order
341341+ ///
342342+ /// 1. Command-line arguments (highest priority)
343343+ /// 2. Environment variables
344344+ /// 3. Default values (lowest priority)
345345+ ///
346346+ /// ## Example
347347+ ///
348348+ /// ```rust,no_run
349349+ /// use quickdid::config::{Args, Config};
350350+ /// use clap::Parser;
351351+ ///
352352+ /// // Parse from environment and command-line
353353+ /// let args = Args::parse();
354354+ /// let config = Config::from_args(args)?;
355355+ ///
356356+ /// // The service DID is automatically generated from HTTP_EXTERNAL
357357+ /// assert!(config.service_did.starts_with("did:web:"));
358358+ /// ```
359359+ ///
360360+ /// ## Errors
361361+ ///
362362+ /// Returns `ConfigError::MissingRequired` if:
363363+ /// - HTTP_EXTERNAL is not provided
364364+ /// - SERVICE_KEY is not provided
365365+ pub fn from_args(args: Args) -> Result<Self, ConfigError> {
101366 let http_external = args
102367 .http_external
103368 .or_else(|| {
···108373 Some(env_val)
109374 }
110375 })
111111- .ok_or_else(|| anyhow::anyhow!("HTTP_EXTERNAL is required"))?;
376376+ .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?;
112377113378 let service_key = args
114379 .service_key
···120385 Some(env_val)
121386 }
122387 })
123123- .ok_or_else(|| anyhow::anyhow!("SERVICE_KEY is required"))?;
388388+ .ok_or_else(|| ConfigError::MissingRequired("SERVICE_KEY".to_string()))?;
124389125125- let default_user_agent =
126126- format!("quickdid/0.1.0 (+https://github.com/smokesignal.events/quickdid)");
390390+ let default_user_agent = format!(
391391+ "quickdid/{} (+https://github.com/smokesignal.events/quickdid)",
392392+ env!("CARGO_PKG_VERSION")
393393+ );
127394128395 let user_agent = args
129396 .user_agent
···194461 }
195462 }),
196463 queue_buffer_size: args.queue_buffer_size,
464464+ cache_ttl_memory: args.cache_ttl_memory,
465465+ cache_ttl_redis: args.cache_ttl_redis,
466466+ queue_redis_timeout: args.queue_redis_timeout,
197467 })
468468+ }
469469+470470+ /// Validate the configuration for correctness and consistency
471471+ ///
472472+ /// Checks:
473473+ /// - Cache TTL values are positive (> 0)
474474+ /// - Queue timeout is positive (> 0)
475475+ /// - Queue adapter is a valid value ('mpsc', 'redis', 'noop', 'none')
476476+ ///
477477+ /// ## Example
478478+ ///
479479+ /// ```rust,no_run
480480+ /// let config = Config::from_args(args)?;
481481+ /// config.validate()?; // Ensures all values are valid
482482+ /// ```
483483+ ///
484484+ /// ## Errors
485485+ ///
486486+ /// Returns `ConfigError::InvalidTtl` if TTL values are 0 or negative
487487+ /// Returns `ConfigError::InvalidTimeout` if timeout values are 0 or negative
488488+ /// Returns `ConfigError::InvalidValue` if queue adapter is invalid
489489+ pub fn validate(&self) -> Result<(), ConfigError> {
490490+ if self.cache_ttl_memory == 0 {
491491+ return Err(ConfigError::InvalidTtl(
492492+ "CACHE_TTL_MEMORY must be > 0".to_string(),
493493+ ));
494494+ }
495495+ if self.cache_ttl_redis == 0 {
496496+ return Err(ConfigError::InvalidTtl(
497497+ "CACHE_TTL_REDIS must be > 0".to_string(),
498498+ ));
499499+ }
500500+ if self.queue_redis_timeout == 0 {
501501+ return Err(ConfigError::InvalidTimeout(
502502+ "QUEUE_REDIS_TIMEOUT must be > 0".to_string(),
503503+ ));
504504+ }
505505+ match self.queue_adapter.as_str() {
506506+ "mpsc" | "redis" | "noop" | "none" => {}
507507+ _ => {
508508+ return Err(ConfigError::InvalidValue(format!(
509509+ "Invalid QUEUE_ADAPTER '{}', must be 'mpsc', 'redis', or 'noop'",
510510+ self.queue_adapter
511511+ )));
512512+ }
513513+ }
514514+ Ok(())
198515 }
199516}
+61-13
src/handle_resolution_result.rs
···6677use serde::{Deserialize, Serialize};
88use std::time::{SystemTime, UNIX_EPOCH};
99+use thiserror::Error;
1010+1111+/// Errors that can occur during handle resolution result operations
1212+#[derive(Debug, Error)]
1313+pub enum HandleResolutionError {
1414+ #[error("error-quickdid-resolution-1 System time error: {0}")]
1515+ SystemTimeError(String),
1616+1717+ #[error("error-quickdid-serialization-1 Failed to serialize resolution result: {0}")]
1818+ SerializationError(String),
1919+2020+ #[error("error-quickdid-serialization-2 Failed to deserialize resolution result: {0}")]
2121+ DeserializationError(String),
2222+}
9231024/// Represents the type of DID method from a resolution
1125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
···34483549impl HandleResolutionResult {
3650 /// Create a new resolution result for a successfully resolved handle
3737- pub fn success(did: &str) -> Self {
5151+ pub fn success(did: &str) -> Result<Self, HandleResolutionError> {
5252+ let timestamp = SystemTime::now()
5353+ .duration_since(UNIX_EPOCH)
5454+ .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))?
5555+ .as_secs();
5656+5757+ let (method_type, payload) = Self::parse_did(did);
5858+5959+ Ok(Self {
6060+ timestamp,
6161+ method_type,
6262+ payload,
6363+ })
6464+ }
6565+6666+ /// Create a new resolution result for a successfully resolved handle (unsafe version for compatibility)
6767+ /// This version panics if system time is invalid and should only be used in tests
6868+ pub fn success_unchecked(did: &str) -> Self {
3869 let timestamp = SystemTime::now()
3970 .duration_since(UNIX_EPOCH)
4071 .expect("Time went backwards")
···5081 }
51825283 /// Create a new resolution result for a failed resolution
5353- pub fn not_resolved() -> Self {
8484+ pub fn not_resolved() -> Result<Self, HandleResolutionError> {
8585+ let timestamp = SystemTime::now()
8686+ .duration_since(UNIX_EPOCH)
8787+ .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))?
8888+ .as_secs();
8989+9090+ Ok(Self {
9191+ timestamp,
9292+ method_type: DidMethodType::NotResolved,
9393+ payload: String::new(),
9494+ })
9595+ }
9696+9797+ /// Create a new resolution result for a failed resolution (unsafe version for compatibility)
9898+ /// This version panics if system time is invalid and should only be used in tests
9999+ pub fn not_resolved_unchecked() -> Self {
54100 let timestamp = SystemTime::now()
55101 .duration_since(UNIX_EPOCH)
56102 .expect("Time went backwards")
···108154 }
109155110156 /// Serialize the result to bytes using bincode
111111- pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
157157+ pub fn to_bytes(&self) -> Result<Vec<u8>, HandleResolutionError> {
112158 bincode::serde::encode_to_vec(self, bincode::config::standard())
159159+ .map_err(|e| HandleResolutionError::SerializationError(e.to_string()))
113160 }
114161115162 /// Deserialize from bytes using bincode
116116- pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
163163+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandleResolutionError> {
117164 bincode::serde::decode_from_slice(bytes, bincode::config::standard())
118165 .map(|(result, _)| result)
166166+ .map_err(|e| HandleResolutionError::DeserializationError(e.to_string()))
119167 }
120168}
121169···126174 #[test]
127175 fn test_parse_did_web() {
128176 let did = "did:web:example.com";
129129- let result = HandleResolutionResult::success(did);
177177+ let result = HandleResolutionResult::success_unchecked(did);
130178 assert_eq!(result.method_type, DidMethodType::Web);
131179 assert_eq!(result.payload, "example.com");
132180 assert_eq!(result.to_did(), Some(did.to_string()));
···135183 #[test]
136184 fn test_parse_did_plc() {
137185 let did = "did:plc:abcdef123456";
138138- let result = HandleResolutionResult::success(did);
186186+ let result = HandleResolutionResult::success_unchecked(did);
139187 assert_eq!(result.method_type, DidMethodType::Plc);
140188 assert_eq!(result.payload, "abcdef123456");
141189 assert_eq!(result.to_did(), Some(did.to_string()));
···144192 #[test]
145193 fn test_parse_did_other() {
146194 let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
147147- let result = HandleResolutionResult::success(did);
195195+ let result = HandleResolutionResult::success_unchecked(did);
148196 assert_eq!(result.method_type, DidMethodType::Other);
149197 assert_eq!(result.payload, did);
150198 assert_eq!(result.to_did(), Some(did.to_string()));
···152200153201 #[test]
154202 fn test_not_resolved() {
155155- let result = HandleResolutionResult::not_resolved();
203203+ let result = HandleResolutionResult::not_resolved_unchecked();
156204 assert_eq!(result.method_type, DidMethodType::NotResolved);
157205 assert_eq!(result.payload, "");
158206 assert_eq!(result.to_did(), None);
···209257 );
210258211259 // Verify the size for not resolved (should be minimal)
212212- let not_resolved = HandleResolutionResult::not_resolved();
260260+ let not_resolved = HandleResolutionResult::not_resolved_unchecked();
213261 let bytes = not_resolved.to_bytes().unwrap();
214262 assert!(
215263 bytes.len() < 50,
···221269 #[test]
222270 fn test_did_web_with_port() {
223271 let did = "did:web:localhost:3000";
224224- let result = HandleResolutionResult::success(did);
272272+ let result = HandleResolutionResult::success_unchecked(did);
225273 assert_eq!(result.method_type, DidMethodType::Web);
226274 assert_eq!(result.payload, "localhost:3000");
227275 assert_eq!(result.to_did(), Some(did.to_string()));
···230278 #[test]
231279 fn test_did_web_with_path() {
232280 let did = "did:web:example.com:path:to:did";
233233- let result = HandleResolutionResult::success(did);
281281+ let result = HandleResolutionResult::success_unchecked(did);
234282 assert_eq!(result.method_type, DidMethodType::Web);
235283 assert_eq!(result.payload, "example.com:path:to:did");
236284 assert_eq!(result.to_did(), Some(did.to_string()));
···239287 #[test]
240288 fn test_invalid_did_format() {
241289 let did = "not-a-did";
242242- let result = HandleResolutionResult::success(did);
290290+ let result = HandleResolutionResult::success_unchecked(did);
243291 assert_eq!(result.method_type, DidMethodType::Other);
244292 assert_eq!(result.payload, did);
245293 assert_eq!(result.to_did(), Some(did.to_string()));
···258306 ];
259307260308 for (did, expected_type, expected_payload) in test_cases {
261261- let result = HandleResolutionResult::success(did);
309309+ let result = HandleResolutionResult::success_unchecked(did);
262310 assert_eq!(result.method_type, expected_type);
263311 assert_eq!(result.payload, expected_payload);
264312
+56-18
src/handle_resolver.rs
···11use crate::handle_resolution_result::HandleResolutionResult;
22use async_trait::async_trait;
33-use atproto_identity::resolve::{resolve_subject, DnsResolver};
33+use atproto_identity::resolve::{DnsResolver, resolve_subject};
44use chrono::Utc;
55-use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool};
55+use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
66use metrohash::MetroHash64;
77use reqwest::Client;
88use std::collections::HashMap;
···1616pub enum HandleResolverError {
1717 #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
1818 ResolutionFailed(String),
1919-1919+2020 #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
2121 HandleNotFoundCached(String),
2222-2222+2323 #[error("error-quickdid-resolve-3 Handle not found (cached)")]
2424 HandleNotFound,
2525-2525+2626 #[error("error-quickdid-resolve-4 Mock resolution failure")]
2727 MockResolutionFailure,
2828}
···148148}
149149150150/// Redis-backed caching handle resolver that caches resolution results in Redis
151151-/// with a 90-day expiration time.
151151+/// with a configurable expiration time.
152152pub struct RedisHandleResolver {
153153 /// Base handle resolver to perform actual resolution
154154 inner: Arc<dyn HandleResolver>,
···156156 pool: RedisPool,
157157 /// Redis key prefix for handle resolution cache
158158 key_prefix: String,
159159+ /// TTL for cache entries in seconds
160160+ ttl_seconds: u64,
159161}
160162161163impl RedisHandleResolver {
162162- /// Create a new Redis-backed handle resolver
164164+ /// Create a new Redis-backed handle resolver with default 90-day TTL
163165 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self {
164164- Self::with_prefix(inner, pool, "handle:".to_string())
166166+ Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default
167167+ }
168168+169169+ /// Create a new Redis-backed handle resolver with custom TTL
170170+ pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self {
171171+ Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds)
165172 }
166173167174 /// Create a new Redis-backed handle resolver with a custom key prefix
···170177 pool: RedisPool,
171178 key_prefix: String,
172179 ) -> Self {
180180+ Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60)
181181+ }
182182+183183+ /// Create a new Redis-backed handle resolver with full configuration
184184+ pub fn with_full_config(
185185+ inner: Arc<dyn HandleResolver>,
186186+ pool: RedisPool,
187187+ key_prefix: String,
188188+ ttl_seconds: u64,
189189+ ) -> Self {
173190 Self {
174191 inner,
175192 pool,
176193 key_prefix,
194194+ ttl_seconds,
177195 }
178196 }
179197···184202 format!("{}{}", self.key_prefix, h.finish())
185203 }
186204187187- /// Get the TTL in seconds (90 days)
188188- fn ttl_seconds() -> u64 {
189189- 90 * 24 * 60 * 60 // 90 days in seconds
205205+ /// Get the TTL in seconds
206206+ fn ttl_seconds(&self) -> u64 {
207207+ self.ttl_seconds
190208 }
191209}
192210···243261 handle,
244262 did
245263 );
246246- HandleResolutionResult::success(did)
264264+ match HandleResolutionResult::success(did) {
265265+ Ok(res) => res,
266266+ Err(e) => {
267267+ tracing::warn!("Failed to create resolution result: {}", e);
268268+ return result;
269269+ }
270270+ }
247271 }
248272 Err(e) => {
249273 tracing::debug!("Caching failed resolution for handle {}: {}", handle, e);
250250- HandleResolutionResult::not_resolved()
274274+ match HandleResolutionResult::not_resolved() {
275275+ Ok(res) => res,
276276+ Err(err) => {
277277+ tracing::warn!("Failed to create not_resolved result: {}", err);
278278+ return result;
279279+ }
280280+ }
251281 }
252282 };
253283···256286 Ok(bytes) => {
257287 // Set with expiration (ignore errors to not fail the resolution)
258288 if let Err(e) = conn
259259- .set_ex::<_, _, ()>(&key, bytes, Self::ttl_seconds())
289289+ .set_ex::<_, _, ()>(&key, bytes, self.ttl_seconds())
260290 .await
261291 {
262292 tracing::warn!("Failed to cache handle resolution in Redis: {}", e);
···336366337367 // Create Redis-backed resolver with a unique key prefix for testing
338368 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
339339- let redis_resolver =
340340- RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone());
369369+ let redis_resolver = RedisHandleResolver::with_full_config(
370370+ mock_resolver,
371371+ pool.clone(),
372372+ test_prefix.clone(),
373373+ 3600,
374374+ );
341375342376 let test_handle = "alice.bsky.social";
343377···385419386420 // Create Redis-backed resolver with a unique key prefix for testing
387421 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
388388- let redis_resolver =
389389- RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone());
422422+ let redis_resolver = RedisHandleResolver::with_full_config(
423423+ mock_resolver,
424424+ pool.clone(),
425425+ test_prefix.clone(),
426426+ 3600,
427427+ );
390428391429 let test_handle = "error.bsky.social";
392430
···44//! that can be used with any work type for handle resolution and other tasks.
5566use async_trait::async_trait;
77-use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool};
77+use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
88use serde::{Deserialize, Serialize};
99use std::sync::Arc;
1010use thiserror::Error;
1111-use tokio::sync::{mpsc, Mutex};
1111+use tokio::sync::{Mutex, mpsc};
1212use tracing::{debug, error, warn};
13131414/// Queue operation errors