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

+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 1 # QuickDID Environment Configuration Template 2 2 # Copy this file to .env and customize for your deployment 3 - # 4 - # IMPORTANT: Never commit .env files with real SERVICE_KEY values 5 3 6 4 # ============================================================================ 7 5 # REQUIRED CONFIGURATION ··· 13 11 # - quickdid.example.com:8080 14 12 # - localhost:3007 15 13 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 14 22 15 # ============================================================================ 23 16 # NETWORK CONFIGURATION ··· 98 91 QUEUE_BUFFER_SIZE=1000 99 92 100 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 + # ============================================================================ 101 106 # LOGGING 102 107 # ============================================================================ 103 108 ··· 112 117 # ============================================================================ 113 118 114 119 # HTTP_EXTERNAL=localhost:3007 115 - # SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 116 120 # RUST_LOG=debug 117 121 # CACHE_TTL_MEMORY=60 118 122 # CACHE_TTL_REDIS=300
+4 -3
CLAUDE.md
··· 21 21 cargo build 22 22 23 23 # Run in debug mode (requires environment variables) 24 - HTTP_EXTERNAL=localhost:3007 SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK cargo run 24 + HTTP_EXTERNAL=localhost:3007 cargo run 25 25 26 26 # Run tests 27 27 cargo test ··· 71 71 4. **HTTP Server** (`src/http/`) 72 72 - XRPC endpoints for AT Protocol compatibility 73 73 - Health check endpoint 74 - - DID document serving via .well-known 74 + - Static file serving from configurable directory (default: www) 75 + - Serves .well-known files as static content 75 76 - CORS headers support for cross-origin requests 76 77 - Cache-Control headers with configurable max-age and stale directives 77 78 - ETag support with configurable seed for cache invalidation ··· 107 108 108 109 ### Required 109 110 - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 110 - - `SERVICE_KEY`: Private key for service identity (DID format) 111 111 112 112 ### Optional - Core Configuration 113 113 - `HTTP_PORT`: Server port (default: 8080) 114 114 - `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory) 115 115 - `RUST_LOG`: Logging level (e.g., debug, info) 116 + - `STATIC_FILES_DIR`: Directory for serving static files (default: www) 116 117 117 118 ### Optional - Caching 118 119 - `REDIS_URL`: Redis connection URL for caching
+33
Cargo.lock
··· 1053 1053 ] 1054 1054 1055 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]] 1056 1062 name = "httparse" 1057 1063 version = "1.10.1" 1058 1064 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1486 1492 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1487 1493 1488 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]] 1489 1505 name = "miniz_oxide" 1490 1506 version = "0.8.9" 1491 1507 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1872 1888 "thiserror 2.0.16", 1873 1889 "tokio", 1874 1890 "tokio-util", 1891 + "tower-http", 1875 1892 "tracing", 1876 1893 "tracing-subscriber", 1877 1894 ] ··· 2973 2990 dependencies = [ 2974 2991 "bitflags", 2975 2992 "bytes", 2993 + "futures-core", 2976 2994 "futures-util", 2977 2995 "http", 2978 2996 "http-body", 2997 + "http-body-util", 2998 + "http-range-header", 2999 + "httpdate", 2979 3000 "iri-string", 3001 + "mime", 3002 + "mime_guess", 3003 + "percent-encoding", 2980 3004 "pin-project-lite", 3005 + "tokio", 3006 + "tokio-util", 2981 3007 "tower", 2982 3008 "tower-layer", 2983 3009 "tower-service", 3010 + "tracing", 2984 3011 ] 2985 3012 2986 3013 [[package]] ··· 3068 3095 version = "1.18.0" 3069 3096 source = "registry+https://github.com/rust-lang/crates.io-index" 3070 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" 3071 3104 3072 3105 [[package]] 3073 3106 name = "unicode-bidi"
+1
Cargo.toml
··· 30 30 thiserror = "2.0" 31 31 tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "signal", "sync", "time", "net", "fs"] } 32 32 tokio-util = { version = "0.7", features = ["rt"] } 33 + tower-http = { version = "0.6", features = ["fs"] } 33 34 tracing = "0.1" 34 35 tracing-subscriber = { version = "0.3", features = ["env-filter"] } 35 36
+4
Dockerfile
··· 24 24 WORKDIR /app 25 25 COPY --from=builder /app/target/release/quickdid /app/quickdid 26 26 27 + # Copy static files for serving 28 + COPY www /app/www 29 + 27 30 ENV HTTP_PORT=8080 31 + ENV STATIC_FILES_DIR=/app/www 28 32 ENV RUST_LOG=info 29 33 ENV RUST_BACKTRACE=full 30 34
+17 -7
README.md
··· 83 83 84 84 ## Minimum Configuration 85 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. 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 87 88 88 ### Required 89 89 90 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 91 93 92 ### Example Minimal Setup 94 93 95 94 ```bash 96 - HTTP_EXTERNAL=localhost:3007 \ 97 - SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 98 - cargo run 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 99 108 ``` 100 109 101 110 This will start QuickDID with: ··· 155 164 - `PROACTIVE_REFRESH_ENABLED`: Enable proactive cache refreshing (default: false) 156 165 - `PROACTIVE_REFRESH_THRESHOLD`: Refresh when TTL remaining is below this threshold (0.0-1.0, default: 0.8) 157 166 167 + #### Static Files 168 + - `STATIC_FILES_DIR`: Directory for serving static files (default: www) 169 + 158 170 #### Logging 159 171 - `RUST_LOG`: Logging level (e.g., debug, info, warn, error) 160 172 ··· 163 175 #### Redis-based with Metrics (Multi-instance/HA) 164 176 ```bash 165 177 HTTP_EXTERNAL=quickdid.example.com \ 166 - SERVICE_KEY=did:key:yourkeyhere \ 167 178 HTTP_PORT=3000 \ 168 179 REDIS_URL=redis://localhost:6379 \ 169 180 CACHE_TTL_REDIS=86400 \ ··· 183 194 #### SQLite-based (Single-instance) 184 195 ```bash 185 196 HTTP_EXTERNAL=quickdid.example.com \ 186 - SERVICE_KEY=did:key:yourkeyhere \ 187 197 HTTP_PORT=3000 \ 188 198 SQLITE_URL=sqlite:./quickdid.db \ 189 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 40 **Constraints**: 41 41 - Must be a valid hostname or hostname:port combination 42 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 43 68 44 ## Network Configuration 69 45 ··· 785 761 - TTL=3600s (1 hour), threshold=0.8: Refresh after 48 minutes 786 762 - TTL=86400s (1 day), threshold=0.8: Refresh after 19.2 hours 787 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 + 788 816 ## HTTP Caching Configuration 789 817 790 818 ### `CACHE_MAX_AGE` ··· 958 986 ```bash 959 987 # .env.development 960 988 HTTP_EXTERNAL=localhost:3007 961 - SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK 962 989 RUST_LOG=debug 963 990 ``` 964 991 ··· 968 995 # .env.production.redis 969 996 # Required 970 997 HTTP_EXTERNAL=quickdid.example.com 971 - SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 972 998 973 999 # Network 974 1000 HTTP_PORT=8080 ··· 1015 1041 # .env.production.sqlite 1016 1042 # Required 1017 1043 HTTP_EXTERNAL=quickdid.example.com 1018 - SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 1019 1044 1020 1045 # Network 1021 1046 HTTP_PORT=8080 ··· 1050 1075 # .env.ha.redis 1051 1076 # Required 1052 1077 HTTP_EXTERNAL=quickdid.example.com 1053 - SERVICE_KEY=${SECRET_SERVICE_KEY} 1054 1078 1055 1079 # Network 1056 1080 HTTP_PORT=8080 ··· 1097 1121 # .env.hybrid 1098 1122 # Required 1099 1123 HTTP_EXTERNAL=quickdid.example.com 1100 - SERVICE_KEY=${SECRET_SERVICE_KEY} 1101 1124 1102 1125 # Network 1103 1126 HTTP_PORT=8080 ··· 1128 1151 image: quickdid:latest 1129 1152 environment: 1130 1153 HTTP_EXTERNAL: quickdid.example.com 1131 - SERVICE_KEY: ${SERVICE_KEY} 1132 1154 HTTP_PORT: 8080 1133 1155 REDIS_URL: redis://redis:6379/0 1134 1156 CACHE_TTL_MEMORY: 600 ··· 1157 1179 image: quickdid:latest 1158 1180 environment: 1159 1181 HTTP_EXTERNAL: quickdid.example.com 1160 - SERVICE_KEY: ${SERVICE_KEY} 1161 1182 HTTP_PORT: 8080 1162 1183 SQLITE_URL: sqlite:/data/quickdid.db 1163 1184 CACHE_TTL_MEMORY: 600 ··· 1183 1204 ### Required Fields 1184 1205 1185 1206 1. **HTTP_EXTERNAL**: Must be provided 1186 - 2. **SERVICE_KEY**: Must be provided 1207 + 2. **HTTP_EXTERNAL**: Must be provided 1187 1208 1188 1209 ### Value Constraints 1189 1210 ··· 1228 1249 1229 1250 ```bash 1230 1251 # Validate configuration 1231 - HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 1252 + HTTP_EXTERNAL=test quickdid --help 1232 1253 1233 1254 # Test with specific values 1234 1255 CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation 1235 1256 1236 1257 # Check parsed configuration (with debug logging) 1237 - RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 1258 + RUST_LOG=debug HTTP_EXTERNAL=test quickdid 1238 1259 ``` 1239 1260 1240 1261 ## Best Practices 1241 1262 1242 1263 ### Security 1243 1264 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://`) 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 1248 1268 5. Implement network segmentation for Redis access 1249 1269 1250 1270 ### Performance ··· 1280 1300 ### Deployment 1281 1301 1282 1302 1. Use `.env` files for local development 1283 - 2. Use secrets management for production SERVICE_KEY 1303 + 2. Use secrets management for production configurations 1284 1304 3. Set resource limits in container orchestration 1285 1305 4. Use health checks to monitor service availability 1286 1306 5. Implement gradual rollouts with feature flags
+18 -14
docs/production-deployment.md
··· 42 42 # - localhost:3007 (for testing only) 43 43 HTTP_EXTERNAL=quickdid.example.com 44 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 45 # ---------------------------------------------------------------------------- 54 46 # NETWORK CONFIGURATION 55 47 # ---------------------------------------------------------------------------- ··· 304 296 PROACTIVE_REFRESH_THRESHOLD=0.8 305 297 306 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 + # ---------------------------------------------------------------------------- 307 312 # PERFORMANCE TUNING 308 313 # ---------------------------------------------------------------------------- 309 314 ··· 512 517 container_name: quickdid-sqlite 513 518 environment: 514 519 HTTP_EXTERNAL: quickdid.example.com 515 - SERVICE_KEY: ${SERVICE_KEY} 516 520 HTTP_PORT: 8080 517 521 SQLITE_URL: sqlite:/data/quickdid.db 518 522 CACHE_TTL_MEMORY: 600 ··· 713 717 714 718 ### 1. Service Key Protection 715 719 716 - - **Never commit** the `SERVICE_KEY` to version control 720 + - **Never commit** sensitive configuration to version control 717 721 - Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager) 718 722 - Rotate keys regularly 719 723 - Use different keys for different environments ··· 758 762 docker logs quickdid 759 763 760 764 # Verify environment variables 761 - docker exec quickdid env | grep -E "HTTP_EXTERNAL|SERVICE_KEY" 765 + docker exec quickdid env | grep -E "HTTP_EXTERNAL|HTTP_PORT" 762 766 763 767 # Test Redis connectivity 764 768 docker exec quickdid redis-cli -h redis ping ··· 943 947 ### Required Fields 944 948 945 949 - **HTTP_EXTERNAL**: Must be provided 946 - - **SERVICE_KEY**: Must be provided 950 + - **HTTP_EXTERNAL**: Must be provided 947 951 948 952 ### Value Constraints 949 953 ··· 982 986 983 987 ```bash 984 988 # Validate configuration without starting service 985 - HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help 989 + HTTP_EXTERNAL=test quickdid --help 986 990 987 991 # Test with specific values (will fail validation) 988 992 CACHE_TTL_MEMORY=0 quickdid --help 989 993 990 994 # Debug configuration parsing 991 - RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid 995 + RUST_LOG=debug HTTP_EXTERNAL=test quickdid 992 996 ``` 993 997 994 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 1 use anyhow::Result; 2 2 use atproto_identity::{ 3 3 config::{CertificateBundles, DnsNameservers}, 4 - key::{identify_key, to_public}, 5 4 resolve::HickoryDnsResolver, 6 5 }; 7 6 use quickdid::{ ··· 23 22 sqlite_schema::create_sqlite_pool, 24 23 task_manager::spawn_cancellable_task, 25 24 }; 26 - use serde_json::json; 27 25 use std::sync::Arc; 28 26 use tokio::signal; 29 27 use tokio_util::{sync::CancellationToken, task::TaskTracker}; ··· 79 77 println!(" -V, --version Print version information"); 80 78 println!(); 81 79 println!("ENVIRONMENT VARIABLES:"); 82 - println!(" SERVICE_KEY Private key for service identity (required)"); 83 80 println!( 84 81 " HTTP_EXTERNAL External hostname for service endpoints (required)" 85 82 ); ··· 191 188 config.validate()?; 192 189 193 190 tracing::info!("Starting QuickDID service on port {}", config.http_port); 194 - tracing::info!("Service DID: {}", config.service_did); 195 191 tracing::info!( 196 192 "Cache TTL - Memory: {}s, Redis: {}s, SQLite: {}s", 197 193 config.cache_ttl_memory, ··· 225 221 226 222 // Create DNS resolver 227 223 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 224 247 225 // Create DNS resolver Arc for sharing 248 226 let dns_resolver_arc = Arc::new(dns_resolver); ··· 543 521 544 522 // Create app context with the queue adapter 545 523 let app_context = AppContext::new( 546 - service_document, 547 - config.service_did.clone(), 548 524 handle_resolver.clone(), 549 525 handle_queue, 550 526 metrics_publisher, 551 527 config.etag_seed.clone(), 552 528 config.cache_control_header.clone(), 529 + config.static_files_dir.clone(), 553 530 ); 554 531 555 532 // Create router
+9 -32
src/config.rs
··· 13 13 //! ```bash 14 14 //! # Minimal configuration 15 15 //! HTTP_EXTERNAL=quickdid.example.com \ 16 - //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 17 16 //! quickdid 18 17 //! 19 18 //! # Full configuration with Redis and custom settings 20 19 //! HTTP_EXTERNAL=quickdid.example.com \ 21 - //! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 22 20 //! HTTP_PORT=3000 \ 23 21 //! REDIS_URL=redis://localhost:6379 \ 24 22 //! CACHE_TTL_MEMORY=300 \ ··· 38 36 pub enum ConfigError { 39 37 /// Missing required environment variable or command-line argument 40 38 /// 41 - /// Example: When SERVICE_KEY or HTTP_EXTERNAL are not provided 39 + /// Example: When HTTP_EXTERNAL is not provided 42 40 #[error("error-quickdid-config-1 Missing required environment variable: {0}")] 43 41 MissingRequired(String), 44 42 ··· 97 95 /// config.validate()?; 98 96 /// 99 97 /// println!("Service running at: {}", config.http_external); 100 - /// println!("Service DID: {}", config.service_did); 101 98 /// # Ok(()) 102 99 /// # } 103 100 /// ``` ··· 112 109 /// External hostname for service endpoints (e.g., "quickdid.example.com") 113 110 pub http_external: String, 114 111 115 - /// Private key for service identity (e.g., "did:key:z42tm...") 116 - pub service_key: String, 117 - 118 112 /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)") 119 113 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 114 125 115 /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4") 126 116 pub dns_nameservers: Option<String>, ··· 250 240 /// For example, 0.8 means refresh when an entry has lived for 80% of its TTL. 251 241 /// Default: 0.8 (80%) 252 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, 253 248 } 254 249 255 250 impl Config { ··· 257 252 /// 258 253 /// This method: 259 254 /// 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 255 + /// 2. Validates required fields (HTTP_EXTERNAL) 256 + /// 3. Applies defaults where appropriate 263 257 /// 264 258 /// ## Example 265 259 /// ··· 270 264 /// // Parse from environment variables 271 265 /// let config = Config::from_env()?; 272 266 /// 273 - /// // The service DID is automatically generated from HTTP_EXTERNAL 274 - /// assert!(config.service_did.starts_with("did:web:")); 275 267 /// # Ok(()) 276 268 /// # } 277 269 /// ``` ··· 280 272 /// 281 273 /// Returns `ConfigError::MissingRequired` if: 282 274 /// - HTTP_EXTERNAL is not provided 283 - /// - SERVICE_KEY is not provided 284 275 pub fn from_env() -> Result<Self, ConfigError> { 285 276 // Required fields 286 277 let http_external = env::var("HTTP_EXTERNAL") ··· 288 279 .filter(|s| !s.is_empty()) 289 280 .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?; 290 281 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 282 // Generate default user agent 297 283 let default_user_agent = format!( 298 284 "quickdid/{} (+https://github.com/smokesignal.events/quickdid)", 299 285 env!("CARGO_PKG_VERSION") 300 286 ); 301 287 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 288 let mut config = Config { 311 289 http_port: get_env_or_default("HTTP_PORT", Some("8080")).unwrap(), 312 290 plc_hostname: get_env_or_default("PLC_HOSTNAME", Some("plc.directory")).unwrap(), 313 291 http_external, 314 - service_key, 315 292 user_agent: get_env_or_default("USER_AGENT", None).unwrap_or(default_user_agent), 316 - service_did, 317 293 dns_nameservers: get_env_or_default("DNS_NAMESERVERS", None), 318 294 certificate_bundles: get_env_or_default("CERTIFICATE_BUNDLES", None), 319 295 redis_url: get_env_or_default("REDIS_URL", None), ··· 350 326 metrics_tags: get_env_or_default("METRICS_TAGS", None), 351 327 proactive_refresh_enabled: parse_env("PROACTIVE_REFRESH_ENABLED", false)?, 352 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(), 353 330 }; 354 331 355 332 // Calculate the Cache-Control header value if enabled
+13 -47
src/http/server.rs
··· 4 4 use axum::{ 5 5 Router, 6 6 extract::{MatchedPath, State}, 7 - http::{Request, StatusCode}, 7 + http::Request, 8 8 middleware::{self, Next}, 9 - response::{Html, IntoResponse, Json, Response}, 9 + response::{Json, Response}, 10 10 routing::get, 11 11 }; 12 12 use serde_json::json; 13 13 use std::sync::Arc; 14 14 use std::time::Instant; 15 + use tower_http::services::ServeDir; 15 16 16 17 pub(crate) struct InnerAppContext { 17 - pub(crate) service_document: serde_json::Value, 18 - pub(crate) service_did: String, 19 18 pub(crate) handle_resolver: Arc<dyn HandleResolver>, 20 19 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 21 20 pub(crate) metrics: SharedMetricsPublisher, 22 21 pub(crate) etag_seed: String, 23 22 pub(crate) cache_control_header: Option<String>, 23 + pub(crate) static_files_dir: String, 24 24 } 25 25 26 26 #[derive(Clone)] ··· 29 29 impl AppContext { 30 30 /// Create a new AppContext with the provided configuration. 31 31 pub fn new( 32 - service_document: serde_json::Value, 33 - service_did: String, 34 32 handle_resolver: Arc<dyn HandleResolver>, 35 33 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 36 34 metrics: SharedMetricsPublisher, 37 35 etag_seed: String, 38 36 cache_control_header: Option<String>, 37 + static_files_dir: String, 39 38 ) -> Self { 40 39 Self(Arc::new(InnerAppContext { 41 - service_document, 42 - service_did, 43 40 handle_resolver, 44 41 handle_queue, 45 42 metrics, 46 43 etag_seed, 47 44 cache_control_header, 45 + static_files_dir, 48 46 })) 49 47 } 50 48 51 49 // 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 50 pub(super) fn etag_seed(&self) -> &str { 61 51 &self.0.etag_seed 62 52 } 63 53 64 54 pub(super) fn cache_control_header(&self) -> Option<&str> { 65 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 66 60 } 67 61 } 68 62 ··· 124 118 } 125 119 126 120 pub fn create_router(app_context: AppContext) -> Router { 121 + let static_dir = app_context.static_files_dir().to_string(); 122 + 127 123 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 124 .route("/xrpc/_health", get(handle_xrpc_health)) 135 125 .route( 136 126 "/xrpc/com.atproto.identity.resolveHandle", 137 127 get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle) 138 128 .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options), 139 129 ) 130 + .fallback_service(ServeDir::new(static_dir)) 140 131 .layer(middleware::from_fn_with_state( 141 132 app_context.0.metrics.clone(), 142 133 metrics_middleware, 143 134 )) 144 135 .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 136 } 171 137 172 138 pub(super) async fn handle_xrpc_health() -> Json<serde_json::Value> {
-8
src/metrics.rs
··· 416 416 // Set up environment for noop adapter 417 417 unsafe { 418 418 env::set_var("HTTP_EXTERNAL", "test.example.com"); 419 - env::set_var("SERVICE_KEY", "did:key:test"); 420 419 env::set_var("METRICS_ADAPTER", "noop"); 421 420 } 422 421 ··· 430 429 unsafe { 431 430 env::remove_var("METRICS_ADAPTER"); 432 431 env::remove_var("HTTP_EXTERNAL"); 433 - env::remove_var("SERVICE_KEY"); 434 432 } 435 433 } 436 434 ··· 452 450 // Set up environment for statsd adapter 453 451 unsafe { 454 452 env::set_var("HTTP_EXTERNAL", "test.example.com"); 455 - env::set_var("SERVICE_KEY", "did:key:test"); 456 453 env::set_var("METRICS_ADAPTER", "statsd"); 457 454 env::set_var("METRICS_STATSD_HOST", "localhost:8125"); 458 455 env::set_var("METRICS_PREFIX", "test"); ··· 472 469 env::remove_var("METRICS_PREFIX"); 473 470 env::remove_var("METRICS_TAGS"); 474 471 env::remove_var("HTTP_EXTERNAL"); 475 - env::remove_var("SERVICE_KEY"); 476 472 } 477 473 } 478 474 ··· 494 490 // Set up environment for statsd adapter without host 495 491 unsafe { 496 492 env::set_var("HTTP_EXTERNAL", "test.example.com"); 497 - env::set_var("SERVICE_KEY", "did:key:test"); 498 493 env::set_var("METRICS_ADAPTER", "statsd"); 499 494 env::remove_var("METRICS_STATSD_HOST"); 500 495 } ··· 512 507 unsafe { 513 508 env::remove_var("METRICS_ADAPTER"); 514 509 env::remove_var("HTTP_EXTERNAL"); 515 - env::remove_var("SERVICE_KEY"); 516 510 } 517 511 } 518 512 ··· 534 528 // Set up environment with invalid adapter 535 529 unsafe { 536 530 env::set_var("HTTP_EXTERNAL", "test.example.com"); 537 - env::set_var("SERVICE_KEY", "did:key:test"); 538 531 env::set_var("METRICS_ADAPTER", "invalid"); 539 532 env::remove_var("METRICS_STATSD_HOST"); // Clean up from other tests 540 533 } ··· 549 542 unsafe { 550 543 env::remove_var("METRICS_ADAPTER"); 551 544 env::remove_var("HTTP_EXTERNAL"); 552 - env::remove_var("SERVICE_KEY"); 553 545 } 554 546 } 555 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>