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.

feature: www directory support

+510 -182
+37
.dockerignore
···
··· 1 + # Git 2 + .git 3 + .gitignore 4 + 5 + # Documentation 6 + *.md 7 + docs/ 8 + LICENSE 9 + 10 + # Development files 11 + .vscode/ 12 + .env 13 + .env.local 14 + *.log 15 + 16 + # Build artifacts 17 + target/ 18 + Dockerfile 19 + .dockerignore 20 + 21 + # Test files 22 + tests/ 23 + benches/ 24 + 25 + # Scripts (except the ones we need) 26 + *.sh 27 + 28 + # SQLite databases 29 + *.db 30 + *.db-* 31 + 32 + # OS files 33 + .DS_Store 34 + Thumbs.db 35 + 36 + # Keep the www directory for static files 37 + !www/
+12 -8
.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 ··· 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 ··· 98 QUEUE_BUFFER_SIZE=1000 99 100 # ============================================================================ 101 # LOGGING 102 # ============================================================================ 103 ··· 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
··· 1 # QuickDID Environment Configuration Template 2 # Copy this file to .env and customize for your deployment 3 4 # ============================================================================ 5 # REQUIRED CONFIGURATION ··· 11 # - quickdid.example.com:8080 12 # - localhost:3007 13 HTTP_EXTERNAL=quickdid.example.com 14 15 # ============================================================================ 16 # NETWORK CONFIGURATION ··· 91 QUEUE_BUFFER_SIZE=1000 92 93 # ============================================================================ 94 + # STATIC FILES CONFIGURATION 95 + # ============================================================================ 96 + 97 + # Directory for serving static files (default: www) 98 + # This should contain: 99 + # - index.html (landing page) 100 + # - .well-known/atproto-did (service DID) 101 + # - .well-known/did.json (DID document) 102 + # Docker default: /app/www 103 + STATIC_FILES_DIR=www 104 + 105 + # ============================================================================ 106 # LOGGING 107 # ============================================================================ 108 ··· 117 # ============================================================================ 118 119 # HTTP_EXTERNAL=localhost:3007 120 # RUST_LOG=debug 121 # CACHE_TTL_MEMORY=60 122 # CACHE_TTL_REDIS=300
+4 -3
CLAUDE.md
··· 21 cargo build 22 23 # Run in debug mode (requires environment variables) 24 - HTTP_EXTERNAL=localhost:3007 SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK cargo run 25 26 # Run tests 27 cargo test ··· 71 4. **HTTP Server** (`src/http/`) 72 - XRPC endpoints for AT Protocol compatibility 73 - Health check endpoint 74 - - DID document serving via .well-known 75 - CORS headers support for cross-origin requests 76 - Cache-Control headers with configurable max-age and stale directives 77 - ETag support with configurable seed for cache invalidation ··· 107 108 ### Required 109 - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 110 - - `SERVICE_KEY`: Private key for service identity (DID format) 111 112 ### Optional - Core Configuration 113 - `HTTP_PORT`: Server port (default: 8080) 114 - `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory) 115 - `RUST_LOG`: Logging level (e.g., debug, info) 116 117 ### Optional - Caching 118 - `REDIS_URL`: Redis connection URL for caching
··· 21 cargo build 22 23 # Run in debug mode (requires environment variables) 24 + HTTP_EXTERNAL=localhost:3007 cargo run 25 26 # Run tests 27 cargo test ··· 71 4. **HTTP Server** (`src/http/`) 72 - XRPC endpoints for AT Protocol compatibility 73 - Health check endpoint 74 + - Static file serving from configurable directory (default: www) 75 + - Serves .well-known files as static content 76 - CORS headers support for cross-origin requests 77 - Cache-Control headers with configurable max-age and stale directives 78 - ETag support with configurable seed for cache invalidation ··· 108 109 ### Required 110 - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 111 112 ### Optional - Core Configuration 113 - `HTTP_PORT`: Server port (default: 8080) 114 - `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory) 115 - `RUST_LOG`: Logging level (e.g., debug, info) 116 + - `STATIC_FILES_DIR`: Directory for serving static files (default: www) 117 118 ### Optional - Caching 119 - `REDIS_URL`: Redis connection URL for caching
+33
Cargo.lock
··· 1053 ] 1054 1055 [[package]] 1056 name = "httparse" 1057 version = "1.10.1" 1058 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1486 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1487 1488 [[package]] 1489 name = "miniz_oxide" 1490 version = "0.8.9" 1491 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1872 "thiserror 2.0.16", 1873 "tokio", 1874 "tokio-util", 1875 "tracing", 1876 "tracing-subscriber", 1877 ] ··· 2973 dependencies = [ 2974 "bitflags", 2975 "bytes", 2976 "futures-util", 2977 "http", 2978 "http-body", 2979 "iri-string", 2980 "pin-project-lite", 2981 "tower", 2982 "tower-layer", 2983 "tower-service", 2984 ] 2985 2986 [[package]] ··· 3068 version = "1.18.0" 3069 source = "registry+https://github.com/rust-lang/crates.io-index" 3070 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 3071 3072 [[package]] 3073 name = "unicode-bidi"
··· 1053 ] 1054 1055 [[package]] 1056 + name = "http-range-header" 1057 + version = "0.4.2" 1058 + source = "registry+https://github.com/rust-lang/crates.io-index" 1059 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 1060 + 1061 + [[package]] 1062 name = "httparse" 1063 version = "1.10.1" 1064 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1492 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1493 1494 [[package]] 1495 + name = "mime_guess" 1496 + version = "2.0.5" 1497 + source = "registry+https://github.com/rust-lang/crates.io-index" 1498 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1499 + dependencies = [ 1500 + "mime", 1501 + "unicase", 1502 + ] 1503 + 1504 + [[package]] 1505 name = "miniz_oxide" 1506 version = "0.8.9" 1507 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1888 "thiserror 2.0.16", 1889 "tokio", 1890 "tokio-util", 1891 + "tower-http", 1892 "tracing", 1893 "tracing-subscriber", 1894 ] ··· 2990 dependencies = [ 2991 "bitflags", 2992 "bytes", 2993 + "futures-core", 2994 "futures-util", 2995 "http", 2996 "http-body", 2997 + "http-body-util", 2998 + "http-range-header", 2999 + "httpdate", 3000 "iri-string", 3001 + "mime", 3002 + "mime_guess", 3003 + "percent-encoding", 3004 "pin-project-lite", 3005 + "tokio", 3006 + "tokio-util", 3007 "tower", 3008 "tower-layer", 3009 "tower-service", 3010 + "tracing", 3011 ] 3012 3013 [[package]] ··· 3095 version = "1.18.0" 3096 source = "registry+https://github.com/rust-lang/crates.io-index" 3097 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 3098 + 3099 + [[package]] 3100 + name = "unicase" 3101 + version = "2.8.1" 3102 + source = "registry+https://github.com/rust-lang/crates.io-index" 3103 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 3104 3105 [[package]] 3106 name = "unicode-bidi"
+1
Cargo.toml
··· 30 thiserror = "2.0" 31 tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "signal", "sync", "time", "net", "fs"] } 32 tokio-util = { version = "0.7", features = ["rt"] } 33 tracing = "0.1" 34 tracing-subscriber = { version = "0.3", features = ["env-filter"] } 35
··· 30 thiserror = "2.0" 31 tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "signal", "sync", "time", "net", "fs"] } 32 tokio-util = { version = "0.7", features = ["rt"] } 33 + tower-http = { version = "0.6", features = ["fs"] } 34 tracing = "0.1" 35 tracing-subscriber = { version = "0.3", features = ["env-filter"] } 36
+4
Dockerfile
··· 24 WORKDIR /app 25 COPY --from=builder /app/target/release/quickdid /app/quickdid 26 27 ENV HTTP_PORT=8080 28 ENV RUST_LOG=info 29 ENV RUST_BACKTRACE=full 30
··· 24 WORKDIR /app 25 COPY --from=builder /app/target/release/quickdid /app/quickdid 26 27 + # Copy static files for serving 28 + COPY www /app/www 29 + 30 ENV HTTP_PORT=8080 31 + ENV STATIC_FILES_DIR=/app/www 32 ENV RUST_LOG=info 33 ENV RUST_BACKTRACE=full 34
+17 -7
README.md
··· 83 84 ## Minimum Configuration 85 86 - QuickDID requires the following environment variables to run. Configuration is validated at startup, and the service will exit with specific error codes if validation fails. 87 88 ### Required 89 90 - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 91 - - `SERVICE_KEY`: Private key for service identity in DID format (e.g., `did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK`) 92 93 ### Example Minimal Setup 94 95 ```bash 96 - HTTP_EXTERNAL=localhost:3007 \ 97 - SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 98 - cargo run 99 ``` 100 101 This will start QuickDID with: ··· 155 - `PROACTIVE_REFRESH_ENABLED`: Enable proactive cache refreshing (default: false) 156 - `PROACTIVE_REFRESH_THRESHOLD`: Refresh when TTL remaining is below this threshold (0.0-1.0, default: 0.8) 157 158 #### Logging 159 - `RUST_LOG`: Logging level (e.g., debug, info, warn, error) 160 ··· 163 #### Redis-based with Metrics (Multi-instance/HA) 164 ```bash 165 HTTP_EXTERNAL=quickdid.example.com \ 166 - SERVICE_KEY=did:key:yourkeyhere \ 167 HTTP_PORT=3000 \ 168 REDIS_URL=redis://localhost:6379 \ 169 CACHE_TTL_REDIS=86400 \ ··· 183 #### SQLite-based (Single-instance) 184 ```bash 185 HTTP_EXTERNAL=quickdid.example.com \ 186 - SERVICE_KEY=did:key:yourkeyhere \ 187 HTTP_PORT=3000 \ 188 SQLITE_URL=sqlite:./quickdid.db \ 189 CACHE_TTL_SQLITE=86400 \
··· 83 84 ## Minimum Configuration 85 86 + QuickDID requires minimal configuration to run. Configuration is validated at startup, and the service will exit with specific error codes if validation fails. 87 88 ### Required 89 90 - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 91 92 ### Example Minimal Setup 93 94 ```bash 95 + HTTP_EXTERNAL=localhost:3007 cargo run 96 + ``` 97 + 98 + ### Static Files 99 + 100 + QuickDID serves static files from the `www` directory by default. This includes: 101 + - Landing page (`index.html`) 102 + - AT Protocol well-known files (`.well-known/atproto-did` and `.well-known/did.json`) 103 + 104 + Generate the `.well-known` files for your deployment: 105 + 106 + ```bash 107 + HTTP_EXTERNAL=your-domain.com ./generate-wellknown.sh 108 ``` 109 110 This will start QuickDID with: ··· 164 - `PROACTIVE_REFRESH_ENABLED`: Enable proactive cache refreshing (default: false) 165 - `PROACTIVE_REFRESH_THRESHOLD`: Refresh when TTL remaining is below this threshold (0.0-1.0, default: 0.8) 166 167 + #### Static Files 168 + - `STATIC_FILES_DIR`: Directory for serving static files (default: www) 169 + 170 #### Logging 171 - `RUST_LOG`: Logging level (e.g., debug, info, warn, error) 172 ··· 175 #### Redis-based with Metrics (Multi-instance/HA) 176 ```bash 177 HTTP_EXTERNAL=quickdid.example.com \ 178 HTTP_PORT=3000 \ 179 REDIS_URL=redis://localhost:6379 \ 180 CACHE_TTL_REDIS=86400 \ ··· 194 #### SQLite-based (Single-instance) 195 ```bash 196 HTTP_EXTERNAL=quickdid.example.com \ 197 HTTP_PORT=3000 \ 198 SQLITE_URL=sqlite:./quickdid.db \ 199 CACHE_TTL_SQLITE=86400 \
+41
docker-compose.yml
···
··· 1 + version: '3.8' 2 + 3 + services: 4 + quickdid: 5 + image: quickdid:latest 6 + build: . 7 + ports: 8 + - "3007:8080" 9 + environment: 10 + - HTTP_EXTERNAL=localhost:3007 11 + - HTTP_PORT=8080 12 + - RUST_LOG=info 13 + # Optional: Override the static files directory 14 + # - STATIC_FILES_DIR=/app/custom-www 15 + volumes: 16 + # Optional: Mount custom static files from host 17 + # - ./custom-www:/app/custom-www:ro 18 + # Optional: Mount custom .well-known files 19 + # - ./www/.well-known:/app/www/.well-known:ro 20 + # Optional: Use SQLite for caching 21 + # - ./data:/app/data 22 + # environment: 23 + # SQLite cache configuration 24 + # - SQLITE_URL=sqlite:/app/data/quickdid.db 25 + # - CACHE_TTL_SQLITE=86400 26 + 27 + # Redis cache configuration (if using external Redis) 28 + # - REDIS_URL=redis://redis:6379 29 + # - CACHE_TTL_REDIS=86400 30 + # - QUEUE_ADAPTER=redis 31 + 32 + # Optional: Redis service for caching 33 + # redis: 34 + # image: redis:7-alpine 35 + # ports: 36 + # - "6379:6379" 37 + # volumes: 38 + # - redis-data:/data 39 + 40 + volumes: 41 + redis-data:
+59 -39
docs/configuration-reference.md
··· 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 ··· 785 - TTL=3600s (1 hour), threshold=0.8: Refresh after 48 minutes 786 - TTL=86400s (1 day), threshold=0.8: Refresh after 19.2 hours 787 788 ## HTTP Caching Configuration 789 790 ### `CACHE_MAX_AGE` ··· 958 ```bash 959 # .env.development 960 HTTP_EXTERNAL=localhost:3007 961 - SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 962 RUST_LOG=debug 963 ``` 964 ··· 968 # .env.production.redis 969 # Required 970 HTTP_EXTERNAL=quickdid.example.com 971 - SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 972 973 # Network 974 HTTP_PORT=8080 ··· 1015 # .env.production.sqlite 1016 # Required 1017 HTTP_EXTERNAL=quickdid.example.com 1018 - SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 1019 1020 # Network 1021 HTTP_PORT=8080 ··· 1050 # .env.ha.redis 1051 # Required 1052 HTTP_EXTERNAL=quickdid.example.com 1053 - SERVICE_KEY=${SECRET_SERVICE_KEY} 1054 1055 # Network 1056 HTTP_PORT=8080 ··· 1097 # .env.hybrid 1098 # Required 1099 HTTP_EXTERNAL=quickdid.example.com 1100 - SERVICE_KEY=${SECRET_SERVICE_KEY} 1101 1102 # Network 1103 HTTP_PORT=8080 ··· 1128 image: quickdid:latest 1129 environment: 1130 HTTP_EXTERNAL: quickdid.example.com 1131 - SERVICE_KEY: ${SERVICE_KEY} 1132 HTTP_PORT: 8080 1133 REDIS_URL: redis://redis:6379/0 1134 CACHE_TTL_MEMORY: 600 ··· 1157 image: quickdid:latest 1158 environment: 1159 HTTP_EXTERNAL: quickdid.example.com 1160 - SERVICE_KEY: ${SERVICE_KEY} 1161 HTTP_PORT: 8080 1162 SQLITE_URL: sqlite:/data/quickdid.db 1163 CACHE_TTL_MEMORY: 600 ··· 1183 ### Required Fields 1184 1185 1. **HTTP_EXTERNAL**: Must be provided 1186 - 2. **SERVICE_KEY**: Must be provided 1187 1188 ### Value Constraints 1189 ··· 1228 1229 ```bash 1230 # Validate configuration 1231 - HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 1232 1233 # Test with specific values 1234 CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation 1235 1236 # Check parsed configuration (with debug logging) 1237 - RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 1238 ``` 1239 1240 ## Best Practices 1241 1242 ### Security 1243 1244 - 1. **Never commit SERVICE_KEY** to version control 1245 - 2. Use environment-specific key management (Vault, AWS Secrets, etc.) 1246 - 3. Rotate SERVICE_KEY regularly 1247 - 4. Use TLS for Redis connections in production (`rediss://`) 1248 5. Implement network segmentation for Redis access 1249 1250 ### Performance ··· 1280 ### Deployment 1281 1282 1. Use `.env` files for local development 1283 - 2. Use secrets management for production SERVICE_KEY 1284 3. Set resource limits in container orchestration 1285 4. Use health checks to monitor service availability 1286 5. Implement gradual rollouts with feature flags
··· 40 **Constraints**: 41 - Must be a valid hostname or hostname:port combination 42 - Port (if specified) must be between 1-65535 43 44 ## Network Configuration 45 ··· 761 - TTL=3600s (1 hour), threshold=0.8: Refresh after 48 minutes 762 - TTL=86400s (1 day), threshold=0.8: Refresh after 19.2 hours 763 764 + ## Static Files Configuration 765 + 766 + ### `STATIC_FILES_DIR` 767 + 768 + **Required**: No 769 + **Type**: String (directory path) 770 + **Default**: `www` 771 + 772 + Directory path for serving static files. This directory should contain the landing page and AT Protocol well-known files. 773 + 774 + **Directory Structure**: 775 + ``` 776 + www/ 777 + ├── index.html # Landing page 778 + ├── .well-known/ 779 + │ ├── atproto-did # Service DID identifier 780 + │ └── did.json # DID document 781 + └── (other static assets) 782 + ``` 783 + 784 + **Examples**: 785 + ```bash 786 + # Default (relative to working directory) 787 + STATIC_FILES_DIR=www 788 + 789 + # Absolute path 790 + STATIC_FILES_DIR=/var/www/quickdid 791 + 792 + # Docker container path 793 + STATIC_FILES_DIR=/app/www 794 + 795 + # Custom directory 796 + STATIC_FILES_DIR=./public 797 + ``` 798 + 799 + **Docker Volume Mounting**: 800 + ```yaml 801 + volumes: 802 + # Mount entire custom directory 803 + - ./custom-www:/app/www:ro 804 + 805 + # Mount specific files 806 + - ./custom-index.html:/app/www/index.html:ro 807 + - ./well-known:/app/www/.well-known:ro 808 + ``` 809 + 810 + **Generating Well-Known Files**: 811 + ```bash 812 + # Generate .well-known files for your domain 813 + HTTP_EXTERNAL=your-domain.com ./generate-wellknown.sh 814 + ``` 815 + 816 ## HTTP Caching Configuration 817 818 ### `CACHE_MAX_AGE` ··· 986 ```bash 987 # .env.development 988 HTTP_EXTERNAL=localhost:3007 989 RUST_LOG=debug 990 ``` 991 ··· 995 # .env.production.redis 996 # Required 997 HTTP_EXTERNAL=quickdid.example.com 998 999 # Network 1000 HTTP_PORT=8080 ··· 1041 # .env.production.sqlite 1042 # Required 1043 HTTP_EXTERNAL=quickdid.example.com 1044 1045 # Network 1046 HTTP_PORT=8080 ··· 1075 # .env.ha.redis 1076 # Required 1077 HTTP_EXTERNAL=quickdid.example.com 1078 1079 # Network 1080 HTTP_PORT=8080 ··· 1121 # .env.hybrid 1122 # Required 1123 HTTP_EXTERNAL=quickdid.example.com 1124 1125 # Network 1126 HTTP_PORT=8080 ··· 1151 image: quickdid:latest 1152 environment: 1153 HTTP_EXTERNAL: quickdid.example.com 1154 HTTP_PORT: 8080 1155 REDIS_URL: redis://redis:6379/0 1156 CACHE_TTL_MEMORY: 600 ··· 1179 image: quickdid:latest 1180 environment: 1181 HTTP_EXTERNAL: quickdid.example.com 1182 HTTP_PORT: 8080 1183 SQLITE_URL: sqlite:/data/quickdid.db 1184 CACHE_TTL_MEMORY: 600 ··· 1204 ### Required Fields 1205 1206 1. **HTTP_EXTERNAL**: Must be provided 1207 + 2. **HTTP_EXTERNAL**: Must be provided 1208 1209 ### Value Constraints 1210 ··· 1249 1250 ```bash 1251 # Validate configuration 1252 + HTTP_EXTERNAL=test quickdid --help 1253 1254 # Test with specific values 1255 CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation 1256 1257 # Check parsed configuration (with debug logging) 1258 + RUST_LOG=debug HTTP_EXTERNAL=test quickdid 1259 ``` 1260 1261 ## Best Practices 1262 1263 ### Security 1264 1265 + 1. Use environment-specific configuration management 1266 + 2. Use TLS for Redis connections in production (`rediss://`) 1267 + 3. Never commit sensitive configuration to version control 1268 5. Implement network segmentation for Redis access 1269 1270 ### Performance ··· 1300 ### Deployment 1301 1302 1. Use `.env` files for local development 1303 + 2. Use secrets management for production configurations 1304 3. Set resource limits in container orchestration 1305 4. Use health checks to monitor service availability 1306 5. Implement gradual rollouts with feature flags
+18 -14
docs/production-deployment.md
··· 42 # - localhost:3007 (for testing only) 43 HTTP_EXTERNAL=quickdid.example.com 44 45 - # Private key for service identity (DID format) 46 - # Generate a new key for production using atproto-identity tools 47 - # SECURITY: Keep this key secure and never commit to version control 48 - # Example formats: 49 - # - did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 50 - # - did:plc:xyz123abc456 51 - SERVICE_KEY=did:key:YOUR_PRODUCTION_KEY_HERE 52 - 53 # ---------------------------------------------------------------------------- 54 # NETWORK CONFIGURATION 55 # ---------------------------------------------------------------------------- ··· 304 PROACTIVE_REFRESH_THRESHOLD=0.8 305 306 # ---------------------------------------------------------------------------- 307 # PERFORMANCE TUNING 308 # ---------------------------------------------------------------------------- 309 ··· 512 container_name: quickdid-sqlite 513 environment: 514 HTTP_EXTERNAL: quickdid.example.com 515 - SERVICE_KEY: ${SERVICE_KEY} 516 HTTP_PORT: 8080 517 SQLITE_URL: sqlite:/data/quickdid.db 518 CACHE_TTL_MEMORY: 600 ··· 713 714 ### 1. Service Key Protection 715 716 - - **Never commit** the `SERVICE_KEY` to version control 717 - Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager) 718 - Rotate keys regularly 719 - Use different keys for different environments ··· 758 docker logs quickdid 759 760 # Verify environment variables 761 - docker exec quickdid env | grep -E "HTTP_EXTERNAL|SERVICE_KEY" 762 763 # Test Redis connectivity 764 docker exec quickdid redis-cli -h redis ping ··· 943 ### Required Fields 944 945 - **HTTP_EXTERNAL**: Must be provided 946 - - **SERVICE_KEY**: Must be provided 947 948 ### Value Constraints 949 ··· 982 983 ```bash 984 # Validate configuration without starting service 985 - HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 986 987 # Test with specific values (will fail validation) 988 CACHE_TTL_MEMORY=0 quickdid --help 989 990 # Debug configuration parsing 991 - RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 992 ``` 993 994 ## Support and Resources
··· 42 # - localhost:3007 (for testing only) 43 HTTP_EXTERNAL=quickdid.example.com 44 45 # ---------------------------------------------------------------------------- 46 # NETWORK CONFIGURATION 47 # ---------------------------------------------------------------------------- ··· 296 PROACTIVE_REFRESH_THRESHOLD=0.8 297 298 # ---------------------------------------------------------------------------- 299 + # STATIC FILES CONFIGURATION 300 + # ---------------------------------------------------------------------------- 301 + 302 + # Directory path for serving static files (default: www) 303 + # This directory should contain: 304 + # - index.html (landing page) 305 + # - .well-known/atproto-did (service DID identifier) 306 + # - .well-known/did.json (DID document) 307 + # In Docker, this defaults to /app/www 308 + # You can mount custom files via Docker volumes 309 + STATIC_FILES_DIR=/app/www 310 + 311 + # ---------------------------------------------------------------------------- 312 # PERFORMANCE TUNING 313 # ---------------------------------------------------------------------------- 314 ··· 517 container_name: quickdid-sqlite 518 environment: 519 HTTP_EXTERNAL: quickdid.example.com 520 HTTP_PORT: 8080 521 SQLITE_URL: sqlite:/data/quickdid.db 522 CACHE_TTL_MEMORY: 600 ··· 717 718 ### 1. Service Key Protection 719 720 + - **Never commit** sensitive configuration to version control 721 - Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager) 722 - Rotate keys regularly 723 - Use different keys for different environments ··· 762 docker logs quickdid 763 764 # Verify environment variables 765 + docker exec quickdid env | grep -E "HTTP_EXTERNAL|HTTP_PORT" 766 767 # Test Redis connectivity 768 docker exec quickdid redis-cli -h redis ping ··· 947 ### Required Fields 948 949 - **HTTP_EXTERNAL**: Must be provided 950 + - **HTTP_EXTERNAL**: Must be provided 951 952 ### Value Constraints 953 ··· 986 987 ```bash 988 # Validate configuration without starting service 989 + HTTP_EXTERNAL=test quickdid --help 990 991 # Test with specific values (will fail validation) 992 CACHE_TTL_MEMORY=0 quickdid --help 993 994 # Debug configuration parsing 995 + RUST_LOG=debug HTTP_EXTERNAL=test quickdid 996 ``` 997 998 ## Support and Resources
+59
generate-wellknown.sh
···
··· 1 + #!/bin/bash 2 + 3 + # Script to generate .well-known static files based on QuickDID configuration 4 + # Usage: HTTP_EXTERNAL=quickdid.smokesignal.tools ./generate-wellknown.sh 5 + # 6 + # Note: Since we no longer process SERVICE_KEY, you'll need to manually 7 + # add the public key to the did.json file if you need DID document support. 8 + 9 + set -e 10 + 11 + # Check required environment variables 12 + if [ -z "$HTTP_EXTERNAL" ]; then 13 + echo "Error: HTTP_EXTERNAL environment variable is required" 14 + echo "Usage: HTTP_EXTERNAL=example.com ./generate-wellknown.sh" 15 + exit 1 16 + fi 17 + 18 + # Ensure www/.well-known directory exists 19 + mkdir -p www/.well-known 20 + 21 + # Generate service DID from HTTP_EXTERNAL 22 + if [[ "$HTTP_EXTERNAL" == *":"* ]]; then 23 + # Contains port - URL encode the colon 24 + SERVICE_DID="did:web:${HTTP_EXTERNAL//:/%3A}" 25 + else 26 + SERVICE_DID="did:web:$HTTP_EXTERNAL" 27 + fi 28 + 29 + echo "Generating .well-known files for $SERVICE_DID" 30 + 31 + # Write atproto-did file 32 + echo "$SERVICE_DID" > www/.well-known/atproto-did 33 + echo "Created: www/.well-known/atproto-did" 34 + 35 + # Create a basic did.json template 36 + # Note: You'll need to manually add the publicKeyMultibase if you need DID document support 37 + 38 + cat > www/.well-known/did.json <<EOF 39 + { 40 + "@context": [ 41 + "https://www.w3.org/ns/did/v1", 42 + "https://w3id.org/security/multikey/v1" 43 + ], 44 + "id": "$SERVICE_DID", 45 + "verificationMethod": [], 46 + "service": [ 47 + { 48 + "id": "${SERVICE_DID}#quickdid", 49 + "type": "QuickDIDService", 50 + "serviceEndpoint": "https://${HTTP_EXTERNAL}" 51 + } 52 + ] 53 + } 54 + EOF 55 + 56 + echo "Created: www/.well-known/did.json" 57 + echo "" 58 + echo "Note: The did.json file is a basic template. If you need DID document support," 59 + echo "you'll need to manually add the verificationMethod with your public key."
+1 -24
src/bin/quickdid.rs
··· 1 use anyhow::Result; 2 use atproto_identity::{ 3 config::{CertificateBundles, DnsNameservers}, 4 - key::{identify_key, to_public}, 5 resolve::HickoryDnsResolver, 6 }; 7 use quickdid::{ ··· 23 sqlite_schema::create_sqlite_pool, 24 task_manager::spawn_cancellable_task, 25 }; 26 - use serde_json::json; 27 use std::sync::Arc; 28 use tokio::signal; 29 use tokio_util::{sync::CancellationToken, task::TaskTracker}; ··· 79 println!(" -V, --version Print version information"); 80 println!(); 81 println!("ENVIRONMENT VARIABLES:"); 82 - println!(" SERVICE_KEY Private key for service identity (required)"); 83 println!( 84 " HTTP_EXTERNAL External hostname for service endpoints (required)" 85 ); ··· 191 config.validate()?; 192 193 tracing::info!("Starting QuickDID service on port {}", config.http_port); 194 - tracing::info!("Service DID: {}", config.service_did); 195 tracing::info!( 196 "Cache TTL - Memory: {}s, Redis: {}s, SQLite: {}s", 197 config.cache_ttl_memory, ··· 225 226 // Create DNS resolver 227 let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref()); 228 - 229 - // Process service key 230 - let private_service_key_data = identify_key(&config.service_key)?; 231 - let public_service_key_data = to_public(&private_service_key_data)?; 232 - let public_service_key = public_service_key_data.to_string(); 233 - 234 - // Create service DID document 235 - let service_document = json!({ 236 - "@context": vec!["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], 237 - "id": config.service_did.clone(), 238 - "verificationMethod": [{ 239 - "id": format!("{}#atproto", config.service_did), 240 - "type": "Multikey", 241 - "controller": config.service_did.clone(), 242 - "publicKeyMultibase": public_service_key 243 - }], 244 - "service": [] 245 - }); 246 247 // Create DNS resolver Arc for sharing 248 let dns_resolver_arc = Arc::new(dns_resolver); ··· 543 544 // Create app context with the queue adapter 545 let app_context = AppContext::new( 546 - service_document, 547 - config.service_did.clone(), 548 handle_resolver.clone(), 549 handle_queue, 550 metrics_publisher, 551 config.etag_seed.clone(), 552 config.cache_control_header.clone(), 553 ); 554 555 // Create router
··· 1 use anyhow::Result; 2 use atproto_identity::{ 3 config::{CertificateBundles, DnsNameservers}, 4 resolve::HickoryDnsResolver, 5 }; 6 use quickdid::{ ··· 22 sqlite_schema::create_sqlite_pool, 23 task_manager::spawn_cancellable_task, 24 }; 25 use std::sync::Arc; 26 use tokio::signal; 27 use tokio_util::{sync::CancellationToken, task::TaskTracker}; ··· 77 println!(" -V, --version Print version information"); 78 println!(); 79 println!("ENVIRONMENT VARIABLES:"); 80 println!( 81 " HTTP_EXTERNAL External hostname for service endpoints (required)" 82 ); ··· 188 config.validate()?; 189 190 tracing::info!("Starting QuickDID service on port {}", config.http_port); 191 tracing::info!( 192 "Cache TTL - Memory: {}s, Redis: {}s, SQLite: {}s", 193 config.cache_ttl_memory, ··· 221 222 // Create DNS resolver 223 let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref()); 224 225 // Create DNS resolver Arc for sharing 226 let dns_resolver_arc = Arc::new(dns_resolver); ··· 521 522 // Create app context with the queue adapter 523 let app_context = AppContext::new( 524 handle_resolver.clone(), 525 handle_queue, 526 metrics_publisher, 527 config.etag_seed.clone(), 528 config.cache_control_header.clone(), 529 + config.static_files_dir.clone(), 530 ); 531 532 // Create router
+9 -32
src/config.rs
··· 13 //! ```bash 14 //! # Minimal configuration 15 //! HTTP_EXTERNAL=quickdid.example.com \ 16 - //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 17 //! quickdid 18 //! 19 //! # Full configuration with Redis and custom settings 20 //! HTTP_EXTERNAL=quickdid.example.com \ 21 - //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 22 //! HTTP_PORT=3000 \ 23 //! REDIS_URL=redis://localhost:6379 \ 24 //! CACHE_TTL_MEMORY=300 \ ··· 38 pub enum ConfigError { 39 /// Missing required environment variable or command-line argument 40 /// 41 - /// Example: When SERVICE_KEY or HTTP_EXTERNAL are not provided 42 #[error("error-quickdid-config-1 Missing required environment variable: {0}")] 43 MissingRequired(String), 44 ··· 97 /// config.validate()?; 98 /// 99 /// println!("Service running at: {}", config.http_external); 100 - /// println!("Service DID: {}", config.service_did); 101 /// # Ok(()) 102 /// # } 103 /// ``` ··· 112 /// External hostname for service endpoints (e.g., "quickdid.example.com") 113 pub http_external: String, 114 115 - /// Private key for service identity (e.g., "did:key:z42tm...") 116 - pub service_key: String, 117 - 118 /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)") 119 pub user_agent: String, 120 - 121 - /// Derived service DID (e.g., "did:web:quickdid.example.com") 122 - /// Automatically generated from http_external with proper encoding 123 - pub service_did: String, 124 125 /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4") 126 pub dns_nameservers: Option<String>, ··· 250 /// For example, 0.8 means refresh when an entry has lived for 80% of its TTL. 251 /// Default: 0.8 (80%) 252 pub proactive_refresh_threshold: f64, 253 } 254 255 impl Config { ··· 257 /// 258 /// This method: 259 /// 1. Reads configuration from environment variables 260 - /// 2. Validates required fields (HTTP_EXTERNAL and SERVICE_KEY) 261 - /// 3. Generates derived values (service_did from http_external) 262 - /// 4. Applies defaults where appropriate 263 /// 264 /// ## Example 265 /// ··· 270 /// // Parse from environment variables 271 /// let config = Config::from_env()?; 272 /// 273 - /// // The service DID is automatically generated from HTTP_EXTERNAL 274 - /// assert!(config.service_did.starts_with("did:web:")); 275 /// # Ok(()) 276 /// # } 277 /// ``` ··· 280 /// 281 /// Returns `ConfigError::MissingRequired` if: 282 /// - HTTP_EXTERNAL is not provided 283 - /// - SERVICE_KEY is not provided 284 pub fn from_env() -> Result<Self, ConfigError> { 285 // Required fields 286 let http_external = env::var("HTTP_EXTERNAL") ··· 288 .filter(|s| !s.is_empty()) 289 .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?; 290 291 - let service_key = env::var("SERVICE_KEY") 292 - .ok() 293 - .filter(|s| !s.is_empty()) 294 - .ok_or_else(|| ConfigError::MissingRequired("SERVICE_KEY".to_string()))?; 295 - 296 // Generate default user agent 297 let default_user_agent = format!( 298 "quickdid/{} (+https://github.com/smokesignal.events/quickdid)", 299 env!("CARGO_PKG_VERSION") 300 ); 301 302 - // Generate service DID from http_external 303 - let service_did = if http_external.contains(':') { 304 - let encoded_external = http_external.replace(':', "%3A"); 305 - format!("did:web:{}", encoded_external) 306 - } else { 307 - format!("did:web:{}", http_external) 308 - }; 309 - 310 let mut config = Config { 311 http_port: get_env_or_default("HTTP_PORT", Some("8080")).unwrap(), 312 plc_hostname: get_env_or_default("PLC_HOSTNAME", Some("plc.directory")).unwrap(), 313 http_external, 314 - service_key, 315 user_agent: get_env_or_default("USER_AGENT", None).unwrap_or(default_user_agent), 316 - service_did, 317 dns_nameservers: get_env_or_default("DNS_NAMESERVERS", None), 318 certificate_bundles: get_env_or_default("CERTIFICATE_BUNDLES", None), 319 redis_url: get_env_or_default("REDIS_URL", None), ··· 350 metrics_tags: get_env_or_default("METRICS_TAGS", None), 351 proactive_refresh_enabled: parse_env("PROACTIVE_REFRESH_ENABLED", false)?, 352 proactive_refresh_threshold: parse_env("PROACTIVE_REFRESH_THRESHOLD", 0.8)?, 353 }; 354 355 // Calculate the Cache-Control header value if enabled
··· 13 //! ```bash 14 //! # Minimal configuration 15 //! HTTP_EXTERNAL=quickdid.example.com \ 16 //! quickdid 17 //! 18 //! # Full configuration with Redis and custom settings 19 //! HTTP_EXTERNAL=quickdid.example.com \ 20 //! HTTP_PORT=3000 \ 21 //! REDIS_URL=redis://localhost:6379 \ 22 //! CACHE_TTL_MEMORY=300 \ ··· 36 pub enum ConfigError { 37 /// Missing required environment variable or command-line argument 38 /// 39 + /// Example: When HTTP_EXTERNAL is not provided 40 #[error("error-quickdid-config-1 Missing required environment variable: {0}")] 41 MissingRequired(String), 42 ··· 95 /// config.validate()?; 96 /// 97 /// println!("Service running at: {}", config.http_external); 98 /// # Ok(()) 99 /// # } 100 /// ``` ··· 109 /// External hostname for service endpoints (e.g., "quickdid.example.com") 110 pub http_external: String, 111 112 /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)") 113 pub user_agent: String, 114 115 /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4") 116 pub dns_nameservers: Option<String>, ··· 240 /// For example, 0.8 means refresh when an entry has lived for 80% of its TTL. 241 /// Default: 0.8 (80%) 242 pub proactive_refresh_threshold: f64, 243 + 244 + /// Directory path for serving static files. 245 + /// When set, the root handler will serve files from this directory. 246 + /// Default: "www" (relative to working directory) 247 + pub static_files_dir: String, 248 } 249 250 impl Config { ··· 252 /// 253 /// This method: 254 /// 1. Reads configuration from environment variables 255 + /// 2. Validates required fields (HTTP_EXTERNAL) 256 + /// 3. Applies defaults where appropriate 257 /// 258 /// ## Example 259 /// ··· 264 /// // Parse from environment variables 265 /// let config = Config::from_env()?; 266 /// 267 /// # Ok(()) 268 /// # } 269 /// ``` ··· 272 /// 273 /// Returns `ConfigError::MissingRequired` if: 274 /// - HTTP_EXTERNAL is not provided 275 pub fn from_env() -> Result<Self, ConfigError> { 276 // Required fields 277 let http_external = env::var("HTTP_EXTERNAL") ··· 279 .filter(|s| !s.is_empty()) 280 .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?; 281 282 // Generate default user agent 283 let default_user_agent = format!( 284 "quickdid/{} (+https://github.com/smokesignal.events/quickdid)", 285 env!("CARGO_PKG_VERSION") 286 ); 287 288 let mut config = Config { 289 http_port: get_env_or_default("HTTP_PORT", Some("8080")).unwrap(), 290 plc_hostname: get_env_or_default("PLC_HOSTNAME", Some("plc.directory")).unwrap(), 291 http_external, 292 user_agent: get_env_or_default("USER_AGENT", None).unwrap_or(default_user_agent), 293 dns_nameservers: get_env_or_default("DNS_NAMESERVERS", None), 294 certificate_bundles: get_env_or_default("CERTIFICATE_BUNDLES", None), 295 redis_url: get_env_or_default("REDIS_URL", None), ··· 326 metrics_tags: get_env_or_default("METRICS_TAGS", None), 327 proactive_refresh_enabled: parse_env("PROACTIVE_REFRESH_ENABLED", false)?, 328 proactive_refresh_threshold: parse_env("PROACTIVE_REFRESH_THRESHOLD", 0.8)?, 329 + static_files_dir: get_env_or_default("STATIC_FILES_DIR", Some("www")).unwrap(), 330 }; 331 332 // Calculate the Cache-Control header value if enabled
+13 -47
src/http/server.rs
··· 4 use axum::{ 5 Router, 6 extract::{MatchedPath, State}, 7 - http::{Request, StatusCode}, 8 middleware::{self, Next}, 9 - response::{Html, IntoResponse, Json, Response}, 10 routing::get, 11 }; 12 use serde_json::json; 13 use std::sync::Arc; 14 use std::time::Instant; 15 16 pub(crate) struct InnerAppContext { 17 - pub(crate) service_document: serde_json::Value, 18 - pub(crate) service_did: String, 19 pub(crate) handle_resolver: Arc<dyn HandleResolver>, 20 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 21 pub(crate) metrics: SharedMetricsPublisher, 22 pub(crate) etag_seed: String, 23 pub(crate) cache_control_header: Option<String>, 24 } 25 26 #[derive(Clone)] ··· 29 impl AppContext { 30 /// Create a new AppContext with the provided configuration. 31 pub fn new( 32 - service_document: serde_json::Value, 33 - service_did: String, 34 handle_resolver: Arc<dyn HandleResolver>, 35 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 36 metrics: SharedMetricsPublisher, 37 etag_seed: String, 38 cache_control_header: Option<String>, 39 ) -> Self { 40 Self(Arc::new(InnerAppContext { 41 - service_document, 42 - service_did, 43 handle_resolver, 44 handle_queue, 45 metrics, 46 etag_seed, 47 cache_control_header, 48 })) 49 } 50 51 // Internal accessor methods for handlers 52 - pub(super) fn service_document(&self) -> &serde_json::Value { 53 - &self.0.service_document 54 - } 55 - 56 - pub(super) fn service_did(&self) -> &str { 57 - &self.0.service_did 58 - } 59 - 60 pub(super) fn etag_seed(&self) -> &str { 61 &self.0.etag_seed 62 } 63 64 pub(super) fn cache_control_header(&self) -> Option<&str> { 65 self.0.cache_control_header.as_deref() 66 } 67 } 68 ··· 124 } 125 126 pub fn create_router(app_context: AppContext) -> Router { 127 Router::new() 128 - .route("/", get(handle_index)) 129 - .route("/.well-known/did.json", get(handle_wellknown_did_json)) 130 - .route( 131 - "/.well-known/atproto-did", 132 - get(handle_wellknown_atproto_did), 133 - ) 134 .route("/xrpc/_health", get(handle_xrpc_health)) 135 .route( 136 "/xrpc/com.atproto.identity.resolveHandle", 137 get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle) 138 .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options), 139 ) 140 .layer(middleware::from_fn_with_state( 141 app_context.0.metrics.clone(), 142 metrics_middleware, 143 )) 144 .with_state(app_context) 145 - } 146 - 147 - pub(super) async fn handle_index() -> Html<&'static str> { 148 - Html( 149 - r#"<!DOCTYPE html> 150 - <html> 151 - <head> 152 - <title>QuickDID</title> 153 - </head> 154 - <body> 155 - <h1>QuickDID</h1> 156 - <p>AT Protocol Identity Resolution Service</p> 157 - </body> 158 - </html>"#, 159 - ) 160 - } 161 - 162 - pub(super) async fn handle_wellknown_did_json( 163 - State(context): State<AppContext>, 164 - ) -> Json<serde_json::Value> { 165 - Json(context.service_document().clone()) 166 - } 167 - 168 - pub(super) async fn handle_wellknown_atproto_did(State(context): State<AppContext>) -> Response { 169 - (StatusCode::OK, context.service_did().to_string()).into_response() 170 } 171 172 pub(super) async fn handle_xrpc_health() -> Json<serde_json::Value> {
··· 4 use axum::{ 5 Router, 6 extract::{MatchedPath, State}, 7 + http::Request, 8 middleware::{self, Next}, 9 + response::{Json, Response}, 10 routing::get, 11 }; 12 use serde_json::json; 13 use std::sync::Arc; 14 use std::time::Instant; 15 + use tower_http::services::ServeDir; 16 17 pub(crate) struct InnerAppContext { 18 pub(crate) handle_resolver: Arc<dyn HandleResolver>, 19 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 20 pub(crate) metrics: SharedMetricsPublisher, 21 pub(crate) etag_seed: String, 22 pub(crate) cache_control_header: Option<String>, 23 + pub(crate) static_files_dir: String, 24 } 25 26 #[derive(Clone)] ··· 29 impl AppContext { 30 /// Create a new AppContext with the provided configuration. 31 pub fn new( 32 handle_resolver: Arc<dyn HandleResolver>, 33 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 34 metrics: SharedMetricsPublisher, 35 etag_seed: String, 36 cache_control_header: Option<String>, 37 + static_files_dir: String, 38 ) -> Self { 39 Self(Arc::new(InnerAppContext { 40 handle_resolver, 41 handle_queue, 42 metrics, 43 etag_seed, 44 cache_control_header, 45 + static_files_dir, 46 })) 47 } 48 49 // Internal accessor methods for handlers 50 pub(super) fn etag_seed(&self) -> &str { 51 &self.0.etag_seed 52 } 53 54 pub(super) fn cache_control_header(&self) -> Option<&str> { 55 self.0.cache_control_header.as_deref() 56 + } 57 + 58 + pub(super) fn static_files_dir(&self) -> &str { 59 + &self.0.static_files_dir 60 } 61 } 62 ··· 118 } 119 120 pub fn create_router(app_context: AppContext) -> Router { 121 + let static_dir = app_context.static_files_dir().to_string(); 122 + 123 Router::new() 124 .route("/xrpc/_health", get(handle_xrpc_health)) 125 .route( 126 "/xrpc/com.atproto.identity.resolveHandle", 127 get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle) 128 .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options), 129 ) 130 + .fallback_service(ServeDir::new(static_dir)) 131 .layer(middleware::from_fn_with_state( 132 app_context.0.metrics.clone(), 133 metrics_middleware, 134 )) 135 .with_state(app_context) 136 } 137 138 pub(super) async fn handle_xrpc_health() -> Json<serde_json::Value> {
-8
src/metrics.rs
··· 416 // Set up environment for noop adapter 417 unsafe { 418 env::set_var("HTTP_EXTERNAL", "test.example.com"); 419 - env::set_var("SERVICE_KEY", "did:key:test"); 420 env::set_var("METRICS_ADAPTER", "noop"); 421 } 422 ··· 430 unsafe { 431 env::remove_var("METRICS_ADAPTER"); 432 env::remove_var("HTTP_EXTERNAL"); 433 - env::remove_var("SERVICE_KEY"); 434 } 435 } 436 ··· 452 // Set up environment for statsd adapter 453 unsafe { 454 env::set_var("HTTP_EXTERNAL", "test.example.com"); 455 - env::set_var("SERVICE_KEY", "did:key:test"); 456 env::set_var("METRICS_ADAPTER", "statsd"); 457 env::set_var("METRICS_STATSD_HOST", "localhost:8125"); 458 env::set_var("METRICS_PREFIX", "test"); ··· 472 env::remove_var("METRICS_PREFIX"); 473 env::remove_var("METRICS_TAGS"); 474 env::remove_var("HTTP_EXTERNAL"); 475 - env::remove_var("SERVICE_KEY"); 476 } 477 } 478 ··· 494 // Set up environment for statsd adapter without host 495 unsafe { 496 env::set_var("HTTP_EXTERNAL", "test.example.com"); 497 - env::set_var("SERVICE_KEY", "did:key:test"); 498 env::set_var("METRICS_ADAPTER", "statsd"); 499 env::remove_var("METRICS_STATSD_HOST"); 500 } ··· 512 unsafe { 513 env::remove_var("METRICS_ADAPTER"); 514 env::remove_var("HTTP_EXTERNAL"); 515 - env::remove_var("SERVICE_KEY"); 516 } 517 } 518 ··· 534 // Set up environment with invalid adapter 535 unsafe { 536 env::set_var("HTTP_EXTERNAL", "test.example.com"); 537 - env::set_var("SERVICE_KEY", "did:key:test"); 538 env::set_var("METRICS_ADAPTER", "invalid"); 539 env::remove_var("METRICS_STATSD_HOST"); // Clean up from other tests 540 } ··· 549 unsafe { 550 env::remove_var("METRICS_ADAPTER"); 551 env::remove_var("HTTP_EXTERNAL"); 552 - env::remove_var("SERVICE_KEY"); 553 } 554 } 555 }
··· 416 // Set up environment for noop adapter 417 unsafe { 418 env::set_var("HTTP_EXTERNAL", "test.example.com"); 419 env::set_var("METRICS_ADAPTER", "noop"); 420 } 421 ··· 429 unsafe { 430 env::remove_var("METRICS_ADAPTER"); 431 env::remove_var("HTTP_EXTERNAL"); 432 } 433 } 434 ··· 450 // Set up environment for statsd adapter 451 unsafe { 452 env::set_var("HTTP_EXTERNAL", "test.example.com"); 453 env::set_var("METRICS_ADAPTER", "statsd"); 454 env::set_var("METRICS_STATSD_HOST", "localhost:8125"); 455 env::set_var("METRICS_PREFIX", "test"); ··· 469 env::remove_var("METRICS_PREFIX"); 470 env::remove_var("METRICS_TAGS"); 471 env::remove_var("HTTP_EXTERNAL"); 472 } 473 } 474 ··· 490 // Set up environment for statsd adapter without host 491 unsafe { 492 env::set_var("HTTP_EXTERNAL", "test.example.com"); 493 env::set_var("METRICS_ADAPTER", "statsd"); 494 env::remove_var("METRICS_STATSD_HOST"); 495 } ··· 507 unsafe { 508 env::remove_var("METRICS_ADAPTER"); 509 env::remove_var("HTTP_EXTERNAL"); 510 } 511 } 512 ··· 528 // Set up environment with invalid adapter 529 unsafe { 530 env::set_var("HTTP_EXTERNAL", "test.example.com"); 531 env::set_var("METRICS_ADAPTER", "invalid"); 532 env::remove_var("METRICS_STATSD_HOST"); // Clean up from other tests 533 } ··· 542 unsafe { 543 env::remove_var("METRICS_ADAPTER"); 544 env::remove_var("HTTP_EXTERNAL"); 545 } 546 } 547 }
+1
www/.well-known/atproto-did
···
··· 1 + did:web:quickdid.smokesignal.tools
+15
www/.well-known/did.json
···
··· 1 + { 2 + "@context": [ 3 + "https://www.w3.org/ns/did/v1", 4 + "https://w3id.org/security/multikey/v1" 5 + ], 6 + "id": "did:web:quickdid.smokesignal.tools", 7 + "verificationMethod": [], 8 + "service": [ 9 + { 10 + "id": "#quickdid", 11 + "type": "QuickDIDService", 12 + "serviceEndpoint": "https://quickdid.smokesignal.tools" 13 + } 14 + ] 15 + }
+74
www/README.md
···
··· 1 + # QuickDID Static Files Directory 2 + 3 + This directory contains static files that are served by QuickDID. By default, QuickDID serves files from the `www` directory, but this can be configured using the `STATIC_FILES_DIR` environment variable. 4 + 5 + ## Directory Structure 6 + 7 + ``` 8 + www/ 9 + ├── .well-known/ 10 + │ ├── atproto-did # AT Protocol DID identifier 11 + │ └── did.json # DID document 12 + ├── index.html # Landing page 13 + └── README.md # This file 14 + ``` 15 + 16 + ## Files 17 + 18 + ### `.well-known/atproto-did` 19 + Contains the service's DID identifier (e.g., `did:web:example.com`). This file is used by AT Protocol clients to discover the service's DID. 20 + 21 + ### `.well-known/did.json` 22 + Contains the DID document with verification methods and service endpoints. This is a JSON-LD document following the W3C DID specification. 23 + 24 + ### `index.html` 25 + The landing page shown when users visit the root URL. This provides information about the service and available endpoints. 26 + 27 + ## Customization 28 + 29 + ### Using the Generation Script 30 + 31 + You can generate the `.well-known` files for your deployment using the provided script: 32 + 33 + ```bash 34 + HTTP_EXTERNAL=your-domain.com ./generate-wellknown.sh 35 + ``` 36 + 37 + This will create the appropriate files based on your domain. 38 + 39 + ### Manual Customization 40 + 41 + 1. **Update `.well-known/atproto-did`**: Replace with your service's DID 42 + 2. **Update `.well-known/did.json`**: Add your public key to the `verificationMethod` array if needed 43 + 3. **Customize `index.html`**: Modify the landing page to match your branding 44 + 45 + ### Docker Deployment 46 + 47 + When using Docker, you can mount custom static files: 48 + 49 + ```yaml 50 + volumes: 51 + - ./custom-www:/app/www:ro 52 + ``` 53 + 54 + Or just override specific files: 55 + 56 + ```yaml 57 + volumes: 58 + - ./custom-index.html:/app/www/index.html:ro 59 + - ./custom-wellknown:/app/www/.well-known:ro 60 + ``` 61 + 62 + ### Environment Variable 63 + 64 + You can change the static files directory using: 65 + 66 + ```bash 67 + STATIC_FILES_DIR=/path/to/custom/www 68 + ``` 69 + 70 + ## Security Notes 71 + 72 + - Static files are served with automatic MIME type detection 73 + - The `.well-known` files are crucial for AT Protocol compatibility 74 + - Ensure proper permissions on mounted volumes in production
+112
www/index.html
···
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>QuickDID - AT Protocol Identity Resolution Service</title> 7 + <style> 8 + body { 9 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 10 + line-height: 1.6; 11 + margin: 0; 12 + padding: 20px; 13 + max-width: 800px; 14 + margin: 0 auto; 15 + color: #333; 16 + } 17 + h1 { 18 + color: #2c3e50; 19 + border-bottom: 2px solid #3498db; 20 + padding-bottom: 10px; 21 + } 22 + .endpoints { 23 + background: #f8f9fa; 24 + border-radius: 5px; 25 + padding: 20px; 26 + margin: 20px 0; 27 + } 28 + .endpoint { 29 + margin: 15px 0; 30 + padding: 10px; 31 + background: white; 32 + border-left: 3px solid #3498db; 33 + border-radius: 3px; 34 + } 35 + .endpoint code { 36 + background: #f4f4f4; 37 + padding: 2px 5px; 38 + border-radius: 3px; 39 + font-size: 14px; 40 + } 41 + .endpoint .method { 42 + display: inline-block; 43 + padding: 2px 6px; 44 + background: #27ae60; 45 + color: white; 46 + border-radius: 3px; 47 + font-size: 12px; 48 + font-weight: bold; 49 + margin-right: 10px; 50 + } 51 + .info { 52 + background: #e3f2fd; 53 + border-left: 4px solid #2196f3; 54 + padding: 15px; 55 + margin: 20px 0; 56 + border-radius: 3px; 57 + } 58 + a { 59 + color: #3498db; 60 + text-decoration: none; 61 + } 62 + a:hover { 63 + text-decoration: underline; 64 + } 65 + </style> 66 + </head> 67 + <body> 68 + <h1>QuickDID</h1> 69 + <p><strong>AT Protocol Identity Resolution Service</strong></p> 70 + 71 + <div class="info"> 72 + <p>QuickDID is a high-performance handle-to-DID resolution service for the AT Protocol ecosystem.</p> 73 + </div> 74 + 75 + <h2>Available Endpoints</h2> 76 + 77 + <div class="endpoints"> 78 + <div class="endpoint"> 79 + <span class="method">GET</span> 80 + <code>/xrpc/com.atproto.identity.resolveHandle</code> 81 + <p>Resolve an AT Protocol handle to its DID</p> 82 + <p>Parameters: <code>?handle={handle}</code></p> 83 + </div> 84 + 85 + <div class="endpoint"> 86 + <span class="method">GET</span> 87 + <code>/xrpc/_health</code> 88 + <p>Health check endpoint</p> 89 + </div> 90 + 91 + <div class="endpoint"> 92 + <span class="method">GET</span> 93 + <code>/.well-known/did.json</code> 94 + <p>Service DID document</p> 95 + </div> 96 + 97 + <div class="endpoint"> 98 + <span class="method">GET</span> 99 + <code>/.well-known/atproto-did</code> 100 + <p>Service DID identifier</p> 101 + </div> 102 + </div> 103 + 104 + <h2>Example Usage</h2> 105 + <div class="endpoint"> 106 + <code>curl "https://quickdid.example.com/xrpc/com.atproto.identity.resolveHandle?handle=alice.bsky.social"</code> 107 + </div> 108 + 109 + <h2>Documentation</h2> 110 + <p>For more information, visit the <a href="https://github.com/your-org/quickdid" target="_blank">QuickDID repository</a>.</p> 111 + </body> 112 + </html>