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.

documentation: configuration improvements

+1864 -57
+118
.env.example
··· 1 + # QuickDID Environment Configuration Template 2 + # Copy this file to .env and customize for your deployment 3 + # 4 + # IMPORTANT: Never commit .env files with real SERVICE_KEY values 5 + 6 + # ============================================================================ 7 + # REQUIRED CONFIGURATION 8 + # ============================================================================ 9 + 10 + # External hostname for service endpoints (REQUIRED) 11 + # Examples: 12 + # - quickdid.example.com 13 + # - quickdid.example.com:8080 14 + # - localhost:3007 15 + HTTP_EXTERNAL=quickdid.example.com 16 + 17 + # Private key for service identity (REQUIRED) 18 + # SECURITY: Generate a new key for each environment 19 + # NEVER commit real keys to version control 20 + SERVICE_KEY=did:key:YOUR_PRIVATE_KEY_HERE 21 + 22 + # ============================================================================ 23 + # NETWORK CONFIGURATION 24 + # ============================================================================ 25 + 26 + # HTTP server port (default: 8080) 27 + HTTP_PORT=8080 28 + 29 + # PLC directory hostname (default: plc.directory) 30 + # Use "plc.directory" for production 31 + PLC_HOSTNAME=plc.directory 32 + 33 + # HTTP User-Agent header (optional) 34 + # Default: quickdid/{version} (+https://github.com/smokesignal.events/quickdid) 35 + # USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com) 36 + 37 + # Custom DNS nameservers (optional, comma-separated) 38 + # Examples: 8.8.8.8,8.8.4.4 or 1.1.1.1,1.0.0.1 39 + # DNS_NAMESERVERS= 40 + 41 + # Additional CA certificates (optional, comma-separated paths) 42 + # CERTIFICATE_BUNDLES= 43 + 44 + # ============================================================================ 45 + # CACHING CONFIGURATION 46 + # ============================================================================ 47 + 48 + # Redis URL for caching (optional but recommended for production) 49 + # Examples: 50 + # - redis://localhost:6379/0 51 + # - redis://user:pass@redis.example.com:6379/0 52 + # - rediss://secure-redis.example.com:6380/0 53 + # REDIS_URL=redis://localhost:6379/0 54 + 55 + # TTL for in-memory cache in seconds (default: 600 = 10 minutes) 56 + # Lower = fresher data, higher = better performance 57 + # Range: 60-3600 recommended 58 + CACHE_TTL_MEMORY=600 59 + 60 + # TTL for Redis cache in seconds (default: 7776000 = 90 days) 61 + # Recommendations: 62 + # - 86400 (1 day) for frequently changing data 63 + # - 604800 (1 week) for balanced performance 64 + # - 7776000 (90 days) for stable data 65 + CACHE_TTL_REDIS=86400 66 + 67 + # ============================================================================ 68 + # QUEUE CONFIGURATION 69 + # ============================================================================ 70 + 71 + # Queue adapter type (default: mpsc) 72 + # Options: 73 + # - mpsc: In-memory queue (single instance) 74 + # - redis: Distributed queue (multi-instance) 75 + # - noop: Disable queue (testing only) 76 + QUEUE_ADAPTER=mpsc 77 + 78 + # Redis URL for queue operations (optional) 79 + # Falls back to REDIS_URL if not specified 80 + # Use when separating cache and queue Redis instances 81 + # QUEUE_REDIS_URL= 82 + 83 + # Redis key prefix for queues (default: queue:handleresolver:) 84 + # Useful for namespacing when sharing Redis 85 + QUEUE_REDIS_PREFIX=queue:handleresolver: 86 + 87 + # Redis blocking timeout in seconds (default: 5) 88 + # Lower = more responsive, higher = less polling 89 + QUEUE_REDIS_TIMEOUT=5 90 + 91 + # Worker ID for queue operations (optional) 92 + # Default: auto-generated UUID 93 + # Examples: worker-001, prod-us-east-1, $(hostname) 94 + # QUEUE_WORKER_ID= 95 + 96 + # Buffer size for MPSC queue (default: 1000) 97 + # Increase for high-traffic deployments 98 + QUEUE_BUFFER_SIZE=1000 99 + 100 + # ============================================================================ 101 + # LOGGING 102 + # ============================================================================ 103 + 104 + # Rust log level 105 + # Options: trace, debug, info, warn, error 106 + # Production: info or warn 107 + # Development: debug 108 + RUST_LOG=info 109 + 110 + # ============================================================================ 111 + # DEVELOPMENT OVERRIDES (uncomment for local development) 112 + # ============================================================================ 113 + 114 + # HTTP_EXTERNAL=localhost:3007 115 + # SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 116 + # RUST_LOG=debug 117 + # CACHE_TTL_MEMORY=60 118 + # CACHE_TTL_REDIS=300
+542
docs/configuration-reference.md
··· 1 + # QuickDID Configuration Reference 2 + 3 + This document provides a comprehensive reference for all configuration options available in QuickDID. 4 + 5 + ## Table of Contents 6 + 7 + - [Required Configuration](#required-configuration) 8 + - [Network Configuration](#network-configuration) 9 + - [Caching Configuration](#caching-configuration) 10 + - [Queue Configuration](#queue-configuration) 11 + - [Security Configuration](#security-configuration) 12 + - [Advanced Configuration](#advanced-configuration) 13 + - [Configuration Examples](#configuration-examples) 14 + - [Validation Rules](#validation-rules) 15 + 16 + ## Required Configuration 17 + 18 + These environment variables MUST be set for QuickDID to start. 19 + 20 + ### `HTTP_EXTERNAL` 21 + 22 + **Required**: Yes 23 + **Type**: String 24 + **Format**: Hostname with optional port 25 + 26 + The external hostname where this service will be accessible. This is used to generate the service DID and for AT Protocol identity resolution. 27 + 28 + **Examples**: 29 + ```bash 30 + # Production domain 31 + HTTP_EXTERNAL=quickdid.example.com 32 + 33 + # With non-standard port 34 + HTTP_EXTERNAL=quickdid.example.com:8080 35 + 36 + # Development/testing 37 + HTTP_EXTERNAL=localhost:3007 38 + ``` 39 + 40 + **Constraints**: 41 + - Must be a valid hostname or hostname:port combination 42 + - Port (if specified) must be between 1-65535 43 + - Used to generate service DID (did:web:{HTTP_EXTERNAL}) 44 + 45 + ### `SERVICE_KEY` 46 + 47 + **Required**: Yes 48 + **Type**: String 49 + **Format**: DID private key 50 + **Security**: SENSITIVE - Never commit to version control 51 + 52 + The private key for the service's AT Protocol identity. This key is used to sign responses and authenticate the service. 53 + 54 + **Examples**: 55 + ```bash 56 + # did:key format (Ed25519) 57 + SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 58 + 59 + # did:plc format 60 + SERVICE_KEY=did:plc:xyz123abc456def789 61 + ``` 62 + 63 + **Constraints**: 64 + - Must be a valid DID format 65 + - Must include the private key component 66 + - Should be stored securely (e.g., secrets manager, encrypted storage) 67 + 68 + ## Network Configuration 69 + 70 + ### `HTTP_PORT` 71 + 72 + **Required**: No 73 + **Type**: String 74 + **Default**: `8080` 75 + **Range**: 1-65535 76 + 77 + The port number for the HTTP server to bind to. 78 + 79 + **Examples**: 80 + ```bash 81 + HTTP_PORT=8080 # Default 82 + HTTP_PORT=3000 # Common alternative 83 + HTTP_PORT=80 # Standard HTTP (requires root/privileges) 84 + ``` 85 + 86 + ### `PLC_HOSTNAME` 87 + 88 + **Required**: No 89 + **Type**: String 90 + **Default**: `plc.directory` 91 + 92 + The hostname of the PLC directory service for DID resolution. 93 + 94 + **Examples**: 95 + ```bash 96 + PLC_HOSTNAME=plc.directory # Production (default) 97 + PLC_HOSTNAME=test.plc.directory # Testing environment 98 + PLC_HOSTNAME=localhost:2582 # Local PLC server 99 + ``` 100 + 101 + ### `DNS_NAMESERVERS` 102 + 103 + **Required**: No 104 + **Type**: String (comma-separated IP addresses) 105 + **Default**: System DNS 106 + 107 + Custom DNS nameservers for handle resolution via TXT records. 108 + 109 + **Examples**: 110 + ```bash 111 + # Google DNS 112 + DNS_NAMESERVERS=8.8.8.8,8.8.4.4 113 + 114 + # Cloudflare DNS 115 + DNS_NAMESERVERS=1.1.1.1,1.0.0.1 116 + 117 + # Multiple providers 118 + DNS_NAMESERVERS=8.8.8.8,1.1.1.1 119 + 120 + # Local DNS 121 + DNS_NAMESERVERS=192.168.1.1 122 + ``` 123 + 124 + ### `USER_AGENT` 125 + 126 + **Required**: No 127 + **Type**: String 128 + **Default**: `quickdid/{version} (+https://github.com/smokesignal.events/quickdid)` 129 + 130 + HTTP User-Agent header for outgoing requests. 131 + 132 + **Examples**: 133 + ```bash 134 + # Custom agent 135 + USER_AGENT="MyService/1.0.0 (+https://myservice.com)" 136 + 137 + # With contact info 138 + USER_AGENT="quickdid/1.0.0 (+https://quickdid.example.com; admin@example.com)" 139 + ``` 140 + 141 + ### `CERTIFICATE_BUNDLES` 142 + 143 + **Required**: No 144 + **Type**: String (comma-separated file paths) 145 + **Default**: System CA certificates 146 + 147 + Additional CA certificate bundles for TLS connections. 148 + 149 + **Examples**: 150 + ```bash 151 + # Single certificate 152 + CERTIFICATE_BUNDLES=/etc/ssl/certs/custom-ca.pem 153 + 154 + # Multiple certificates 155 + CERTIFICATE_BUNDLES=/certs/ca1.pem,/certs/ca2.pem 156 + 157 + # Corporate CA 158 + CERTIFICATE_BUNDLES=/usr/local/share/ca-certificates/corporate-ca.crt 159 + ``` 160 + 161 + ## Caching Configuration 162 + 163 + ### `REDIS_URL` 164 + 165 + **Required**: No (but highly recommended for production) 166 + **Type**: String 167 + **Format**: Redis connection URL 168 + 169 + Redis connection URL for persistent caching. Enables distributed caching and better performance. 170 + 171 + **Examples**: 172 + ```bash 173 + # Local Redis (no auth) 174 + REDIS_URL=redis://localhost:6379/0 175 + 176 + # With authentication 177 + REDIS_URL=redis://user:password@redis.example.com:6379/0 178 + 179 + # Using database 1 180 + REDIS_URL=redis://localhost:6379/1 181 + 182 + # Redis Sentinel 183 + REDIS_URL=redis-sentinel://sentinel1:26379,sentinel2:26379/mymaster/0 184 + 185 + # TLS connection 186 + REDIS_URL=rediss://secure-redis.example.com:6380/0 187 + ``` 188 + 189 + ### `CACHE_TTL_MEMORY` 190 + 191 + **Required**: No 192 + **Type**: Integer (seconds) 193 + **Default**: `600` (10 minutes) 194 + **Range**: 60-3600 (recommended) 195 + **Constraints**: Must be > 0 196 + 197 + Time-to-live for in-memory cache entries in seconds. Used when Redis is not available. 198 + 199 + **Examples**: 200 + ```bash 201 + CACHE_TTL_MEMORY=300 # 5 minutes (aggressive refresh) 202 + CACHE_TTL_MEMORY=600 # 10 minutes (default, balanced) 203 + CACHE_TTL_MEMORY=1800 # 30 minutes (less frequent updates) 204 + CACHE_TTL_MEMORY=3600 # 1 hour (stable data) 205 + ``` 206 + 207 + **Recommendations**: 208 + - Lower values: Fresher data, more DNS/HTTP lookups, higher load 209 + - Higher values: Better performance, potentially stale data 210 + - Production with Redis: Can use lower values (300-600) 211 + - Production without Redis: Use higher values (1800-3600) 212 + 213 + ### `CACHE_TTL_REDIS` 214 + 215 + **Required**: No 216 + **Type**: Integer (seconds) 217 + **Default**: `7776000` (90 days) 218 + **Range**: 3600-31536000 (1 hour to 1 year) 219 + **Constraints**: Must be > 0 220 + 221 + Time-to-live for Redis cache entries in seconds. 222 + 223 + **Examples**: 224 + ```bash 225 + CACHE_TTL_REDIS=3600 # 1 hour (frequently changing data) 226 + CACHE_TTL_REDIS=86400 # 1 day (recommended for active handles) 227 + CACHE_TTL_REDIS=604800 # 1 week (balanced) 228 + CACHE_TTL_REDIS=2592000 # 30 days (stable handles) 229 + CACHE_TTL_REDIS=7776000 # 90 days (default, maximum stability) 230 + ``` 231 + 232 + **Recommendations**: 233 + - Social media handles: 1-7 days 234 + - Corporate/stable handles: 30-90 days 235 + - Test environments: 1 hour 236 + 237 + ## Queue Configuration 238 + 239 + ### `QUEUE_ADAPTER` 240 + 241 + **Required**: No 242 + **Type**: String 243 + **Default**: `mpsc` 244 + **Values**: `mpsc`, `redis`, `noop` 245 + 246 + The type of queue adapter for background handle resolution. 247 + 248 + **Options**: 249 + - `mpsc`: In-memory multi-producer single-consumer queue (default) 250 + - `redis`: Redis-backed distributed queue 251 + - `noop`: Disable queue processing (testing only) 252 + 253 + **Examples**: 254 + ```bash 255 + # Single instance deployment 256 + QUEUE_ADAPTER=mpsc 257 + 258 + # Multi-instance or high availability 259 + QUEUE_ADAPTER=redis 260 + 261 + # Testing without background processing 262 + QUEUE_ADAPTER=noop 263 + ``` 264 + 265 + ### `QUEUE_REDIS_URL` 266 + 267 + **Required**: No 268 + **Type**: String 269 + **Default**: Falls back to `REDIS_URL` 270 + 271 + Dedicated Redis URL for queue operations. Use when separating cache and queue Redis instances. 272 + 273 + **Examples**: 274 + ```bash 275 + # Separate Redis for queues 276 + QUEUE_REDIS_URL=redis://queue-redis:6379/2 277 + 278 + # With different credentials 279 + QUEUE_REDIS_URL=redis://queue_user:queue_pass@redis.example.com:6379/1 280 + ``` 281 + 282 + ### `QUEUE_REDIS_PREFIX` 283 + 284 + **Required**: No 285 + **Type**: String 286 + **Default**: `queue:handleresolver:` 287 + 288 + Redis key prefix for queue operations. Use to namespace queues when sharing Redis. 289 + 290 + **Examples**: 291 + ```bash 292 + # Default 293 + QUEUE_REDIS_PREFIX=queue:handleresolver: 294 + 295 + # Environment-specific 296 + QUEUE_REDIS_PREFIX=prod:queue:hr: 297 + QUEUE_REDIS_PREFIX=staging:queue:hr: 298 + 299 + # Version-specific 300 + QUEUE_REDIS_PREFIX=quickdid:v1:queue: 301 + 302 + # Instance-specific 303 + QUEUE_REDIS_PREFIX=us-east-1:queue:hr: 304 + ``` 305 + 306 + ### `QUEUE_REDIS_TIMEOUT` 307 + 308 + **Required**: No 309 + **Type**: Integer (seconds) 310 + **Default**: `5` 311 + **Range**: 1-60 (recommended) 312 + **Constraints**: Must be > 0 313 + 314 + Redis blocking timeout for queue operations in seconds. Controls how long to wait for new items. 315 + 316 + **Examples**: 317 + ```bash 318 + QUEUE_REDIS_TIMEOUT=1 # Very responsive, more polling 319 + QUEUE_REDIS_TIMEOUT=5 # Default, balanced 320 + QUEUE_REDIS_TIMEOUT=10 # Less polling, slower shutdown 321 + QUEUE_REDIS_TIMEOUT=30 # Minimal polling, slow shutdown 322 + ``` 323 + 324 + ### `QUEUE_WORKER_ID` 325 + 326 + **Required**: No 327 + **Type**: String 328 + **Default**: Auto-generated UUID 329 + 330 + Worker identifier for queue operations. Used in logs and monitoring. 331 + 332 + **Examples**: 333 + ```bash 334 + # Simple numbering 335 + QUEUE_WORKER_ID=worker-001 336 + 337 + # Environment-based 338 + QUEUE_WORKER_ID=prod-us-east-1 339 + QUEUE_WORKER_ID=staging-worker-2 340 + 341 + # Hostname-based 342 + QUEUE_WORKER_ID=$(hostname) 343 + 344 + # Pod name in Kubernetes 345 + QUEUE_WORKER_ID=$HOSTNAME 346 + ``` 347 + 348 + ### `QUEUE_BUFFER_SIZE` 349 + 350 + **Required**: No 351 + **Type**: Integer 352 + **Default**: `1000` 353 + **Range**: 100-100000 (recommended) 354 + 355 + Buffer size for the MPSC queue adapter. Only used when `QUEUE_ADAPTER=mpsc`. 356 + 357 + **Examples**: 358 + ```bash 359 + QUEUE_BUFFER_SIZE=100 # Minimal memory, may block 360 + QUEUE_BUFFER_SIZE=1000 # Default, balanced 361 + QUEUE_BUFFER_SIZE=5000 # High traffic 362 + QUEUE_BUFFER_SIZE=10000 # Very high traffic 363 + ``` 364 + 365 + ## Configuration Examples 366 + 367 + ### Minimal Development Configuration 368 + 369 + ```bash 370 + # .env.development 371 + HTTP_EXTERNAL=localhost:3007 372 + SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 373 + RUST_LOG=debug 374 + ``` 375 + 376 + ### Standard Production Configuration 377 + 378 + ```bash 379 + # .env.production 380 + # Required 381 + HTTP_EXTERNAL=quickdid.example.com 382 + SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 383 + 384 + # Network 385 + HTTP_PORT=8080 386 + USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com) 387 + 388 + # Caching 389 + REDIS_URL=redis://redis:6379/0 390 + CACHE_TTL_MEMORY=600 391 + CACHE_TTL_REDIS=86400 # 1 day 392 + 393 + # Queue 394 + QUEUE_ADAPTER=redis 395 + QUEUE_REDIS_TIMEOUT=5 396 + QUEUE_BUFFER_SIZE=5000 397 + 398 + # Logging 399 + RUST_LOG=info 400 + ``` 401 + 402 + ### High-Availability Configuration 403 + 404 + ```bash 405 + # .env.ha 406 + # Required 407 + HTTP_EXTERNAL=quickdid.example.com 408 + SERVICE_KEY=${SECRET_SERVICE_KEY} 409 + 410 + # Network 411 + HTTP_PORT=8080 412 + DNS_NAMESERVERS=8.8.8.8,8.8.4.4,1.1.1.1,1.0.0.1 413 + 414 + # Caching (separate Redis instances) 415 + REDIS_URL=redis://cache-redis:6379/0 416 + CACHE_TTL_MEMORY=300 417 + CACHE_TTL_REDIS=3600 418 + 419 + # Queue (dedicated Redis) 420 + QUEUE_ADAPTER=redis 421 + QUEUE_REDIS_URL=redis://queue-redis:6379/0 422 + QUEUE_REDIS_PREFIX=prod:queue: 423 + QUEUE_WORKER_ID=${HOSTNAME} 424 + QUEUE_REDIS_TIMEOUT=10 425 + 426 + # Performance 427 + QUEUE_BUFFER_SIZE=10000 428 + 429 + # Logging 430 + RUST_LOG=warn 431 + ``` 432 + 433 + ### Docker Compose Configuration 434 + 435 + ```yaml 436 + version: '3.8' 437 + 438 + services: 439 + quickdid: 440 + image: quickdid:latest 441 + environment: 442 + HTTP_EXTERNAL: quickdid.example.com 443 + SERVICE_KEY: ${SERVICE_KEY} 444 + HTTP_PORT: 8080 445 + REDIS_URL: redis://redis:6379/0 446 + CACHE_TTL_MEMORY: 600 447 + CACHE_TTL_REDIS: 86400 448 + QUEUE_ADAPTER: redis 449 + QUEUE_REDIS_TIMEOUT: 5 450 + RUST_LOG: info 451 + ports: 452 + - "8080:8080" 453 + depends_on: 454 + - redis 455 + 456 + redis: 457 + image: redis:7-alpine 458 + command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru 459 + ``` 460 + 461 + ## Validation Rules 462 + 463 + QuickDID validates configuration at startup. The following rules are enforced: 464 + 465 + ### Required Fields 466 + 467 + 1. **HTTP_EXTERNAL**: Must be provided 468 + 2. **SERVICE_KEY**: Must be provided 469 + 470 + ### Value Constraints 471 + 472 + 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`): 473 + - Must be positive integers (> 0) 474 + - Recommended minimum: 60 seconds 475 + 476 + 2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`): 477 + - Must be positive integers (> 0) 478 + - Recommended range: 1-60 seconds 479 + 480 + 3. **Queue Adapter** (`QUEUE_ADAPTER`): 481 + - Must be one of: `mpsc`, `redis`, `noop`, `none` 482 + - Case-sensitive 483 + 484 + 4. **Port** (`HTTP_PORT`): 485 + - Must be valid port number (1-65535) 486 + - Ports < 1024 require elevated privileges 487 + 488 + ### Validation Errors 489 + 490 + If validation fails, QuickDID will exit with one of these error codes: 491 + 492 + - `error-quickdid-config-1`: Missing required environment variable 493 + - `error-quickdid-config-2`: Invalid configuration value 494 + - `error-quickdid-config-3`: Invalid TTL value (must be positive) 495 + - `error-quickdid-config-4`: Invalid timeout value (must be positive) 496 + 497 + ### Testing Configuration 498 + 499 + Test your configuration without starting the service: 500 + 501 + ```bash 502 + # Validate configuration 503 + HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 504 + 505 + # Test with specific values 506 + CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation 507 + 508 + # Check parsed configuration (with debug logging) 509 + RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 510 + ``` 511 + 512 + ## Best Practices 513 + 514 + ### Security 515 + 516 + 1. **Never commit SERVICE_KEY** to version control 517 + 2. Use environment-specific key management (Vault, AWS Secrets, etc.) 518 + 3. Rotate SERVICE_KEY regularly 519 + 4. Use TLS for Redis connections in production (`rediss://`) 520 + 5. Implement network segmentation for Redis access 521 + 522 + ### Performance 523 + 524 + 1. **With Redis**: Use lower memory cache TTL (300-600s) 525 + 2. **Without Redis**: Use higher memory cache TTL (1800-3600s) 526 + 3. **High traffic**: Increase QUEUE_BUFFER_SIZE (5000-10000) 527 + 4. **Multi-region**: Use region-specific QUEUE_WORKER_ID 528 + 529 + ### Monitoring 530 + 531 + 1. Set descriptive QUEUE_WORKER_ID for log correlation 532 + 2. Use structured logging with appropriate RUST_LOG levels 533 + 3. Monitor Redis memory usage and adjust TTLs accordingly 534 + 4. Track cache hit rates to optimize TTL values 535 + 536 + ### Deployment 537 + 538 + 1. Use `.env` files for local development 539 + 2. Use secrets management for production SERVICE_KEY 540 + 3. Set resource limits in container orchestration 541 + 4. Use health checks to monitor service availability 542 + 5. Implement gradual rollouts with feature flags
+706
docs/production-deployment.md
··· 1 + # QuickDID Production Deployment Guide 2 + 3 + This guide provides comprehensive instructions for deploying QuickDID in a production environment using Docker. 4 + 5 + ## Table of Contents 6 + 7 + - [Prerequisites](#prerequisites) 8 + - [Environment Configuration](#environment-configuration) 9 + - [Docker Deployment](#docker-deployment) 10 + - [Docker Compose Setup](#docker-compose-setup) 11 + - [Health Monitoring](#health-monitoring) 12 + - [Security Considerations](#security-considerations) 13 + - [Troubleshooting](#troubleshooting) 14 + 15 + ## Prerequisites 16 + 17 + - Docker 20.10.0 or higher 18 + - Docker Compose 2.0.0 or higher (optional, for multi-container setup) 19 + - Redis 6.0 or higher (optional, for persistent caching and queue management) 20 + - Valid SSL certificates for HTTPS (recommended for production) 21 + - Domain name configured with appropriate DNS records 22 + 23 + ## Environment Configuration 24 + 25 + Create a `.env` file in your deployment directory with the following configuration: 26 + 27 + ```bash 28 + # ============================================================================ 29 + # QuickDID Production Environment Configuration 30 + # ============================================================================ 31 + 32 + # ---------------------------------------------------------------------------- 33 + # REQUIRED CONFIGURATION 34 + # ---------------------------------------------------------------------------- 35 + 36 + # External hostname for service endpoints 37 + # This should be your public domain name with port if non-standard 38 + # Examples: 39 + # - quickdid.example.com 40 + # - quickdid.example.com:8080 41 + # - localhost:3007 (for testing only) 42 + HTTP_EXTERNAL=quickdid.example.com 43 + 44 + # Private key for service identity (DID format) 45 + # Generate a new key for production using atproto-identity tools 46 + # SECURITY: Keep this key secure and never commit to version control 47 + # Example formats: 48 + # - did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 49 + # - did:plc:xyz123abc456 50 + SERVICE_KEY=did:key:YOUR_PRODUCTION_KEY_HERE 51 + 52 + # ---------------------------------------------------------------------------- 53 + # NETWORK CONFIGURATION 54 + # ---------------------------------------------------------------------------- 55 + 56 + # HTTP server port (default: 8080) 57 + # This is the port the service will bind to inside the container 58 + # Map this to your desired external port in docker-compose.yml 59 + HTTP_PORT=8080 60 + 61 + # PLC directory hostname (default: plc.directory) 62 + # Change this if using a custom PLC directory or testing environment 63 + PLC_HOSTNAME=plc.directory 64 + 65 + # ---------------------------------------------------------------------------- 66 + # CACHING CONFIGURATION 67 + # ---------------------------------------------------------------------------- 68 + 69 + # Redis connection URL for caching (highly recommended for production) 70 + # Format: redis://[username:password@]host:port/database 71 + # Examples: 72 + # - redis://localhost:6379/0 (local Redis, no auth) 73 + # - redis://user:pass@redis.example.com:6379/0 (remote with auth) 74 + # - redis://redis:6379/0 (Docker network) 75 + # - rediss://secure-redis.example.com:6380/0 (TLS) 76 + # Benefits: Persistent cache, distributed caching, better performance 77 + REDIS_URL=redis://redis:6379/0 78 + 79 + # TTL for in-memory cache in seconds (default: 600 = 10 minutes) 80 + # Range: 60-3600 recommended 81 + # Lower = fresher data, more DNS/HTTP lookups 82 + # Higher = better performance, potentially stale data 83 + CACHE_TTL_MEMORY=600 84 + 85 + # TTL for Redis cache in seconds (default: 7776000 = 90 days) 86 + # Range: 3600-31536000 (1 hour to 1 year) 87 + # Recommendations: 88 + # - 86400 (1 day) for frequently changing data 89 + # - 604800 (1 week) for balanced performance 90 + # - 7776000 (90 days) for stable data 91 + CACHE_TTL_REDIS=86400 92 + 93 + # ---------------------------------------------------------------------------- 94 + # QUEUE CONFIGURATION 95 + # ---------------------------------------------------------------------------- 96 + 97 + # Queue adapter type: 'mpsc', 'redis', or 'noop' (default: mpsc) 98 + # - 'mpsc': In-memory queue for single-instance deployments 99 + # - 'redis': Distributed queue for multi-instance or HA deployments 100 + # - 'noop': Disable queue processing (testing only) 101 + QUEUE_ADAPTER=redis 102 + 103 + # Redis URL for queue adapter (uses REDIS_URL if not set) 104 + # Set this if you want to use a separate Redis instance for queuing 105 + # QUEUE_REDIS_URL=redis://queue-redis:6379/1 106 + 107 + # Redis key prefix for queues (default: queue:handleresolver:) 108 + # Useful when sharing Redis instance with other services 109 + QUEUE_REDIS_PREFIX=queue:quickdid:prod: 110 + 111 + # Redis blocking timeout for queue operations in seconds (default: 5) 112 + # Range: 1-60 recommended 113 + # Lower = more responsive to shutdown, more polling 114 + # Higher = less polling overhead, slower shutdown 115 + QUEUE_REDIS_TIMEOUT=5 116 + 117 + # Worker ID for Redis queue (auto-generated UUID if not set) 118 + # Set this for predictable worker identification in multi-instance deployments 119 + # Examples: worker-001, prod-us-east-1, $(hostname) 120 + # QUEUE_WORKER_ID=worker-001 121 + 122 + # Buffer size for MPSC queue (default: 1000) 123 + # Range: 100-100000 124 + # Increase for high-traffic deployments using MPSC adapter 125 + QUEUE_BUFFER_SIZE=5000 126 + 127 + # ---------------------------------------------------------------------------- 128 + # HTTP CLIENT CONFIGURATION 129 + # ---------------------------------------------------------------------------- 130 + 131 + # HTTP User-Agent header 132 + # Identifies your service to other AT Protocol services 133 + # Default: Auto-generated with current version from Cargo.toml 134 + # Format: quickdid/{version} (+https://github.com/smokesignal.events/quickdid) 135 + USER_AGENT=quickdid/1.0.0-rc.1 (+https://quickdid.example.com) 136 + 137 + # Custom DNS nameservers (comma-separated) 138 + # Use for custom DNS resolution or to bypass local DNS 139 + # Examples: 140 + # - 8.8.8.8,8.8.4.4 (Google DNS) 141 + # - 1.1.1.1,1.0.0.1 (Cloudflare DNS) 142 + # DNS_NAMESERVERS=1.1.1.1,1.0.0.1 143 + 144 + # Additional CA certificates (comma-separated file paths) 145 + # Use when connecting to services with custom CA certificates 146 + # CERTIFICATE_BUNDLES=/certs/custom-ca.pem,/certs/internal-ca.pem 147 + 148 + # ---------------------------------------------------------------------------- 149 + # LOGGING AND MONITORING 150 + # ---------------------------------------------------------------------------- 151 + 152 + # Logging level (debug, info, warn, error) 153 + # Use 'info' for production, 'debug' for troubleshooting 154 + RUST_LOG=info 155 + 156 + # Structured logging format (optional) 157 + # Set to 'json' for machine-readable logs 158 + # RUST_LOG_FORMAT=json 159 + 160 + # ---------------------------------------------------------------------------- 161 + # PERFORMANCE TUNING 162 + # ---------------------------------------------------------------------------- 163 + 164 + # Tokio runtime worker threads (defaults to CPU count) 165 + # Adjust based on your container's CPU allocation 166 + # TOKIO_WORKER_THREADS=4 167 + 168 + # Maximum concurrent connections (optional) 169 + # Helps prevent resource exhaustion 170 + # MAX_CONNECTIONS=10000 171 + 172 + # ---------------------------------------------------------------------------- 173 + # DOCKER-SPECIFIC CONFIGURATION 174 + # ---------------------------------------------------------------------------- 175 + 176 + # Container restart policy (for docker-compose) 177 + # Options: no, always, on-failure, unless-stopped 178 + RESTART_POLICY=unless-stopped 179 + 180 + # Resource limits (for docker-compose) 181 + # Adjust based on your available resources 182 + MEMORY_LIMIT=512M 183 + CPU_LIMIT=1.0 184 + ``` 185 + 186 + ## Docker Deployment 187 + 188 + ### Building the Docker Image 189 + 190 + Create a `Dockerfile` in your project root: 191 + 192 + ```dockerfile 193 + # Build stage 194 + FROM rust:1.75-slim AS builder 195 + 196 + # Install build dependencies 197 + RUN apt-get update && apt-get install -y \ 198 + pkg-config \ 199 + libssl-dev \ 200 + && rm -rf /var/lib/apt/lists/* 201 + 202 + # Create app directory 203 + WORKDIR /app 204 + 205 + # Copy source files 206 + COPY Cargo.toml Cargo.lock ./ 207 + COPY src ./src 208 + 209 + # Build the application 210 + RUN cargo build --release 211 + 212 + # Runtime stage 213 + FROM debian:bookworm-slim 214 + 215 + # Install runtime dependencies 216 + RUN apt-get update && apt-get install -y \ 217 + ca-certificates \ 218 + libssl3 \ 219 + curl \ 220 + && rm -rf /var/lib/apt/lists/* 221 + 222 + # Create non-root user 223 + RUN useradd -m -u 1000 quickdid 224 + 225 + # Copy binary from builder 226 + COPY --from=builder /app/target/release/quickdid /usr/local/bin/quickdid 227 + 228 + # Set ownership and permissions 229 + RUN chown quickdid:quickdid /usr/local/bin/quickdid 230 + 231 + # Switch to non-root user 232 + USER quickdid 233 + 234 + # Expose default port 235 + EXPOSE 8080 236 + 237 + # Health check 238 + HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 239 + CMD curl -f http://localhost:8080/health || exit 1 240 + 241 + # Run the application 242 + ENTRYPOINT ["quickdid"] 243 + ``` 244 + 245 + Build the image: 246 + 247 + ```bash 248 + docker build -t quickdid:latest . 249 + ``` 250 + 251 + ### Running a Single Instance 252 + 253 + ```bash 254 + # Run with environment file 255 + docker run -d \ 256 + --name quickdid \ 257 + --env-file .env \ 258 + -p 8080:8080 \ 259 + --restart unless-stopped \ 260 + quickdid:latest 261 + ``` 262 + 263 + ## Docker Compose Setup 264 + 265 + Create a `docker-compose.yml` file for a complete production setup: 266 + 267 + ```yaml 268 + version: '3.8' 269 + 270 + services: 271 + quickdid: 272 + image: quickdid:latest 273 + container_name: quickdid 274 + env_file: .env 275 + ports: 276 + - "8080:8080" 277 + depends_on: 278 + redis: 279 + condition: service_healthy 280 + networks: 281 + - quickdid-network 282 + restart: ${RESTART_POLICY:-unless-stopped} 283 + deploy: 284 + resources: 285 + limits: 286 + memory: ${MEMORY_LIMIT:-512M} 287 + cpus: ${CPU_LIMIT:-1.0} 288 + reservations: 289 + memory: 256M 290 + cpus: '0.5' 291 + healthcheck: 292 + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] 293 + interval: 30s 294 + timeout: 3s 295 + retries: 3 296 + start_period: 10s 297 + logging: 298 + driver: "json-file" 299 + options: 300 + max-size: "10m" 301 + max-file: "3" 302 + 303 + redis: 304 + image: redis:7-alpine 305 + container_name: quickdid-redis 306 + command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru 307 + volumes: 308 + - redis-data:/data 309 + networks: 310 + - quickdid-network 311 + restart: unless-stopped 312 + healthcheck: 313 + test: ["CMD", "redis-cli", "ping"] 314 + interval: 10s 315 + timeout: 3s 316 + retries: 3 317 + logging: 318 + driver: "json-file" 319 + options: 320 + max-size: "10m" 321 + max-file: "3" 322 + 323 + # Optional: Nginx reverse proxy with SSL 324 + nginx: 325 + image: nginx:alpine 326 + container_name: quickdid-nginx 327 + ports: 328 + - "80:80" 329 + - "443:443" 330 + volumes: 331 + - ./nginx.conf:/etc/nginx/nginx.conf:ro 332 + - ./certs:/etc/nginx/certs:ro 333 + - ./acme-challenge:/var/www/acme:ro 334 + depends_on: 335 + - quickdid 336 + networks: 337 + - quickdid-network 338 + restart: unless-stopped 339 + logging: 340 + driver: "json-file" 341 + options: 342 + max-size: "10m" 343 + max-file: "3" 344 + 345 + networks: 346 + quickdid-network: 347 + driver: bridge 348 + 349 + volumes: 350 + redis-data: 351 + driver: local 352 + ``` 353 + 354 + ### Nginx Configuration (nginx.conf) 355 + 356 + ```nginx 357 + events { 358 + worker_connections 1024; 359 + } 360 + 361 + http { 362 + upstream quickdid { 363 + server quickdid:8080; 364 + } 365 + 366 + server { 367 + listen 80; 368 + server_name quickdid.example.com; 369 + 370 + # ACME challenge for Let's Encrypt 371 + location /.well-known/acme-challenge/ { 372 + root /var/www/acme; 373 + } 374 + 375 + # Redirect HTTP to HTTPS 376 + location / { 377 + return 301 https://$server_name$request_uri; 378 + } 379 + } 380 + 381 + server { 382 + listen 443 ssl http2; 383 + server_name quickdid.example.com; 384 + 385 + ssl_certificate /etc/nginx/certs/fullchain.pem; 386 + ssl_certificate_key /etc/nginx/certs/privkey.pem; 387 + ssl_protocols TLSv1.2 TLSv1.3; 388 + ssl_ciphers HIGH:!aNULL:!MD5; 389 + 390 + location / { 391 + proxy_pass http://quickdid; 392 + proxy_set_header Host $host; 393 + proxy_set_header X-Real-IP $remote_addr; 394 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 395 + proxy_set_header X-Forwarded-Proto $scheme; 396 + 397 + # WebSocket support (if needed) 398 + proxy_http_version 1.1; 399 + proxy_set_header Upgrade $http_upgrade; 400 + proxy_set_header Connection "upgrade"; 401 + 402 + # Timeouts 403 + proxy_connect_timeout 60s; 404 + proxy_send_timeout 60s; 405 + proxy_read_timeout 60s; 406 + } 407 + 408 + # Health check endpoint 409 + location /health { 410 + proxy_pass http://quickdid/health; 411 + access_log off; 412 + } 413 + } 414 + } 415 + ``` 416 + 417 + ### Starting the Stack 418 + 419 + ```bash 420 + # Start all services 421 + docker-compose up -d 422 + 423 + # View logs 424 + docker-compose logs -f 425 + 426 + # Check service status 427 + docker-compose ps 428 + 429 + # Stop all services 430 + docker-compose down 431 + ``` 432 + 433 + ## Health Monitoring 434 + 435 + QuickDID provides health check endpoints for monitoring: 436 + 437 + ### Basic Health Check 438 + 439 + ```bash 440 + curl http://quickdid.example.com/health 441 + ``` 442 + 443 + Expected response: 444 + ```json 445 + { 446 + "status": "healthy", 447 + "version": "1.0.0", 448 + "uptime_seconds": 3600 449 + } 450 + ``` 451 + 452 + ### Monitoring with Prometheus (Optional) 453 + 454 + Add to your `docker-compose.yml`: 455 + 456 + ```yaml 457 + prometheus: 458 + image: prom/prometheus:latest 459 + container_name: quickdid-prometheus 460 + volumes: 461 + - ./prometheus.yml:/etc/prometheus/prometheus.yml 462 + - prometheus-data:/prometheus 463 + command: 464 + - '--config.file=/etc/prometheus/prometheus.yml' 465 + - '--storage.tsdb.path=/prometheus' 466 + ports: 467 + - "9090:9090" 468 + networks: 469 + - quickdid-network 470 + restart: unless-stopped 471 + 472 + volumes: 473 + prometheus-data: 474 + driver: local 475 + ``` 476 + 477 + ## Security Considerations 478 + 479 + ### 1. Service Key Protection 480 + 481 + - **Never commit** the `SERVICE_KEY` to version control 482 + - Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager) 483 + - Rotate keys regularly 484 + - Use different keys for different environments 485 + 486 + ### 2. Network Security 487 + 488 + - Use HTTPS in production with valid SSL certificates 489 + - Implement rate limiting at the reverse proxy level 490 + - Use firewall rules to restrict access to Redis 491 + - Enable Redis authentication in production 492 + 493 + ### 3. Container Security 494 + 495 + - Run containers as non-root user (already configured in Dockerfile) 496 + - Keep base images updated 497 + - Scan images for vulnerabilities regularly 498 + - Use read-only filesystems where possible 499 + 500 + ### 4. Redis Security 501 + 502 + ```bash 503 + # Add to Redis configuration for production 504 + requirepass your_strong_password_here 505 + maxclients 10000 506 + timeout 300 507 + ``` 508 + 509 + ### 5. Environment Variables 510 + 511 + - Use Docker secrets or external secret management 512 + - Avoid logging sensitive environment variables 513 + - Implement proper access controls 514 + 515 + ## Troubleshooting 516 + 517 + ### Common Issues and Solutions 518 + 519 + #### 1. Container Won't Start 520 + 521 + ```bash 522 + # Check logs 523 + docker logs quickdid 524 + 525 + # Verify environment variables 526 + docker exec quickdid env | grep -E "HTTP_EXTERNAL|SERVICE_KEY" 527 + 528 + # Test Redis connectivity 529 + docker exec quickdid redis-cli -h redis ping 530 + ``` 531 + 532 + #### 2. Handle Resolution Failures 533 + 534 + ```bash 535 + # Enable debug logging 536 + docker exec quickdid sh -c "export RUST_LOG=debug" 537 + 538 + # Check DNS resolution 539 + docker exec quickdid nslookup plc.directory 540 + 541 + # Verify Redis cache 542 + docker exec -it quickdid-redis redis-cli 543 + > KEYS handle:* 544 + > TTL handle:example_key 545 + ``` 546 + 547 + #### 3. Performance Issues 548 + 549 + ```bash 550 + # Monitor Redis memory usage 551 + docker exec quickdid-redis redis-cli INFO memory 552 + 553 + # Check container resource usage 554 + docker stats quickdid 555 + 556 + # Analyze slow queries (with debug logging) 557 + docker logs quickdid | grep "resolution took" 558 + ``` 559 + 560 + #### 4. Health Check Failures 561 + 562 + ```bash 563 + # Manual health check 564 + docker exec quickdid curl -v http://localhost:8080/health 565 + 566 + # Check service binding 567 + docker exec quickdid netstat -tlnp | grep 8080 568 + ``` 569 + 570 + ### Debugging Commands 571 + 572 + ```bash 573 + # Interactive shell in container 574 + docker exec -it quickdid /bin/bash 575 + 576 + # Test handle resolution 577 + curl "http://localhost:8080/xrpc/com.atproto.identity.resolveHandle?handle=example.bsky.social" 578 + 579 + # Check Redis keys 580 + docker exec quickdid-redis redis-cli --scan --pattern "handle:*" | head -20 581 + 582 + # Monitor real-time logs 583 + docker-compose logs -f quickdid | grep -E "ERROR|WARN" 584 + ``` 585 + 586 + ## Maintenance 587 + 588 + ### Backup and Restore 589 + 590 + ```bash 591 + # Backup Redis data 592 + docker exec quickdid-redis redis-cli BGSAVE 593 + docker cp quickdid-redis:/data/dump.rdb ./backups/redis-$(date +%Y%m%d).rdb 594 + 595 + # Restore Redis data 596 + docker cp ./backups/redis-backup.rdb quickdid-redis:/data/dump.rdb 597 + docker restart quickdid-redis 598 + ``` 599 + 600 + ### Updates and Rollbacks 601 + 602 + ```bash 603 + # Update to new version 604 + docker pull quickdid:new-version 605 + docker-compose down 606 + docker-compose up -d 607 + 608 + # Rollback if needed 609 + docker-compose down 610 + docker tag quickdid:previous quickdid:latest 611 + docker-compose up -d 612 + ``` 613 + 614 + ### Log Rotation 615 + 616 + Configure Docker's built-in log rotation in `/etc/docker/daemon.json`: 617 + 618 + ```json 619 + { 620 + "log-driver": "json-file", 621 + "log-opts": { 622 + "max-size": "10m", 623 + "max-file": "3" 624 + } 625 + } 626 + ``` 627 + 628 + ## Performance Optimization 629 + 630 + ### Redis Optimization 631 + 632 + ```redis 633 + # Add to redis.conf or pass as command arguments 634 + maxmemory 2gb 635 + maxmemory-policy allkeys-lru 636 + save "" # Disable persistence for cache-only usage 637 + tcp-keepalive 300 638 + timeout 0 639 + ``` 640 + 641 + ### System Tuning 642 + 643 + ```bash 644 + # Add to host system's /etc/sysctl.conf 645 + net.core.somaxconn = 1024 646 + net.ipv4.tcp_tw_reuse = 1 647 + net.ipv4.ip_local_port_range = 10000 65000 648 + fs.file-max = 100000 649 + ``` 650 + 651 + ## Configuration Validation 652 + 653 + QuickDID validates all configuration at startup. The following rules are enforced: 654 + 655 + ### Required Fields 656 + 657 + - **HTTP_EXTERNAL**: Must be provided 658 + - **SERVICE_KEY**: Must be provided 659 + 660 + ### Value Constraints 661 + 662 + 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`): 663 + - Must be positive integers (> 0) 664 + - Recommended minimum: 60 seconds 665 + 666 + 2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`): 667 + - Must be positive integers (> 0) 668 + - Recommended range: 1-60 seconds 669 + 670 + 3. **Queue Adapter** (`QUEUE_ADAPTER`): 671 + - Must be one of: `mpsc`, `redis`, `noop`, `none` 672 + - Case-sensitive 673 + 674 + ### Validation Errors 675 + 676 + If validation fails, QuickDID will exit with one of these error codes: 677 + 678 + - `error-quickdid-config-1`: Missing required environment variable 679 + - `error-quickdid-config-2`: Invalid configuration value 680 + - `error-quickdid-config-3`: Invalid TTL value (must be positive) 681 + - `error-quickdid-config-4`: Invalid timeout value (must be positive) 682 + 683 + ### Testing Configuration 684 + 685 + ```bash 686 + # Validate configuration without starting service 687 + HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 688 + 689 + # Test with specific values (will fail validation) 690 + CACHE_TTL_MEMORY=0 quickdid --help 691 + 692 + # Debug configuration parsing 693 + RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 694 + ``` 695 + 696 + ## Support and Resources 697 + 698 + - **Documentation**: [QuickDID GitHub Repository](https://github.com/smokesignal.events/quickdid) 699 + - **Configuration Reference**: See [configuration-reference.md](./configuration-reference.md) for detailed documentation of all options 700 + - **AT Protocol Specs**: [atproto.com](https://atproto.com) 701 + - **Issues**: Report bugs via GitHub Issues 702 + - **Community**: Join the AT Protocol Discord server 703 + 704 + ## License 705 + 706 + QuickDID is licensed under the MIT License. See LICENSE file for details.
+27 -7
src/bin/quickdid.rs
··· 2 2 use async_trait::async_trait; 3 3 use atproto_identity::{ 4 4 config::{CertificateBundles, DnsNameservers}, 5 - key::{identify_key, to_public, KeyData, KeyProvider}, 5 + key::{KeyData, KeyProvider, identify_key, to_public}, 6 6 resolve::HickoryDnsResolver, 7 7 }; 8 8 use clap::Parser; ··· 58 58 let args = Args::parse(); 59 59 let config = Config::from_args(args)?; 60 60 61 + // Validate configuration 62 + config.validate()?; 63 + 61 64 tracing::info!("Starting QuickDID service on port {}", config.http_port); 62 65 tracing::info!("Service DID: {}", config.service_did); 66 + tracing::info!( 67 + "Cache TTL - Memory: {}s, Redis: {}s", 68 + config.cache_ttl_memory, 69 + config.cache_ttl_redis 70 + ); 63 71 64 72 // Parse certificate bundles if provided 65 73 let certificate_bundles: CertificateBundles = config ··· 135 143 // Create handle resolver with Redis caching if available, otherwise use in-memory caching 136 144 let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> = 137 145 if let Some(pool) = redis_pool { 138 - tracing::info!("Using Redis-backed handle resolver with 90-day cache TTL"); 139 - Arc::new(RedisHandleResolver::new(base_handle_resolver, pool)) 146 + tracing::info!( 147 + "Using Redis-backed handle resolver with {}-second cache TTL", 148 + config.cache_ttl_redis 149 + ); 150 + Arc::new(RedisHandleResolver::with_ttl( 151 + base_handle_resolver, 152 + pool, 153 + config.cache_ttl_redis, 154 + )) 140 155 } else { 141 - tracing::info!("Using in-memory handle resolver with 10-minute cache TTL"); 156 + tracing::info!( 157 + "Using in-memory handle resolver with {}-second cache TTL", 158 + config.cache_ttl_memory 159 + ); 142 160 Arc::new(CachingHandleResolver::new( 143 161 base_handle_resolver, 144 - 600, // 10 minutes TTL for in-memory cache 162 + config.cache_ttl_memory, 145 163 )) 146 164 }; 147 165 ··· 174 192 pool, 175 193 config.queue_worker_id.clone(), 176 194 config.queue_redis_prefix.clone(), 177 - 5, // 5 second timeout for blocking operations 195 + config.queue_redis_timeout, // Configurable timeout for blocking operations 178 196 )) 179 197 } 180 198 Err(e) => { ··· 192 210 } 193 211 }, 194 212 None => { 195 - tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter"); 213 + tracing::warn!( 214 + "Redis queue adapter requested but no Redis URL configured, using no-op adapter" 215 + ); 196 216 Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new()) 197 217 } 198 218 }
+18 -3
src/cache.rs
··· 1 1 //! Redis cache utilities for QuickDID 2 2 3 - use anyhow::Result; 4 3 use deadpool_redis::{Config, Pool, Runtime}; 4 + use thiserror::Error; 5 + 6 + /// Cache-specific errors following the QuickDID error format 7 + #[derive(Debug, Error)] 8 + pub enum CacheError { 9 + #[error("error-quickdid-cache-1 Redis pool creation failed: {0}")] 10 + PoolCreationFailed(String), 11 + 12 + #[error("error-quickdid-cache-2 Invalid Redis URL: {0}")] 13 + InvalidRedisUrl(String), 14 + 15 + #[error("error-quickdid-cache-3 Redis connection failed: {0}")] 16 + ConnectionFailed(String), 17 + } 5 18 6 19 /// Create a Redis connection pool from a Redis URL 7 - pub fn create_redis_pool(redis_url: &str) -> Result<Pool> { 20 + pub fn create_redis_pool(redis_url: &str) -> Result<Pool, CacheError> { 8 21 let config = Config::from_url(redis_url); 9 - let pool = config.create_pool(Some(Runtime::Tokio1))?; 22 + let pool = config 23 + .create_pool(Some(Runtime::Tokio1)) 24 + .map_err(|e| CacheError::PoolCreationFailed(e.to_string()))?; 10 25 Ok(pool) 11 26 }
+328 -11
src/config.rs
··· 1 - use anyhow::Result; 1 + //! Configuration management for QuickDID service 2 + //! 3 + //! This module handles all configuration parsing, validation, and error handling 4 + //! for the QuickDID AT Protocol identity resolution service. 5 + //! 6 + //! ## Configuration Sources 7 + //! 8 + //! Configuration can be provided through: 9 + //! - Environment variables (highest priority) 10 + //! - Command-line arguments 11 + //! - Default values (lowest priority) 12 + //! 13 + //! ## Example 14 + //! 15 + //! ```bash 16 + //! # Minimal configuration 17 + //! HTTP_EXTERNAL=quickdid.example.com \ 18 + //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 19 + //! quickdid 20 + //! 21 + //! # Full configuration with Redis and custom settings 22 + //! HTTP_EXTERNAL=quickdid.example.com \ 23 + //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 24 + //! HTTP_PORT=3000 \ 25 + //! REDIS_URL=redis://localhost:6379 \ 26 + //! CACHE_TTL_MEMORY=300 \ 27 + //! CACHE_TTL_REDIS=86400 \ 28 + //! QUEUE_ADAPTER=redis \ 29 + //! QUEUE_REDIS_TIMEOUT=10 \ 30 + //! quickdid 31 + //! ``` 32 + 2 33 use atproto_identity::config::optional_env; 3 34 use clap::Parser; 35 + use thiserror::Error; 36 + 37 + /// Configuration-specific errors following the QuickDID error format 38 + /// 39 + /// All errors follow the pattern: `error-quickdid-config-{number} {message}: {details}` 40 + #[derive(Debug, Error)] 41 + pub enum ConfigError { 42 + /// Missing required environment variable or command-line argument 43 + /// 44 + /// Example: When SERVICE_KEY or HTTP_EXTERNAL are not provided 45 + #[error("error-quickdid-config-1 Missing required environment variable: {0}")] 46 + MissingRequired(String), 47 + 48 + /// Invalid configuration value that doesn't meet expected format or constraints 49 + /// 50 + /// Example: Invalid QUEUE_ADAPTER value (must be 'mpsc', 'redis', or 'noop') 51 + #[error("error-quickdid-config-2 Invalid configuration value: {0}")] 52 + InvalidValue(String), 53 + 54 + /// Invalid TTL (Time To Live) value 55 + /// 56 + /// TTL values must be positive integers representing seconds 57 + #[error("error-quickdid-config-3 Invalid TTL value (must be positive): {0}")] 58 + InvalidTtl(String), 59 + 60 + /// Invalid timeout value 61 + /// 62 + /// Timeout values must be positive integers representing seconds 63 + #[error("error-quickdid-config-4 Invalid timeout value (must be positive): {0}")] 64 + InvalidTimeout(String), 65 + } 4 66 5 67 #[derive(Parser, Clone)] 6 68 #[command( ··· 22 84 HTTP_EXTERNAL External hostname for service endpoints (required) 23 85 HTTP_PORT HTTP server port (default: 8080) 24 86 PLC_HOSTNAME PLC directory hostname (default: plc.directory) 25 - USER_AGENT HTTP User-Agent header (auto-generated) 26 - DNS_NAMESERVERS Custom DNS nameservers (optional) 27 - CERTIFICATE_BUNDLES Additional CA certificates (optional) 87 + USER_AGENT HTTP User-Agent header (auto-generated with version) 88 + DNS_NAMESERVERS Custom DNS nameservers (comma-separated IPs) 89 + CERTIFICATE_BUNDLES Additional CA certificates (comma-separated paths) 90 + 91 + CACHING: 28 92 REDIS_URL Redis URL for handle resolution caching (optional) 29 - QUEUE_ADAPTER Queue adapter type: 'mpsc' or 'redis' (default: mpsc) 93 + CACHE_TTL_MEMORY TTL for in-memory cache in seconds (default: 600) 94 + CACHE_TTL_REDIS TTL for Redis cache in seconds (default: 7776000 = 90 days) 95 + 96 + QUEUE CONFIGURATION: 97 + QUEUE_ADAPTER Queue adapter: 'mpsc', 'redis', 'noop' (default: mpsc) 30 98 QUEUE_REDIS_URL Redis URL for queue adapter (uses REDIS_URL if not set) 31 99 QUEUE_REDIS_PREFIX Redis key prefix for queues (default: queue:handleresolver:) 32 - QUEUE_WORKER_ID Worker ID for Redis queue (random UUID if not set) 100 + QUEUE_REDIS_TIMEOUT Queue blocking timeout in seconds (default: 5) 101 + QUEUE_WORKER_ID Worker ID for Redis queue (auto-generated UUID if not set) 33 102 QUEUE_BUFFER_SIZE Buffer size for MPSC queue (default: 1000) 34 103 " 35 104 )] 105 + /// Command-line arguments and environment variables configuration 36 106 pub struct Args { 107 + /// HTTP server port to bind to 108 + /// 109 + /// Examples: "8080", "3000", "80" 110 + /// Constraints: Must be a valid port number (1-65535) 37 111 #[arg(long, env = "HTTP_PORT", default_value = "8080")] 38 112 pub http_port: String, 39 113 114 + /// PLC directory hostname for DID resolution 115 + /// 116 + /// Examples: "plc.directory", "test.plc.directory" 117 + /// Use "plc.directory" for production 40 118 #[arg(long, env = "PLC_HOSTNAME", default_value = "plc.directory")] 41 119 pub plc_hostname: String, 42 120 121 + /// External hostname for service endpoints (REQUIRED) 122 + /// 123 + /// Examples: 124 + /// - "quickdid.example.com" (standard) 125 + /// - "quickdid.example.com:8080" (with port) 126 + /// - "localhost:3007" (development) 43 127 #[arg(long, env = "HTTP_EXTERNAL")] 44 128 pub http_external: Option<String>, 45 129 130 + /// Private key for service identity in DID format (REQUIRED) 131 + /// 132 + /// Examples: 133 + /// - "did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK" 134 + /// - "did:plc:xyz123abc456" 135 + /// 136 + /// SECURITY: Keep this key secure and never commit to version control 46 137 #[arg(long, env = "SERVICE_KEY")] 47 138 pub service_key: Option<String>, 48 139 140 + /// HTTP User-Agent header for outgoing requests 141 + /// 142 + /// Example: `quickdid/1.0.0 (+https://quickdid.example.com)` 143 + /// Default: Auto-generated with current version 49 144 #[arg(long, env = "USER_AGENT")] 50 145 pub user_agent: Option<String>, 51 146 147 + /// Custom DNS nameservers (comma-separated IP addresses) 148 + /// 149 + /// Examples: 150 + /// - "8.8.8.8,8.8.4.4" (Google DNS) 151 + /// - "1.1.1.1,1.0.0.1" (Cloudflare DNS) 152 + /// - "192.168.1.1" (Local DNS) 52 153 #[arg(long, env = "DNS_NAMESERVERS")] 53 154 pub dns_nameservers: Option<String>, 54 155 156 + /// Additional CA certificates (comma-separated file paths) 157 + /// 158 + /// Examples: 159 + /// - "/etc/ssl/certs/custom-ca.pem" 160 + /// - "/certs/ca1.pem,/certs/ca2.pem" 161 + /// 162 + /// Use for custom or internal certificate authorities 55 163 #[arg(long, env = "CERTIFICATE_BUNDLES")] 56 164 pub certificate_bundles: Option<String>, 57 165 166 + /// Redis connection URL for caching 167 + /// 168 + /// Examples: 169 + /// - "redis://localhost:6379/0" (local, no auth) 170 + /// - "redis://user:pass@redis.example.com:6379/0" (with auth) 171 + /// - "rediss://secure-redis.example.com:6380/0" (TLS) 172 + /// 173 + /// Benefits: Persistent cache, distributed caching, better performance 58 174 #[arg(long, env = "REDIS_URL")] 59 175 pub redis_url: Option<String>, 60 176 177 + /// Queue adapter type for background processing 178 + /// 179 + /// Values: 180 + /// - "mpsc": In-memory multi-producer single-consumer queue 181 + /// - "redis": Redis-backed distributed queue 182 + /// - "noop": Disable queue processing (for testing) 183 + /// 184 + /// Default: "mpsc" for single-instance deployments 61 185 #[arg(long, env = "QUEUE_ADAPTER", default_value = "mpsc")] 62 186 pub queue_adapter: String, 63 187 188 + /// Redis URL specifically for queue operations 189 + /// 190 + /// Falls back to REDIS_URL if not specified 191 + /// Use when separating cache and queue Redis instances 64 192 #[arg(long, env = "QUEUE_REDIS_URL")] 65 193 pub queue_redis_url: Option<String>, 66 194 195 + /// Redis key prefix for queue operations 196 + /// 197 + /// Examples: 198 + /// - "queue:handleresolver:" (default) 199 + /// - "prod:queue:hr:" (environment-specific) 200 + /// - "quickdid:v1:queue:" (version-specific) 201 + /// 202 + /// Use to namespace queues when sharing Redis 67 203 #[arg( 68 204 long, 69 205 env = "QUEUE_REDIS_PREFIX", ··· 71 207 )] 72 208 pub queue_redis_prefix: String, 73 209 210 + /// Worker ID for Redis queue operations 211 + /// 212 + /// Examples: "worker-001", "prod-us-east-1", "quickdid-1" 213 + /// Default: Auto-generated UUID 214 + /// 215 + /// Use for identifying specific workers in logs 74 216 #[arg(long, env = "QUEUE_WORKER_ID")] 75 217 pub queue_worker_id: Option<String>, 76 218 219 + /// Buffer size for MPSC queue 220 + /// 221 + /// Range: 100-100000 (recommended) 222 + /// Default: 1000 223 + /// 224 + /// Increase for high-traffic deployments 77 225 #[arg(long, env = "QUEUE_BUFFER_SIZE", default_value = "1000")] 78 226 pub queue_buffer_size: usize, 227 + 228 + /// TTL for in-memory cache in seconds 229 + /// 230 + /// Range: 60-3600 (recommended) 231 + /// Default: 600 (10 minutes) 232 + /// 233 + /// Lower values = fresher data, more resolution requests 234 + /// Higher values = better performance, potentially stale data 235 + #[arg(long, env = "CACHE_TTL_MEMORY", default_value = "600")] 236 + pub cache_ttl_memory: u64, 237 + 238 + /// TTL for Redis cache in seconds 239 + /// 240 + /// Range: 3600-31536000 (1 hour to 1 year) 241 + /// Default: 7776000 (90 days) 242 + /// 243 + /// Recommendation: 86400 (1 day) for frequently changing data 244 + #[arg(long, env = "CACHE_TTL_REDIS", default_value = "7776000")] 245 + pub cache_ttl_redis: u64, 246 + 247 + /// Redis blocking timeout for queue operations in seconds 248 + /// 249 + /// Range: 1-60 (recommended) 250 + /// Default: 5 251 + /// 252 + /// Lower values = more responsive to shutdown 253 + /// Higher values = less Redis polling overhead 254 + #[arg(long, env = "QUEUE_REDIS_TIMEOUT", default_value = "5")] 255 + pub queue_redis_timeout: u64, 79 256 } 80 257 258 + /// Validated configuration for QuickDID service 259 + /// 260 + /// This struct contains all configuration after validation and processing. 261 + /// Use `Config::from_args()` to create from command-line arguments and environment variables. 262 + /// 263 + /// ## Example 264 + /// 265 + /// ```rust,no_run 266 + /// use quickdid::config::{Args, Config}; 267 + /// use clap::Parser; 268 + /// 269 + /// let args = Args::parse(); 270 + /// let config = Config::from_args(args)?; 271 + /// config.validate()?; 272 + /// 273 + /// println!("Service running at: {}", config.http_external); 274 + /// println!("Service DID: {}", config.service_did); 275 + /// ``` 81 276 #[derive(Clone)] 82 277 pub struct Config { 278 + /// HTTP server port (e.g., "8080", "3000") 83 279 pub http_port: String, 280 + 281 + /// PLC directory hostname (e.g., "plc.directory") 84 282 pub plc_hostname: String, 283 + 284 + /// External hostname for service endpoints (e.g., "quickdid.example.com") 85 285 pub http_external: String, 286 + 287 + /// Private key for service identity (e.g., "did:key:z42tm...") 86 288 pub service_key: String, 289 + 290 + /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)") 87 291 pub user_agent: String, 292 + 293 + /// Derived service DID (e.g., "did:web:quickdid.example.com") 294 + /// Automatically generated from http_external with proper encoding 88 295 pub service_did: String, 296 + 297 + /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4") 89 298 pub dns_nameservers: Option<String>, 299 + 300 + /// Additional CA certificate bundles, comma-separated paths 90 301 pub certificate_bundles: Option<String>, 302 + 303 + /// Redis URL for caching (e.g., "redis://localhost:6379/0") 91 304 pub redis_url: Option<String>, 305 + 306 + /// Queue adapter type: "mpsc", "redis", or "noop" 92 307 pub queue_adapter: String, 308 + 309 + /// Redis URL for queue operations (falls back to redis_url) 93 310 pub queue_redis_url: Option<String>, 311 + 312 + /// Redis key prefix for queues (e.g., "queue:handleresolver:") 94 313 pub queue_redis_prefix: String, 314 + 315 + /// Worker ID for queue operations (auto-generated if not set) 95 316 pub queue_worker_id: Option<String>, 317 + 318 + /// Buffer size for MPSC queue (e.g., 1000) 96 319 pub queue_buffer_size: usize, 320 + 321 + /// TTL for in-memory cache in seconds (e.g., 600 = 10 minutes) 322 + pub cache_ttl_memory: u64, 323 + 324 + /// TTL for Redis cache in seconds (e.g., 7776000 = 90 days) 325 + pub cache_ttl_redis: u64, 326 + 327 + /// Redis blocking timeout for queue operations in seconds (e.g., 5) 328 + pub queue_redis_timeout: u64, 97 329 } 98 330 99 331 impl Config { 100 - pub fn from_args(args: Args) -> Result<Self> { 332 + /// Create a validated Config from command-line arguments and environment variables 333 + /// 334 + /// This method: 335 + /// 1. Processes command-line arguments with environment variable fallbacks 336 + /// 2. Validates required fields (HTTP_EXTERNAL and SERVICE_KEY) 337 + /// 3. Generates derived values (service_did from http_external) 338 + /// 4. Applies defaults where appropriate 339 + /// 340 + /// ## Priority Order 341 + /// 342 + /// 1. Command-line arguments (highest priority) 343 + /// 2. Environment variables 344 + /// 3. Default values (lowest priority) 345 + /// 346 + /// ## Example 347 + /// 348 + /// ```rust,no_run 349 + /// use quickdid::config::{Args, Config}; 350 + /// use clap::Parser; 351 + /// 352 + /// // Parse from environment and command-line 353 + /// let args = Args::parse(); 354 + /// let config = Config::from_args(args)?; 355 + /// 356 + /// // The service DID is automatically generated from HTTP_EXTERNAL 357 + /// assert!(config.service_did.starts_with("did:web:")); 358 + /// ``` 359 + /// 360 + /// ## Errors 361 + /// 362 + /// Returns `ConfigError::MissingRequired` if: 363 + /// - HTTP_EXTERNAL is not provided 364 + /// - SERVICE_KEY is not provided 365 + pub fn from_args(args: Args) -> Result<Self, ConfigError> { 101 366 let http_external = args 102 367 .http_external 103 368 .or_else(|| { ··· 108 373 Some(env_val) 109 374 } 110 375 }) 111 - .ok_or_else(|| anyhow::anyhow!("HTTP_EXTERNAL is required"))?; 376 + .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?; 112 377 113 378 let service_key = args 114 379 .service_key ··· 120 385 Some(env_val) 121 386 } 122 387 }) 123 - .ok_or_else(|| anyhow::anyhow!("SERVICE_KEY is required"))?; 388 + .ok_or_else(|| ConfigError::MissingRequired("SERVICE_KEY".to_string()))?; 124 389 125 - let default_user_agent = 126 - format!("quickdid/0.1.0 (+https://github.com/smokesignal.events/quickdid)"); 390 + let default_user_agent = format!( 391 + "quickdid/{} (+https://github.com/smokesignal.events/quickdid)", 392 + env!("CARGO_PKG_VERSION") 393 + ); 127 394 128 395 let user_agent = args 129 396 .user_agent ··· 194 461 } 195 462 }), 196 463 queue_buffer_size: args.queue_buffer_size, 464 + cache_ttl_memory: args.cache_ttl_memory, 465 + cache_ttl_redis: args.cache_ttl_redis, 466 + queue_redis_timeout: args.queue_redis_timeout, 197 467 }) 468 + } 469 + 470 + /// Validate the configuration for correctness and consistency 471 + /// 472 + /// Checks: 473 + /// - Cache TTL values are positive (> 0) 474 + /// - Queue timeout is positive (> 0) 475 + /// - Queue adapter is a valid value ('mpsc', 'redis', 'noop', 'none') 476 + /// 477 + /// ## Example 478 + /// 479 + /// ```rust,no_run 480 + /// let config = Config::from_args(args)?; 481 + /// config.validate()?; // Ensures all values are valid 482 + /// ``` 483 + /// 484 + /// ## Errors 485 + /// 486 + /// Returns `ConfigError::InvalidTtl` if TTL values are 0 or negative 487 + /// Returns `ConfigError::InvalidTimeout` if timeout values are 0 or negative 488 + /// Returns `ConfigError::InvalidValue` if queue adapter is invalid 489 + pub fn validate(&self) -> Result<(), ConfigError> { 490 + if self.cache_ttl_memory == 0 { 491 + return Err(ConfigError::InvalidTtl( 492 + "CACHE_TTL_MEMORY must be > 0".to_string(), 493 + )); 494 + } 495 + if self.cache_ttl_redis == 0 { 496 + return Err(ConfigError::InvalidTtl( 497 + "CACHE_TTL_REDIS must be > 0".to_string(), 498 + )); 499 + } 500 + if self.queue_redis_timeout == 0 { 501 + return Err(ConfigError::InvalidTimeout( 502 + "QUEUE_REDIS_TIMEOUT must be > 0".to_string(), 503 + )); 504 + } 505 + match self.queue_adapter.as_str() { 506 + "mpsc" | "redis" | "noop" | "none" => {} 507 + _ => { 508 + return Err(ConfigError::InvalidValue(format!( 509 + "Invalid QUEUE_ADAPTER '{}', must be 'mpsc', 'redis', or 'noop'", 510 + self.queue_adapter 511 + ))); 512 + } 513 + } 514 + Ok(()) 198 515 } 199 516 }
+61 -13
src/handle_resolution_result.rs
··· 6 6 7 7 use serde::{Deserialize, Serialize}; 8 8 use std::time::{SystemTime, UNIX_EPOCH}; 9 + use thiserror::Error; 10 + 11 + /// Errors that can occur during handle resolution result operations 12 + #[derive(Debug, Error)] 13 + pub enum HandleResolutionError { 14 + #[error("error-quickdid-resolution-1 System time error: {0}")] 15 + SystemTimeError(String), 16 + 17 + #[error("error-quickdid-serialization-1 Failed to serialize resolution result: {0}")] 18 + SerializationError(String), 19 + 20 + #[error("error-quickdid-serialization-2 Failed to deserialize resolution result: {0}")] 21 + DeserializationError(String), 22 + } 9 23 10 24 /// Represents the type of DID method from a resolution 11 25 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] ··· 34 48 35 49 impl HandleResolutionResult { 36 50 /// Create a new resolution result for a successfully resolved handle 37 - pub fn success(did: &str) -> Self { 51 + pub fn success(did: &str) -> Result<Self, HandleResolutionError> { 52 + let timestamp = SystemTime::now() 53 + .duration_since(UNIX_EPOCH) 54 + .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))? 55 + .as_secs(); 56 + 57 + let (method_type, payload) = Self::parse_did(did); 58 + 59 + Ok(Self { 60 + timestamp, 61 + method_type, 62 + payload, 63 + }) 64 + } 65 + 66 + /// Create a new resolution result for a successfully resolved handle (unsafe version for compatibility) 67 + /// This version panics if system time is invalid and should only be used in tests 68 + pub fn success_unchecked(did: &str) -> Self { 38 69 let timestamp = SystemTime::now() 39 70 .duration_since(UNIX_EPOCH) 40 71 .expect("Time went backwards") ··· 50 81 } 51 82 52 83 /// Create a new resolution result for a failed resolution 53 - pub fn not_resolved() -> Self { 84 + pub fn not_resolved() -> Result<Self, HandleResolutionError> { 85 + let timestamp = SystemTime::now() 86 + .duration_since(UNIX_EPOCH) 87 + .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))? 88 + .as_secs(); 89 + 90 + Ok(Self { 91 + timestamp, 92 + method_type: DidMethodType::NotResolved, 93 + payload: String::new(), 94 + }) 95 + } 96 + 97 + /// Create a new resolution result for a failed resolution (unsafe version for compatibility) 98 + /// This version panics if system time is invalid and should only be used in tests 99 + pub fn not_resolved_unchecked() -> Self { 54 100 let timestamp = SystemTime::now() 55 101 .duration_since(UNIX_EPOCH) 56 102 .expect("Time went backwards") ··· 108 154 } 109 155 110 156 /// Serialize the result to bytes using bincode 111 - pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> { 157 + pub fn to_bytes(&self) -> Result<Vec<u8>, HandleResolutionError> { 112 158 bincode::serde::encode_to_vec(self, bincode::config::standard()) 159 + .map_err(|e| HandleResolutionError::SerializationError(e.to_string())) 113 160 } 114 161 115 162 /// Deserialize from bytes using bincode 116 - pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> { 163 + pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandleResolutionError> { 117 164 bincode::serde::decode_from_slice(bytes, bincode::config::standard()) 118 165 .map(|(result, _)| result) 166 + .map_err(|e| HandleResolutionError::DeserializationError(e.to_string())) 119 167 } 120 168 } 121 169 ··· 126 174 #[test] 127 175 fn test_parse_did_web() { 128 176 let did = "did:web:example.com"; 129 - let result = HandleResolutionResult::success(did); 177 + let result = HandleResolutionResult::success_unchecked(did); 130 178 assert_eq!(result.method_type, DidMethodType::Web); 131 179 assert_eq!(result.payload, "example.com"); 132 180 assert_eq!(result.to_did(), Some(did.to_string())); ··· 135 183 #[test] 136 184 fn test_parse_did_plc() { 137 185 let did = "did:plc:abcdef123456"; 138 - let result = HandleResolutionResult::success(did); 186 + let result = HandleResolutionResult::success_unchecked(did); 139 187 assert_eq!(result.method_type, DidMethodType::Plc); 140 188 assert_eq!(result.payload, "abcdef123456"); 141 189 assert_eq!(result.to_did(), Some(did.to_string())); ··· 144 192 #[test] 145 193 fn test_parse_did_other() { 146 194 let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; 147 - let result = HandleResolutionResult::success(did); 195 + let result = HandleResolutionResult::success_unchecked(did); 148 196 assert_eq!(result.method_type, DidMethodType::Other); 149 197 assert_eq!(result.payload, did); 150 198 assert_eq!(result.to_did(), Some(did.to_string())); ··· 152 200 153 201 #[test] 154 202 fn test_not_resolved() { 155 - let result = HandleResolutionResult::not_resolved(); 203 + let result = HandleResolutionResult::not_resolved_unchecked(); 156 204 assert_eq!(result.method_type, DidMethodType::NotResolved); 157 205 assert_eq!(result.payload, ""); 158 206 assert_eq!(result.to_did(), None); ··· 209 257 ); 210 258 211 259 // Verify the size for not resolved (should be minimal) 212 - let not_resolved = HandleResolutionResult::not_resolved(); 260 + let not_resolved = HandleResolutionResult::not_resolved_unchecked(); 213 261 let bytes = not_resolved.to_bytes().unwrap(); 214 262 assert!( 215 263 bytes.len() < 50, ··· 221 269 #[test] 222 270 fn test_did_web_with_port() { 223 271 let did = "did:web:localhost:3000"; 224 - let result = HandleResolutionResult::success(did); 272 + let result = HandleResolutionResult::success_unchecked(did); 225 273 assert_eq!(result.method_type, DidMethodType::Web); 226 274 assert_eq!(result.payload, "localhost:3000"); 227 275 assert_eq!(result.to_did(), Some(did.to_string())); ··· 230 278 #[test] 231 279 fn test_did_web_with_path() { 232 280 let did = "did:web:example.com:path:to:did"; 233 - let result = HandleResolutionResult::success(did); 281 + let result = HandleResolutionResult::success_unchecked(did); 234 282 assert_eq!(result.method_type, DidMethodType::Web); 235 283 assert_eq!(result.payload, "example.com:path:to:did"); 236 284 assert_eq!(result.to_did(), Some(did.to_string())); ··· 239 287 #[test] 240 288 fn test_invalid_did_format() { 241 289 let did = "not-a-did"; 242 - let result = HandleResolutionResult::success(did); 290 + let result = HandleResolutionResult::success_unchecked(did); 243 291 assert_eq!(result.method_type, DidMethodType::Other); 244 292 assert_eq!(result.payload, did); 245 293 assert_eq!(result.to_did(), Some(did.to_string())); ··· 258 306 ]; 259 307 260 308 for (did, expected_type, expected_payload) in test_cases { 261 - let result = HandleResolutionResult::success(did); 309 + let result = HandleResolutionResult::success_unchecked(did); 262 310 assert_eq!(result.method_type, expected_type); 263 311 assert_eq!(result.payload, expected_payload); 264 312
+56 -18
src/handle_resolver.rs
··· 1 1 use crate::handle_resolution_result::HandleResolutionResult; 2 2 use async_trait::async_trait; 3 - use atproto_identity::resolve::{resolve_subject, DnsResolver}; 3 + use atproto_identity::resolve::{DnsResolver, resolve_subject}; 4 4 use chrono::Utc; 5 - use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool}; 5 + use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands}; 6 6 use metrohash::MetroHash64; 7 7 use reqwest::Client; 8 8 use std::collections::HashMap; ··· 16 16 pub enum HandleResolverError { 17 17 #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")] 18 18 ResolutionFailed(String), 19 - 19 + 20 20 #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")] 21 21 HandleNotFoundCached(String), 22 - 22 + 23 23 #[error("error-quickdid-resolve-3 Handle not found (cached)")] 24 24 HandleNotFound, 25 - 25 + 26 26 #[error("error-quickdid-resolve-4 Mock resolution failure")] 27 27 MockResolutionFailure, 28 28 } ··· 148 148 } 149 149 150 150 /// Redis-backed caching handle resolver that caches resolution results in Redis 151 - /// with a 90-day expiration time. 151 + /// with a configurable expiration time. 152 152 pub struct RedisHandleResolver { 153 153 /// Base handle resolver to perform actual resolution 154 154 inner: Arc<dyn HandleResolver>, ··· 156 156 pool: RedisPool, 157 157 /// Redis key prefix for handle resolution cache 158 158 key_prefix: String, 159 + /// TTL for cache entries in seconds 160 + ttl_seconds: u64, 159 161 } 160 162 161 163 impl RedisHandleResolver { 162 - /// Create a new Redis-backed handle resolver 164 + /// Create a new Redis-backed handle resolver with default 90-day TTL 163 165 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self { 164 - Self::with_prefix(inner, pool, "handle:".to_string()) 166 + Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default 167 + } 168 + 169 + /// Create a new Redis-backed handle resolver with custom TTL 170 + pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self { 171 + Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds) 165 172 } 166 173 167 174 /// Create a new Redis-backed handle resolver with a custom key prefix ··· 170 177 pool: RedisPool, 171 178 key_prefix: String, 172 179 ) -> Self { 180 + Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60) 181 + } 182 + 183 + /// Create a new Redis-backed handle resolver with full configuration 184 + pub fn with_full_config( 185 + inner: Arc<dyn HandleResolver>, 186 + pool: RedisPool, 187 + key_prefix: String, 188 + ttl_seconds: u64, 189 + ) -> Self { 173 190 Self { 174 191 inner, 175 192 pool, 176 193 key_prefix, 194 + ttl_seconds, 177 195 } 178 196 } 179 197 ··· 184 202 format!("{}{}", self.key_prefix, h.finish()) 185 203 } 186 204 187 - /// Get the TTL in seconds (90 days) 188 - fn ttl_seconds() -> u64 { 189 - 90 * 24 * 60 * 60 // 90 days in seconds 205 + /// Get the TTL in seconds 206 + fn ttl_seconds(&self) -> u64 { 207 + self.ttl_seconds 190 208 } 191 209 } 192 210 ··· 243 261 handle, 244 262 did 245 263 ); 246 - HandleResolutionResult::success(did) 264 + match HandleResolutionResult::success(did) { 265 + Ok(res) => res, 266 + Err(e) => { 267 + tracing::warn!("Failed to create resolution result: {}", e); 268 + return result; 269 + } 270 + } 247 271 } 248 272 Err(e) => { 249 273 tracing::debug!("Caching failed resolution for handle {}: {}", handle, e); 250 - HandleResolutionResult::not_resolved() 274 + match HandleResolutionResult::not_resolved() { 275 + Ok(res) => res, 276 + Err(err) => { 277 + tracing::warn!("Failed to create not_resolved result: {}", err); 278 + return result; 279 + } 280 + } 251 281 } 252 282 }; 253 283 ··· 256 286 Ok(bytes) => { 257 287 // Set with expiration (ignore errors to not fail the resolution) 258 288 if let Err(e) = conn 259 - .set_ex::<_, _, ()>(&key, bytes, Self::ttl_seconds()) 289 + .set_ex::<_, _, ()>(&key, bytes, self.ttl_seconds()) 260 290 .await 261 291 { 262 292 tracing::warn!("Failed to cache handle resolution in Redis: {}", e); ··· 336 366 337 367 // Create Redis-backed resolver with a unique key prefix for testing 338 368 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4()); 339 - let redis_resolver = 340 - RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone()); 369 + let redis_resolver = RedisHandleResolver::with_full_config( 370 + mock_resolver, 371 + pool.clone(), 372 + test_prefix.clone(), 373 + 3600, 374 + ); 341 375 342 376 let test_handle = "alice.bsky.social"; 343 377 ··· 385 419 386 420 // Create Redis-backed resolver with a unique key prefix for testing 387 421 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4()); 388 - let redis_resolver = 389 - RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone()); 422 + let redis_resolver = RedisHandleResolver::with_full_config( 423 + mock_resolver, 424 + pool.clone(), 425 + test_prefix.clone(), 426 + 3600, 427 + ); 390 428 391 429 let test_handle = "error.bsky.social"; 392 430
+4 -1
src/handle_resolver_task.rs
··· 276 276 277 277 #[async_trait] 278 278 impl HandleResolver for MockHandleResolver { 279 - async fn resolve(&self, handle: &str) -> Result<String, crate::handle_resolver::HandleResolverError> { 279 + async fn resolve( 280 + &self, 281 + handle: &str, 282 + ) -> Result<String, crate::handle_resolver::HandleResolverError> { 280 283 if self.should_fail { 281 284 Err(crate::handle_resolver::HandleResolverError::MockResolutionFailure) 282 285 } else {
+1 -1
src/http/handle_xrpc_resolve_handle.rs
··· 5 5 queue_adapter::{HandleResolutionWork, QueueAdapter}, 6 6 }; 7 7 8 - use atproto_identity::resolve::{parse_input, InputType}; 8 + use atproto_identity::resolve::{InputType, parse_input}; 9 9 use axum::{ 10 10 extract::{Query, State}, 11 11 http::StatusCode,
+1 -1
src/http/server.rs
··· 1 1 use crate::handle_resolver::HandleResolver; 2 2 use crate::queue_adapter::{HandleResolutionWork, QueueAdapter}; 3 3 use axum::{ 4 + Router, 4 5 extract::State, 5 6 response::{Html, IntoResponse, Json, Response}, 6 7 routing::get, 7 - Router, 8 8 }; 9 9 use http::StatusCode; 10 10 use serde_json::json;
+2 -2
src/queue_adapter.rs
··· 4 4 //! that can be used with any work type for handle resolution and other tasks. 5 5 6 6 use async_trait::async_trait; 7 - use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool}; 7 + use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands}; 8 8 use serde::{Deserialize, Serialize}; 9 9 use std::sync::Arc; 10 10 use thiserror::Error; 11 - use tokio::sync::{mpsc, Mutex}; 11 + use tokio::sync::{Mutex, mpsc}; 12 12 use tracing::{debug, error, warn}; 13 13 14 14 /// Queue operation errors