+37
.dockerignore
+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
+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
+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
+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
+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
+4
Dockerfile
+17
-7
README.md
+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
+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
+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
+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
+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
-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
+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
+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
-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
www/.well-known/atproto-did
···
1
+
did:web:quickdid.smokesignal.tools
+15
www/.well-known/did.json
+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
+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
+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>