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: caching resolve handle xrpc service

Nick Gerakines 65fc5031

+6963
+9
.gitignore
··· 1 + /target 2 + .vscode/ 3 + 4 + # Environment variables 5 + .env 6 + .env.local 7 + 8 + # Docker 9 + docker-compose.override.yml
+167
CLAUDE.md
··· 1 + # QuickDID - Development Guide for Claude 2 + 3 + ## Overview 4 + 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. 5 + 6 + ## Common Commands 7 + 8 + ### Building and Running 9 + ```bash 10 + # Build the project 11 + cargo build 12 + 13 + # Run in debug mode 14 + cargo run 15 + 16 + # Run tests 17 + cargo test 18 + 19 + # Type checking 20 + cargo check 21 + 22 + # Run with environment variables 23 + HTTP_EXTERNAL=localhost:3007 SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK cargo run 24 + ``` 25 + 26 + ### Development with VS Code 27 + The project includes a `.vscode/launch.json` configuration for debugging with Redis integration. Use the "Debug executable 'quickdid'" launch configuration. 28 + 29 + ## Architecture 30 + 31 + ### Core Components 32 + 33 + 1. **Handle Resolution** (`src/handle_resolver.rs`) 34 + - `BaseHandleResolver`: Core resolution using DNS and HTTP 35 + - `CachingHandleResolver`: In-memory caching layer 36 + - `RedisHandleResolver`: Redis-backed persistent caching with 90-day TTL 37 + - Uses binary serialization via `HandleResolutionResult` for space efficiency 38 + 39 + 2. **Binary Serialization** (`src/handle_resolution_result.rs`) 40 + - Compact storage format using bincode 41 + - Strips DID prefixes for did:web and did:plc methods 42 + - Stores: timestamp (u64), method type (i16), payload (String) 43 + 44 + 3. **Queue System** (`src/queue_adapter.rs`) 45 + - Supports MPSC (in-process) and Redis adapters 46 + - `HandleResolutionWork` items processed asynchronously 47 + - Redis uses reliable queue pattern (LPUSH/RPOPLPUSH/LREM) 48 + 49 + 4. **HTTP Server** (`src/http/`) 50 + - XRPC endpoints for AT Protocol compatibility 51 + - Health check endpoint 52 + - DID document serving via .well-known 53 + 54 + ## Key Technical Details 55 + 56 + ### DID Method Types 57 + - `did:web`: Web-based DIDs, prefix stripped for storage 58 + - `did:plc`: PLC directory DIDs, prefix stripped for storage 59 + - Other DID methods stored with full identifier 60 + 61 + ### Redis Integration 62 + - **Caching**: Uses MetroHash64 for key generation, stores binary data 63 + - **Queuing**: Reliable queue with processing/dead letter queues 64 + - **Key Prefixes**: Configurable via `QUEUE_REDIS_PREFIX` environment variable 65 + 66 + ### Handle Resolution Flow 67 + 1. Check Redis cache (if configured) 68 + 2. Fall back to in-memory cache 69 + 3. Perform DNS TXT lookup or HTTP well-known query 70 + 4. Cache result with appropriate TTL 71 + 5. Return DID or error 72 + 73 + ## Environment Variables 74 + 75 + ### Required 76 + - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 77 + - `SERVICE_KEY`: Private key for service identity (DID format) 78 + 79 + ### Optional 80 + - `HTTP_PORT`: Server port (default: 8080) 81 + - `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory) 82 + - `REDIS_URL`: Redis connection URL for caching 83 + - `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc) 84 + - `QUEUE_REDIS_PREFIX`: Redis key prefix for queues (default: queue:handleresolver:) 85 + - `QUEUE_WORKER_ID`: Worker ID for Redis queue (auto-generated if not set) 86 + - `RUST_LOG`: Logging level (e.g., debug, info) 87 + 88 + ## Error Handling 89 + 90 + All error strings must use this format: 91 + 92 + error-quickdid-<domain>-<number> <message>: <details> 93 + 94 + Example errors: 95 + 96 + * error-quickdid-resolve-1 Multiple DIDs resolved for method 97 + * error-quickdid-plc-1 HTTP request failed: https://google.com/ Not Found 98 + * error-quickdid-key-1 Error decoding key: invalid 99 + 100 + Errors should be represented as enums using the `thiserror` library. 101 + 102 + Avoid creating new errors with the `anyhow!(...)` or `bail!(...)` macro. 103 + 104 + ## Testing 105 + 106 + ### Running Tests 107 + ```bash 108 + # Run all tests 109 + cargo test 110 + 111 + # Run with Redis integration tests 112 + TEST_REDIS_URL=redis://localhost:6379 cargo test 113 + 114 + # Run specific test module 115 + cargo test handle_resolver::tests 116 + ``` 117 + 118 + ### Test Coverage Areas 119 + - Handle resolution with various DID methods 120 + - Binary serialization/deserialization 121 + - Redis caching and expiration 122 + - Queue processing logic 123 + - HTTP endpoint responses 124 + 125 + ## Development Patterns 126 + 127 + ### Error Handling 128 + - Uses `anyhow::Result` for error propagation 129 + - Graceful fallbacks when Redis is unavailable 130 + - Detailed tracing for debugging 131 + 132 + ### Performance Optimizations 133 + - Binary serialization reduces storage by ~40% 134 + - MetroHash64 for fast key generation 135 + - Connection pooling for Redis 136 + - Configurable TTLs for cache entries 137 + 138 + ### Code Style 139 + - Follow existing Rust idioms and patterns 140 + - Use `tracing` for logging, not `println!` 141 + - Prefer `Arc` for shared state across async tasks 142 + - Handle errors explicitly, avoid `.unwrap()` in production code 143 + 144 + ## Common Tasks 145 + 146 + ### Adding a New DID Method 147 + 1. Update `DidMethodType` enum in `handle_resolution_result.rs` 148 + 2. Modify `parse_did()` and `to_did()` methods 149 + 3. Add test cases for the new method type 150 + 151 + ### Modifying Cache TTL 152 + - For in-memory: Pass TTL to `CachingHandleResolver::new()` 153 + - For Redis: Modify `RedisHandleResolver::ttl_seconds()` 154 + 155 + ### Debugging Resolution Issues 156 + 1. Enable debug logging: `RUST_LOG=debug` 157 + 2. Check Redis cache: `redis-cli GET "handle:<hash>"` 158 + 3. Monitor queue processing in logs 159 + 4. Verify DNS/HTTP connectivity to AT Protocol infrastructure 160 + 161 + ## Dependencies 162 + - `atproto-identity`: Core AT Protocol identity resolution 163 + - `bincode`: Binary serialization 164 + - `deadpool-redis`: Redis connection pooling 165 + - `metrohash`: Fast non-cryptographic hashing 166 + - `tokio`: Async runtime 167 + - `axum`: Web framework
+3581
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.24.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.1" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "allocator-api2" 31 + version = "0.2.21" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 + 35 + [[package]] 36 + name = "android-tzdata" 37 + version = "0.1.1" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 40 + 41 + [[package]] 42 + name = "android_system_properties" 43 + version = "0.1.5" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 46 + dependencies = [ 47 + "libc", 48 + ] 49 + 50 + [[package]] 51 + name = "anstream" 52 + version = "0.6.20" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 55 + dependencies = [ 56 + "anstyle", 57 + "anstyle-parse", 58 + "anstyle-query", 59 + "anstyle-wincon", 60 + "colorchoice", 61 + "is_terminal_polyfill", 62 + "utf8parse", 63 + ] 64 + 65 + [[package]] 66 + name = "anstyle" 67 + version = "1.0.11" 68 + source = "registry+https://github.com/rust-lang/crates.io-index" 69 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 70 + 71 + [[package]] 72 + name = "anstyle-parse" 73 + version = "0.2.7" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 76 + dependencies = [ 77 + "utf8parse", 78 + ] 79 + 80 + [[package]] 81 + name = "anstyle-query" 82 + version = "1.1.4" 83 + source = "registry+https://github.com/rust-lang/crates.io-index" 84 + checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 85 + dependencies = [ 86 + "windows-sys 0.60.2", 87 + ] 88 + 89 + [[package]] 90 + name = "anstyle-wincon" 91 + version = "3.0.10" 92 + source = "registry+https://github.com/rust-lang/crates.io-index" 93 + checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 94 + dependencies = [ 95 + "anstyle", 96 + "once_cell_polyfill", 97 + "windows-sys 0.60.2", 98 + ] 99 + 100 + [[package]] 101 + name = "anyhow" 102 + version = "1.0.99" 103 + source = "registry+https://github.com/rust-lang/crates.io-index" 104 + checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 105 + 106 + [[package]] 107 + name = "arc-swap" 108 + version = "1.7.1" 109 + source = "registry+https://github.com/rust-lang/crates.io-index" 110 + checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 111 + 112 + [[package]] 113 + name = "async-trait" 114 + version = "0.1.89" 115 + source = "registry+https://github.com/rust-lang/crates.io-index" 116 + checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" 117 + dependencies = [ 118 + "proc-macro2", 119 + "quote", 120 + "syn", 121 + ] 122 + 123 + [[package]] 124 + name = "atomic-waker" 125 + version = "1.1.2" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 128 + 129 + [[package]] 130 + name = "atproto-identity" 131 + version = "0.11.3" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "aaac8751c7e4329a95714c01d9e47d22d94bc8c96e78079098312235128acb9f" 134 + dependencies = [ 135 + "anyhow", 136 + "async-trait", 137 + "ecdsa", 138 + "elliptic-curve", 139 + "hickory-resolver", 140 + "k256", 141 + "lru", 142 + "multibase", 143 + "p256", 144 + "p384", 145 + "rand 0.8.5", 146 + "reqwest", 147 + "serde", 148 + "serde_ipld_dagcbor", 149 + "serde_json", 150 + "thiserror 2.0.16", 151 + "tokio", 152 + "tracing", 153 + ] 154 + 155 + [[package]] 156 + name = "atproto-oauth" 157 + version = "0.11.3" 158 + source = "registry+https://github.com/rust-lang/crates.io-index" 159 + checksum = "ee92a16f57838093bf72aa517a462613d3786603a2b5e5cd734e2215a971448f" 160 + dependencies = [ 161 + "anyhow", 162 + "async-trait", 163 + "atproto-identity", 164 + "base64", 165 + "chrono", 166 + "ecdsa", 167 + "elliptic-curve", 168 + "k256", 169 + "lru", 170 + "multibase", 171 + "p256", 172 + "p384", 173 + "rand 0.8.5", 174 + "reqwest", 175 + "reqwest-chain", 176 + "reqwest-middleware", 177 + "serde", 178 + "serde_ipld_dagcbor", 179 + "serde_json", 180 + "sha2", 181 + "thiserror 2.0.16", 182 + "tokio", 183 + "tracing", 184 + "ulid", 185 + ] 186 + 187 + [[package]] 188 + name = "atproto-record" 189 + version = "0.11.3" 190 + source = "registry+https://github.com/rust-lang/crates.io-index" 191 + checksum = "34e7c05334833c46feb38e2dbc5e80df6c2f044d32e6248198665809b405dc28" 192 + dependencies = [ 193 + "anyhow", 194 + "atproto-identity", 195 + "base64", 196 + "chrono", 197 + "serde", 198 + "serde_ipld_dagcbor", 199 + "serde_json", 200 + "thiserror 2.0.16", 201 + ] 202 + 203 + [[package]] 204 + name = "atproto-xrpcs" 205 + version = "0.11.3" 206 + source = "registry+https://github.com/rust-lang/crates.io-index" 207 + checksum = "c25b0475dc63f9db54c6c0397860f8368796ba2067dc137ea5bcb7fbbee43575" 208 + dependencies = [ 209 + "anyhow", 210 + "async-trait", 211 + "atproto-identity", 212 + "atproto-oauth", 213 + "atproto-record", 214 + "axum", 215 + "base64", 216 + "chrono", 217 + "elliptic-curve", 218 + "hickory-resolver", 219 + "http", 220 + "rand 0.8.5", 221 + "reqwest", 222 + "reqwest-chain", 223 + "reqwest-middleware", 224 + "serde", 225 + "serde_json", 226 + "thiserror 2.0.16", 227 + "tokio", 228 + "tracing", 229 + ] 230 + 231 + [[package]] 232 + name = "autocfg" 233 + version = "1.5.0" 234 + source = "registry+https://github.com/rust-lang/crates.io-index" 235 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 236 + 237 + [[package]] 238 + name = "axum" 239 + version = "0.8.4" 240 + source = "registry+https://github.com/rust-lang/crates.io-index" 241 + checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 242 + dependencies = [ 243 + "axum-core", 244 + "axum-macros", 245 + "bytes", 246 + "form_urlencoded", 247 + "futures-util", 248 + "http", 249 + "http-body", 250 + "http-body-util", 251 + "hyper", 252 + "hyper-util", 253 + "itoa", 254 + "matchit", 255 + "memchr", 256 + "mime", 257 + "percent-encoding", 258 + "pin-project-lite", 259 + "rustversion", 260 + "serde", 261 + "serde_json", 262 + "serde_path_to_error", 263 + "serde_urlencoded", 264 + "sync_wrapper", 265 + "tokio", 266 + "tower", 267 + "tower-layer", 268 + "tower-service", 269 + "tracing", 270 + ] 271 + 272 + [[package]] 273 + name = "axum-core" 274 + version = "0.5.2" 275 + source = "registry+https://github.com/rust-lang/crates.io-index" 276 + checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 277 + dependencies = [ 278 + "bytes", 279 + "futures-core", 280 + "http", 281 + "http-body", 282 + "http-body-util", 283 + "mime", 284 + "pin-project-lite", 285 + "rustversion", 286 + "sync_wrapper", 287 + "tower-layer", 288 + "tower-service", 289 + "tracing", 290 + ] 291 + 292 + [[package]] 293 + name = "axum-macros" 294 + version = "0.5.0" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 297 + dependencies = [ 298 + "proc-macro2", 299 + "quote", 300 + "syn", 301 + ] 302 + 303 + [[package]] 304 + name = "backon" 305 + version = "1.5.2" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" 308 + dependencies = [ 309 + "fastrand", 310 + ] 311 + 312 + [[package]] 313 + name = "backtrace" 314 + version = "0.3.75" 315 + source = "registry+https://github.com/rust-lang/crates.io-index" 316 + checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 317 + dependencies = [ 318 + "addr2line", 319 + "cfg-if", 320 + "libc", 321 + "miniz_oxide", 322 + "object", 323 + "rustc-demangle", 324 + "windows-targets 0.52.6", 325 + ] 326 + 327 + [[package]] 328 + name = "base-x" 329 + version = "0.2.11" 330 + source = "registry+https://github.com/rust-lang/crates.io-index" 331 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 332 + 333 + [[package]] 334 + name = "base16ct" 335 + version = "0.2.0" 336 + source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 338 + 339 + [[package]] 340 + name = "base64" 341 + version = "0.22.1" 342 + source = "registry+https://github.com/rust-lang/crates.io-index" 343 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 344 + 345 + [[package]] 346 + name = "base64ct" 347 + version = "1.8.0" 348 + source = "registry+https://github.com/rust-lang/crates.io-index" 349 + checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 350 + 351 + [[package]] 352 + name = "bincode" 353 + version = "2.0.1" 354 + source = "registry+https://github.com/rust-lang/crates.io-index" 355 + checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" 356 + dependencies = [ 357 + "bincode_derive", 358 + "serde", 359 + "unty", 360 + ] 361 + 362 + [[package]] 363 + name = "bincode_derive" 364 + version = "2.0.1" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" 367 + dependencies = [ 368 + "virtue", 369 + ] 370 + 371 + [[package]] 372 + name = "bitflags" 373 + version = "2.9.4" 374 + source = "registry+https://github.com/rust-lang/crates.io-index" 375 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 376 + 377 + [[package]] 378 + name = "block-buffer" 379 + version = "0.10.4" 380 + source = "registry+https://github.com/rust-lang/crates.io-index" 381 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 382 + dependencies = [ 383 + "generic-array", 384 + ] 385 + 386 + [[package]] 387 + name = "bumpalo" 388 + version = "3.19.0" 389 + source = "registry+https://github.com/rust-lang/crates.io-index" 390 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 391 + 392 + [[package]] 393 + name = "bytes" 394 + version = "1.10.1" 395 + source = "registry+https://github.com/rust-lang/crates.io-index" 396 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 397 + 398 + [[package]] 399 + name = "cbor4ii" 400 + version = "0.2.14" 401 + source = "registry+https://github.com/rust-lang/crates.io-index" 402 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 403 + dependencies = [ 404 + "serde", 405 + ] 406 + 407 + [[package]] 408 + name = "cc" 409 + version = "1.2.35" 410 + source = "registry+https://github.com/rust-lang/crates.io-index" 411 + checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" 412 + dependencies = [ 413 + "find-msvc-tools", 414 + "shlex", 415 + ] 416 + 417 + [[package]] 418 + name = "cfg-if" 419 + version = "1.0.3" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 422 + 423 + [[package]] 424 + name = "cfg_aliases" 425 + version = "0.2.1" 426 + source = "registry+https://github.com/rust-lang/crates.io-index" 427 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 428 + 429 + [[package]] 430 + name = "chrono" 431 + version = "0.4.41" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 434 + dependencies = [ 435 + "android-tzdata", 436 + "iana-time-zone", 437 + "js-sys", 438 + "num-traits", 439 + "serde", 440 + "wasm-bindgen", 441 + "windows-link", 442 + ] 443 + 444 + [[package]] 445 + name = "cid" 446 + version = "0.11.1" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 449 + dependencies = [ 450 + "core2", 451 + "multibase", 452 + "multihash", 453 + "serde", 454 + "serde_bytes", 455 + "unsigned-varint", 456 + ] 457 + 458 + [[package]] 459 + name = "clap" 460 + version = "4.5.47" 461 + source = "registry+https://github.com/rust-lang/crates.io-index" 462 + checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 463 + dependencies = [ 464 + "clap_builder", 465 + "clap_derive", 466 + ] 467 + 468 + [[package]] 469 + name = "clap_builder" 470 + version = "4.5.47" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 473 + dependencies = [ 474 + "anstream", 475 + "anstyle", 476 + "clap_lex", 477 + "strsim", 478 + ] 479 + 480 + [[package]] 481 + name = "clap_derive" 482 + version = "4.5.47" 483 + source = "registry+https://github.com/rust-lang/crates.io-index" 484 + checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 485 + dependencies = [ 486 + "heck", 487 + "proc-macro2", 488 + "quote", 489 + "syn", 490 + ] 491 + 492 + [[package]] 493 + name = "clap_lex" 494 + version = "0.7.5" 495 + source = "registry+https://github.com/rust-lang/crates.io-index" 496 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 497 + 498 + [[package]] 499 + name = "colorchoice" 500 + version = "1.0.4" 501 + source = "registry+https://github.com/rust-lang/crates.io-index" 502 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 503 + 504 + [[package]] 505 + name = "combine" 506 + version = "4.6.7" 507 + source = "registry+https://github.com/rust-lang/crates.io-index" 508 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 509 + dependencies = [ 510 + "bytes", 511 + "futures-core", 512 + "memchr", 513 + "pin-project-lite", 514 + "tokio", 515 + "tokio-util", 516 + ] 517 + 518 + [[package]] 519 + name = "const-oid" 520 + version = "0.9.6" 521 + source = "registry+https://github.com/rust-lang/crates.io-index" 522 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 523 + 524 + [[package]] 525 + name = "core-foundation" 526 + version = "0.9.4" 527 + source = "registry+https://github.com/rust-lang/crates.io-index" 528 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 529 + dependencies = [ 530 + "core-foundation-sys", 531 + "libc", 532 + ] 533 + 534 + [[package]] 535 + name = "core-foundation" 536 + version = "0.10.1" 537 + source = "registry+https://github.com/rust-lang/crates.io-index" 538 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 539 + dependencies = [ 540 + "core-foundation-sys", 541 + "libc", 542 + ] 543 + 544 + [[package]] 545 + name = "core-foundation-sys" 546 + version = "0.8.7" 547 + source = "registry+https://github.com/rust-lang/crates.io-index" 548 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 549 + 550 + [[package]] 551 + name = "core2" 552 + version = "0.4.0" 553 + source = "registry+https://github.com/rust-lang/crates.io-index" 554 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 555 + dependencies = [ 556 + "memchr", 557 + ] 558 + 559 + [[package]] 560 + name = "cpufeatures" 561 + version = "0.2.17" 562 + source = "registry+https://github.com/rust-lang/crates.io-index" 563 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 564 + dependencies = [ 565 + "libc", 566 + ] 567 + 568 + [[package]] 569 + name = "critical-section" 570 + version = "1.2.0" 571 + source = "registry+https://github.com/rust-lang/crates.io-index" 572 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 573 + 574 + [[package]] 575 + name = "crossbeam-channel" 576 + version = "0.5.15" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 579 + dependencies = [ 580 + "crossbeam-utils", 581 + ] 582 + 583 + [[package]] 584 + name = "crossbeam-epoch" 585 + version = "0.9.18" 586 + source = "registry+https://github.com/rust-lang/crates.io-index" 587 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 588 + dependencies = [ 589 + "crossbeam-utils", 590 + ] 591 + 592 + [[package]] 593 + name = "crossbeam-utils" 594 + version = "0.8.21" 595 + source = "registry+https://github.com/rust-lang/crates.io-index" 596 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 597 + 598 + [[package]] 599 + name = "crypto-bigint" 600 + version = "0.5.5" 601 + source = "registry+https://github.com/rust-lang/crates.io-index" 602 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 603 + dependencies = [ 604 + "generic-array", 605 + "rand_core 0.6.4", 606 + "subtle", 607 + "zeroize", 608 + ] 609 + 610 + [[package]] 611 + name = "crypto-common" 612 + version = "0.1.6" 613 + source = "registry+https://github.com/rust-lang/crates.io-index" 614 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 615 + dependencies = [ 616 + "generic-array", 617 + "typenum", 618 + ] 619 + 620 + [[package]] 621 + name = "data-encoding" 622 + version = "2.9.0" 623 + source = "registry+https://github.com/rust-lang/crates.io-index" 624 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 625 + 626 + [[package]] 627 + name = "data-encoding-macro" 628 + version = "0.1.18" 629 + source = "registry+https://github.com/rust-lang/crates.io-index" 630 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 631 + dependencies = [ 632 + "data-encoding", 633 + "data-encoding-macro-internal", 634 + ] 635 + 636 + [[package]] 637 + name = "data-encoding-macro-internal" 638 + version = "0.1.16" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 641 + dependencies = [ 642 + "data-encoding", 643 + "syn", 644 + ] 645 + 646 + [[package]] 647 + name = "deadpool" 648 + version = "0.12.3" 649 + source = "registry+https://github.com/rust-lang/crates.io-index" 650 + checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" 651 + dependencies = [ 652 + "deadpool-runtime", 653 + "lazy_static", 654 + "num_cpus", 655 + "tokio", 656 + ] 657 + 658 + [[package]] 659 + name = "deadpool-redis" 660 + version = "0.20.0" 661 + source = "registry+https://github.com/rust-lang/crates.io-index" 662 + checksum = "c136f185b3ca9d1f4e4e19c11570e1002f4bfdd592d589053e225716d613851f" 663 + dependencies = [ 664 + "deadpool", 665 + "redis", 666 + ] 667 + 668 + [[package]] 669 + name = "deadpool-runtime" 670 + version = "0.1.4" 671 + source = "registry+https://github.com/rust-lang/crates.io-index" 672 + checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" 673 + dependencies = [ 674 + "tokio", 675 + ] 676 + 677 + [[package]] 678 + name = "der" 679 + version = "0.7.10" 680 + source = "registry+https://github.com/rust-lang/crates.io-index" 681 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 682 + dependencies = [ 683 + "const-oid", 684 + "pem-rfc7468", 685 + "zeroize", 686 + ] 687 + 688 + [[package]] 689 + name = "digest" 690 + version = "0.10.7" 691 + source = "registry+https://github.com/rust-lang/crates.io-index" 692 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 693 + dependencies = [ 694 + "block-buffer", 695 + "const-oid", 696 + "crypto-common", 697 + "subtle", 698 + ] 699 + 700 + [[package]] 701 + name = "displaydoc" 702 + version = "0.2.5" 703 + source = "registry+https://github.com/rust-lang/crates.io-index" 704 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 705 + dependencies = [ 706 + "proc-macro2", 707 + "quote", 708 + "syn", 709 + ] 710 + 711 + [[package]] 712 + name = "ecdsa" 713 + version = "0.16.9" 714 + source = "registry+https://github.com/rust-lang/crates.io-index" 715 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 716 + dependencies = [ 717 + "der", 718 + "digest", 719 + "elliptic-curve", 720 + "rfc6979", 721 + "serdect", 722 + "signature", 723 + "spki", 724 + ] 725 + 726 + [[package]] 727 + name = "elliptic-curve" 728 + version = "0.13.8" 729 + source = "registry+https://github.com/rust-lang/crates.io-index" 730 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 731 + dependencies = [ 732 + "base16ct", 733 + "base64ct", 734 + "crypto-bigint", 735 + "digest", 736 + "ff", 737 + "generic-array", 738 + "group", 739 + "hkdf", 740 + "pem-rfc7468", 741 + "pkcs8", 742 + "rand_core 0.6.4", 743 + "sec1", 744 + "serde_json", 745 + "serdect", 746 + "subtle", 747 + "zeroize", 748 + ] 749 + 750 + [[package]] 751 + name = "encoding_rs" 752 + version = "0.8.35" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 755 + dependencies = [ 756 + "cfg-if", 757 + ] 758 + 759 + [[package]] 760 + name = "enum-as-inner" 761 + version = "0.6.1" 762 + source = "registry+https://github.com/rust-lang/crates.io-index" 763 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 764 + dependencies = [ 765 + "heck", 766 + "proc-macro2", 767 + "quote", 768 + "syn", 769 + ] 770 + 771 + [[package]] 772 + name = "equivalent" 773 + version = "1.0.2" 774 + source = "registry+https://github.com/rust-lang/crates.io-index" 775 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 776 + 777 + [[package]] 778 + name = "errno" 779 + version = "0.3.13" 780 + source = "registry+https://github.com/rust-lang/crates.io-index" 781 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 782 + dependencies = [ 783 + "libc", 784 + "windows-sys 0.60.2", 785 + ] 786 + 787 + [[package]] 788 + name = "fastrand" 789 + version = "2.3.0" 790 + source = "registry+https://github.com/rust-lang/crates.io-index" 791 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 792 + 793 + [[package]] 794 + name = "ff" 795 + version = "0.13.1" 796 + source = "registry+https://github.com/rust-lang/crates.io-index" 797 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 798 + dependencies = [ 799 + "rand_core 0.6.4", 800 + "subtle", 801 + ] 802 + 803 + [[package]] 804 + name = "find-msvc-tools" 805 + version = "0.1.0" 806 + source = "registry+https://github.com/rust-lang/crates.io-index" 807 + checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" 808 + 809 + [[package]] 810 + name = "fnv" 811 + version = "1.0.7" 812 + source = "registry+https://github.com/rust-lang/crates.io-index" 813 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 814 + 815 + [[package]] 816 + name = "foldhash" 817 + version = "0.1.5" 818 + source = "registry+https://github.com/rust-lang/crates.io-index" 819 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 820 + 821 + [[package]] 822 + name = "foreign-types" 823 + version = "0.3.2" 824 + source = "registry+https://github.com/rust-lang/crates.io-index" 825 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 826 + dependencies = [ 827 + "foreign-types-shared", 828 + ] 829 + 830 + [[package]] 831 + name = "foreign-types-shared" 832 + version = "0.1.1" 833 + source = "registry+https://github.com/rust-lang/crates.io-index" 834 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 835 + 836 + [[package]] 837 + name = "form_urlencoded" 838 + version = "1.2.2" 839 + source = "registry+https://github.com/rust-lang/crates.io-index" 840 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 841 + dependencies = [ 842 + "percent-encoding", 843 + ] 844 + 845 + [[package]] 846 + name = "futures-channel" 847 + version = "0.3.31" 848 + source = "registry+https://github.com/rust-lang/crates.io-index" 849 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 850 + dependencies = [ 851 + "futures-core", 852 + ] 853 + 854 + [[package]] 855 + name = "futures-core" 856 + version = "0.3.31" 857 + source = "registry+https://github.com/rust-lang/crates.io-index" 858 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 859 + 860 + [[package]] 861 + name = "futures-io" 862 + version = "0.3.31" 863 + source = "registry+https://github.com/rust-lang/crates.io-index" 864 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 865 + 866 + [[package]] 867 + name = "futures-macro" 868 + version = "0.3.31" 869 + source = "registry+https://github.com/rust-lang/crates.io-index" 870 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 871 + dependencies = [ 872 + "proc-macro2", 873 + "quote", 874 + "syn", 875 + ] 876 + 877 + [[package]] 878 + name = "futures-sink" 879 + version = "0.3.31" 880 + source = "registry+https://github.com/rust-lang/crates.io-index" 881 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 882 + 883 + [[package]] 884 + name = "futures-task" 885 + version = "0.3.31" 886 + source = "registry+https://github.com/rust-lang/crates.io-index" 887 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 888 + 889 + [[package]] 890 + name = "futures-util" 891 + version = "0.3.31" 892 + source = "registry+https://github.com/rust-lang/crates.io-index" 893 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 894 + dependencies = [ 895 + "futures-core", 896 + "futures-macro", 897 + "futures-sink", 898 + "futures-task", 899 + "pin-project-lite", 900 + "pin-utils", 901 + "slab", 902 + ] 903 + 904 + [[package]] 905 + name = "generator" 906 + version = "0.8.7" 907 + source = "registry+https://github.com/rust-lang/crates.io-index" 908 + checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" 909 + dependencies = [ 910 + "cc", 911 + "cfg-if", 912 + "libc", 913 + "log", 914 + "rustversion", 915 + "windows", 916 + ] 917 + 918 + [[package]] 919 + name = "generic-array" 920 + version = "0.14.7" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 923 + dependencies = [ 924 + "typenum", 925 + "version_check", 926 + "zeroize", 927 + ] 928 + 929 + [[package]] 930 + name = "getrandom" 931 + version = "0.2.16" 932 + source = "registry+https://github.com/rust-lang/crates.io-index" 933 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 934 + dependencies = [ 935 + "cfg-if", 936 + "js-sys", 937 + "libc", 938 + "wasi 0.11.1+wasi-snapshot-preview1", 939 + "wasm-bindgen", 940 + ] 941 + 942 + [[package]] 943 + name = "getrandom" 944 + version = "0.3.3" 945 + source = "registry+https://github.com/rust-lang/crates.io-index" 946 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 947 + dependencies = [ 948 + "cfg-if", 949 + "js-sys", 950 + "libc", 951 + "r-efi", 952 + "wasi 0.14.3+wasi-0.2.4", 953 + "wasm-bindgen", 954 + ] 955 + 956 + [[package]] 957 + name = "gimli" 958 + version = "0.31.1" 959 + source = "registry+https://github.com/rust-lang/crates.io-index" 960 + checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 961 + 962 + [[package]] 963 + name = "group" 964 + version = "0.13.0" 965 + source = "registry+https://github.com/rust-lang/crates.io-index" 966 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 967 + dependencies = [ 968 + "ff", 969 + "rand_core 0.6.4", 970 + "subtle", 971 + ] 972 + 973 + [[package]] 974 + name = "h2" 975 + version = "0.4.12" 976 + source = "registry+https://github.com/rust-lang/crates.io-index" 977 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 978 + dependencies = [ 979 + "atomic-waker", 980 + "bytes", 981 + "fnv", 982 + "futures-core", 983 + "futures-sink", 984 + "http", 985 + "indexmap", 986 + "slab", 987 + "tokio", 988 + "tokio-util", 989 + "tracing", 990 + ] 991 + 992 + [[package]] 993 + name = "hashbrown" 994 + version = "0.15.5" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 997 + dependencies = [ 998 + "allocator-api2", 999 + "equivalent", 1000 + "foldhash", 1001 + ] 1002 + 1003 + [[package]] 1004 + name = "heck" 1005 + version = "0.5.0" 1006 + source = "registry+https://github.com/rust-lang/crates.io-index" 1007 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1008 + 1009 + [[package]] 1010 + name = "hermit-abi" 1011 + version = "0.5.2" 1012 + source = "registry+https://github.com/rust-lang/crates.io-index" 1013 + checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 1014 + 1015 + [[package]] 1016 + name = "hickory-proto" 1017 + version = "0.25.2" 1018 + source = "registry+https://github.com/rust-lang/crates.io-index" 1019 + checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" 1020 + dependencies = [ 1021 + "async-trait", 1022 + "cfg-if", 1023 + "data-encoding", 1024 + "enum-as-inner", 1025 + "futures-channel", 1026 + "futures-io", 1027 + "futures-util", 1028 + "idna", 1029 + "ipnet", 1030 + "once_cell", 1031 + "rand 0.9.2", 1032 + "ring", 1033 + "thiserror 2.0.16", 1034 + "tinyvec", 1035 + "tokio", 1036 + "tracing", 1037 + "url", 1038 + ] 1039 + 1040 + [[package]] 1041 + name = "hickory-resolver" 1042 + version = "0.25.2" 1043 + source = "registry+https://github.com/rust-lang/crates.io-index" 1044 + checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" 1045 + dependencies = [ 1046 + "cfg-if", 1047 + "futures-util", 1048 + "hickory-proto", 1049 + "ipconfig", 1050 + "moka", 1051 + "once_cell", 1052 + "parking_lot", 1053 + "rand 0.9.2", 1054 + "resolv-conf", 1055 + "smallvec", 1056 + "thiserror 2.0.16", 1057 + "tokio", 1058 + "tracing", 1059 + ] 1060 + 1061 + [[package]] 1062 + name = "hkdf" 1063 + version = "0.12.4" 1064 + source = "registry+https://github.com/rust-lang/crates.io-index" 1065 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1066 + dependencies = [ 1067 + "hmac", 1068 + ] 1069 + 1070 + [[package]] 1071 + name = "hmac" 1072 + version = "0.12.1" 1073 + source = "registry+https://github.com/rust-lang/crates.io-index" 1074 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1075 + dependencies = [ 1076 + "digest", 1077 + ] 1078 + 1079 + [[package]] 1080 + name = "http" 1081 + version = "1.3.1" 1082 + source = "registry+https://github.com/rust-lang/crates.io-index" 1083 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1084 + dependencies = [ 1085 + "bytes", 1086 + "fnv", 1087 + "itoa", 1088 + ] 1089 + 1090 + [[package]] 1091 + name = "http-body" 1092 + version = "1.0.1" 1093 + source = "registry+https://github.com/rust-lang/crates.io-index" 1094 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1095 + dependencies = [ 1096 + "bytes", 1097 + "http", 1098 + ] 1099 + 1100 + [[package]] 1101 + name = "http-body-util" 1102 + version = "0.1.3" 1103 + source = "registry+https://github.com/rust-lang/crates.io-index" 1104 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1105 + dependencies = [ 1106 + "bytes", 1107 + "futures-core", 1108 + "http", 1109 + "http-body", 1110 + "pin-project-lite", 1111 + ] 1112 + 1113 + [[package]] 1114 + name = "httparse" 1115 + version = "1.10.1" 1116 + source = "registry+https://github.com/rust-lang/crates.io-index" 1117 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1118 + 1119 + [[package]] 1120 + name = "httpdate" 1121 + version = "1.0.3" 1122 + source = "registry+https://github.com/rust-lang/crates.io-index" 1123 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1124 + 1125 + [[package]] 1126 + name = "hyper" 1127 + version = "1.7.0" 1128 + source = "registry+https://github.com/rust-lang/crates.io-index" 1129 + checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 1130 + dependencies = [ 1131 + "atomic-waker", 1132 + "bytes", 1133 + "futures-channel", 1134 + "futures-core", 1135 + "h2", 1136 + "http", 1137 + "http-body", 1138 + "httparse", 1139 + "httpdate", 1140 + "itoa", 1141 + "pin-project-lite", 1142 + "pin-utils", 1143 + "smallvec", 1144 + "tokio", 1145 + "want", 1146 + ] 1147 + 1148 + [[package]] 1149 + name = "hyper-rustls" 1150 + version = "0.27.7" 1151 + source = "registry+https://github.com/rust-lang/crates.io-index" 1152 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1153 + dependencies = [ 1154 + "http", 1155 + "hyper", 1156 + "hyper-util", 1157 + "rustls", 1158 + "rustls-pki-types", 1159 + "tokio", 1160 + "tokio-rustls", 1161 + "tower-service", 1162 + "webpki-roots", 1163 + ] 1164 + 1165 + [[package]] 1166 + name = "hyper-tls" 1167 + version = "0.6.0" 1168 + source = "registry+https://github.com/rust-lang/crates.io-index" 1169 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1170 + dependencies = [ 1171 + "bytes", 1172 + "http-body-util", 1173 + "hyper", 1174 + "hyper-util", 1175 + "native-tls", 1176 + "tokio", 1177 + "tokio-native-tls", 1178 + "tower-service", 1179 + ] 1180 + 1181 + [[package]] 1182 + name = "hyper-util" 1183 + version = "0.1.16" 1184 + source = "registry+https://github.com/rust-lang/crates.io-index" 1185 + checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 1186 + dependencies = [ 1187 + "base64", 1188 + "bytes", 1189 + "futures-channel", 1190 + "futures-core", 1191 + "futures-util", 1192 + "http", 1193 + "http-body", 1194 + "hyper", 1195 + "ipnet", 1196 + "libc", 1197 + "percent-encoding", 1198 + "pin-project-lite", 1199 + "socket2 0.6.0", 1200 + "system-configuration", 1201 + "tokio", 1202 + "tower-service", 1203 + "tracing", 1204 + "windows-registry", 1205 + ] 1206 + 1207 + [[package]] 1208 + name = "iana-time-zone" 1209 + version = "0.1.63" 1210 + source = "registry+https://github.com/rust-lang/crates.io-index" 1211 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1212 + dependencies = [ 1213 + "android_system_properties", 1214 + "core-foundation-sys", 1215 + "iana-time-zone-haiku", 1216 + "js-sys", 1217 + "log", 1218 + "wasm-bindgen", 1219 + "windows-core", 1220 + ] 1221 + 1222 + [[package]] 1223 + name = "iana-time-zone-haiku" 1224 + version = "0.1.2" 1225 + source = "registry+https://github.com/rust-lang/crates.io-index" 1226 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1227 + dependencies = [ 1228 + "cc", 1229 + ] 1230 + 1231 + [[package]] 1232 + name = "icu_collections" 1233 + version = "2.0.0" 1234 + source = "registry+https://github.com/rust-lang/crates.io-index" 1235 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1236 + dependencies = [ 1237 + "displaydoc", 1238 + "potential_utf", 1239 + "yoke", 1240 + "zerofrom", 1241 + "zerovec", 1242 + ] 1243 + 1244 + [[package]] 1245 + name = "icu_locale_core" 1246 + version = "2.0.0" 1247 + source = "registry+https://github.com/rust-lang/crates.io-index" 1248 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1249 + dependencies = [ 1250 + "displaydoc", 1251 + "litemap", 1252 + "tinystr", 1253 + "writeable", 1254 + "zerovec", 1255 + ] 1256 + 1257 + [[package]] 1258 + name = "icu_normalizer" 1259 + version = "2.0.0" 1260 + source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1262 + dependencies = [ 1263 + "displaydoc", 1264 + "icu_collections", 1265 + "icu_normalizer_data", 1266 + "icu_properties", 1267 + "icu_provider", 1268 + "smallvec", 1269 + "zerovec", 1270 + ] 1271 + 1272 + [[package]] 1273 + name = "icu_normalizer_data" 1274 + version = "2.0.0" 1275 + source = "registry+https://github.com/rust-lang/crates.io-index" 1276 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1277 + 1278 + [[package]] 1279 + name = "icu_properties" 1280 + version = "2.0.1" 1281 + source = "registry+https://github.com/rust-lang/crates.io-index" 1282 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1283 + dependencies = [ 1284 + "displaydoc", 1285 + "icu_collections", 1286 + "icu_locale_core", 1287 + "icu_properties_data", 1288 + "icu_provider", 1289 + "potential_utf", 1290 + "zerotrie", 1291 + "zerovec", 1292 + ] 1293 + 1294 + [[package]] 1295 + name = "icu_properties_data" 1296 + version = "2.0.1" 1297 + source = "registry+https://github.com/rust-lang/crates.io-index" 1298 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1299 + 1300 + [[package]] 1301 + name = "icu_provider" 1302 + version = "2.0.0" 1303 + source = "registry+https://github.com/rust-lang/crates.io-index" 1304 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1305 + dependencies = [ 1306 + "displaydoc", 1307 + "icu_locale_core", 1308 + "stable_deref_trait", 1309 + "tinystr", 1310 + "writeable", 1311 + "yoke", 1312 + "zerofrom", 1313 + "zerotrie", 1314 + "zerovec", 1315 + ] 1316 + 1317 + [[package]] 1318 + name = "idna" 1319 + version = "1.1.0" 1320 + source = "registry+https://github.com/rust-lang/crates.io-index" 1321 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1322 + dependencies = [ 1323 + "idna_adapter", 1324 + "smallvec", 1325 + "utf8_iter", 1326 + ] 1327 + 1328 + [[package]] 1329 + name = "idna_adapter" 1330 + version = "1.2.1" 1331 + source = "registry+https://github.com/rust-lang/crates.io-index" 1332 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1333 + dependencies = [ 1334 + "icu_normalizer", 1335 + "icu_properties", 1336 + ] 1337 + 1338 + [[package]] 1339 + name = "indexmap" 1340 + version = "2.11.0" 1341 + source = "registry+https://github.com/rust-lang/crates.io-index" 1342 + checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" 1343 + dependencies = [ 1344 + "equivalent", 1345 + "hashbrown", 1346 + ] 1347 + 1348 + [[package]] 1349 + name = "io-uring" 1350 + version = "0.7.10" 1351 + source = "registry+https://github.com/rust-lang/crates.io-index" 1352 + checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 1353 + dependencies = [ 1354 + "bitflags", 1355 + "cfg-if", 1356 + "libc", 1357 + ] 1358 + 1359 + [[package]] 1360 + name = "ipconfig" 1361 + version = "0.3.2" 1362 + source = "registry+https://github.com/rust-lang/crates.io-index" 1363 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1364 + dependencies = [ 1365 + "socket2 0.5.10", 1366 + "widestring", 1367 + "windows-sys 0.48.0", 1368 + "winreg", 1369 + ] 1370 + 1371 + [[package]] 1372 + name = "ipld-core" 1373 + version = "0.4.2" 1374 + source = "registry+https://github.com/rust-lang/crates.io-index" 1375 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1376 + dependencies = [ 1377 + "cid", 1378 + "serde", 1379 + "serde_bytes", 1380 + ] 1381 + 1382 + [[package]] 1383 + name = "ipnet" 1384 + version = "2.11.0" 1385 + source = "registry+https://github.com/rust-lang/crates.io-index" 1386 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1387 + 1388 + [[package]] 1389 + name = "iri-string" 1390 + version = "0.7.8" 1391 + source = "registry+https://github.com/rust-lang/crates.io-index" 1392 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1393 + dependencies = [ 1394 + "memchr", 1395 + "serde", 1396 + ] 1397 + 1398 + [[package]] 1399 + name = "is_terminal_polyfill" 1400 + version = "1.70.1" 1401 + source = "registry+https://github.com/rust-lang/crates.io-index" 1402 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1403 + 1404 + [[package]] 1405 + name = "itoa" 1406 + version = "1.0.15" 1407 + source = "registry+https://github.com/rust-lang/crates.io-index" 1408 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1409 + 1410 + [[package]] 1411 + name = "js-sys" 1412 + version = "0.3.78" 1413 + source = "registry+https://github.com/rust-lang/crates.io-index" 1414 + checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" 1415 + dependencies = [ 1416 + "once_cell", 1417 + "wasm-bindgen", 1418 + ] 1419 + 1420 + [[package]] 1421 + name = "k256" 1422 + version = "0.13.4" 1423 + source = "registry+https://github.com/rust-lang/crates.io-index" 1424 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1425 + dependencies = [ 1426 + "cfg-if", 1427 + "ecdsa", 1428 + "elliptic-curve", 1429 + "once_cell", 1430 + "sha2", 1431 + "signature", 1432 + ] 1433 + 1434 + [[package]] 1435 + name = "lazy_static" 1436 + version = "1.5.0" 1437 + source = "registry+https://github.com/rust-lang/crates.io-index" 1438 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1439 + 1440 + [[package]] 1441 + name = "libc" 1442 + version = "0.2.175" 1443 + source = "registry+https://github.com/rust-lang/crates.io-index" 1444 + checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1445 + 1446 + [[package]] 1447 + name = "linux-raw-sys" 1448 + version = "0.9.4" 1449 + source = "registry+https://github.com/rust-lang/crates.io-index" 1450 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1451 + 1452 + [[package]] 1453 + name = "litemap" 1454 + version = "0.8.0" 1455 + source = "registry+https://github.com/rust-lang/crates.io-index" 1456 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1457 + 1458 + [[package]] 1459 + name = "lock_api" 1460 + version = "0.4.13" 1461 + source = "registry+https://github.com/rust-lang/crates.io-index" 1462 + checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1463 + dependencies = [ 1464 + "autocfg", 1465 + "scopeguard", 1466 + ] 1467 + 1468 + [[package]] 1469 + name = "log" 1470 + version = "0.4.28" 1471 + source = "registry+https://github.com/rust-lang/crates.io-index" 1472 + checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1473 + 1474 + [[package]] 1475 + name = "loom" 1476 + version = "0.7.2" 1477 + source = "registry+https://github.com/rust-lang/crates.io-index" 1478 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 1479 + dependencies = [ 1480 + "cfg-if", 1481 + "generator", 1482 + "scoped-tls", 1483 + "tracing", 1484 + "tracing-subscriber", 1485 + ] 1486 + 1487 + [[package]] 1488 + name = "lru" 1489 + version = "0.12.5" 1490 + source = "registry+https://github.com/rust-lang/crates.io-index" 1491 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 1492 + dependencies = [ 1493 + "hashbrown", 1494 + ] 1495 + 1496 + [[package]] 1497 + name = "lru-slab" 1498 + version = "0.1.2" 1499 + source = "registry+https://github.com/rust-lang/crates.io-index" 1500 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1501 + 1502 + [[package]] 1503 + name = "matchers" 1504 + version = "0.2.0" 1505 + source = "registry+https://github.com/rust-lang/crates.io-index" 1506 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 1507 + dependencies = [ 1508 + "regex-automata", 1509 + ] 1510 + 1511 + [[package]] 1512 + name = "matchit" 1513 + version = "0.8.4" 1514 + source = "registry+https://github.com/rust-lang/crates.io-index" 1515 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1516 + 1517 + [[package]] 1518 + name = "memchr" 1519 + version = "2.7.5" 1520 + source = "registry+https://github.com/rust-lang/crates.io-index" 1521 + checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1522 + 1523 + [[package]] 1524 + name = "metrohash" 1525 + version = "1.0.7" 1526 + source = "registry+https://github.com/rust-lang/crates.io-index" 1527 + checksum = "a84011bfadc339f60fbcc38181da8a0a91cd16375394dd52edf9da80deacd8c5" 1528 + 1529 + [[package]] 1530 + name = "mime" 1531 + version = "0.3.17" 1532 + source = "registry+https://github.com/rust-lang/crates.io-index" 1533 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1534 + 1535 + [[package]] 1536 + name = "mime_guess" 1537 + version = "2.0.5" 1538 + source = "registry+https://github.com/rust-lang/crates.io-index" 1539 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 1540 + dependencies = [ 1541 + "mime", 1542 + "unicase", 1543 + ] 1544 + 1545 + [[package]] 1546 + name = "miniz_oxide" 1547 + version = "0.8.9" 1548 + source = "registry+https://github.com/rust-lang/crates.io-index" 1549 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1550 + dependencies = [ 1551 + "adler2", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "mio" 1556 + version = "1.0.4" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1559 + dependencies = [ 1560 + "libc", 1561 + "wasi 0.11.1+wasi-snapshot-preview1", 1562 + "windows-sys 0.59.0", 1563 + ] 1564 + 1565 + [[package]] 1566 + name = "moka" 1567 + version = "0.12.10" 1568 + source = "registry+https://github.com/rust-lang/crates.io-index" 1569 + checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" 1570 + dependencies = [ 1571 + "crossbeam-channel", 1572 + "crossbeam-epoch", 1573 + "crossbeam-utils", 1574 + "loom", 1575 + "parking_lot", 1576 + "portable-atomic", 1577 + "rustc_version", 1578 + "smallvec", 1579 + "tagptr", 1580 + "thiserror 1.0.69", 1581 + "uuid", 1582 + ] 1583 + 1584 + [[package]] 1585 + name = "multibase" 1586 + version = "0.9.1" 1587 + source = "registry+https://github.com/rust-lang/crates.io-index" 1588 + checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 1589 + dependencies = [ 1590 + "base-x", 1591 + "data-encoding", 1592 + "data-encoding-macro", 1593 + ] 1594 + 1595 + [[package]] 1596 + name = "multihash" 1597 + version = "0.19.3" 1598 + source = "registry+https://github.com/rust-lang/crates.io-index" 1599 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1600 + dependencies = [ 1601 + "core2", 1602 + "serde", 1603 + "unsigned-varint", 1604 + ] 1605 + 1606 + [[package]] 1607 + name = "native-tls" 1608 + version = "0.2.14" 1609 + source = "registry+https://github.com/rust-lang/crates.io-index" 1610 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1611 + dependencies = [ 1612 + "libc", 1613 + "log", 1614 + "openssl", 1615 + "openssl-probe", 1616 + "openssl-sys", 1617 + "schannel", 1618 + "security-framework 2.11.1", 1619 + "security-framework-sys", 1620 + "tempfile", 1621 + ] 1622 + 1623 + [[package]] 1624 + name = "nu-ansi-term" 1625 + version = "0.50.1" 1626 + source = "registry+https://github.com/rust-lang/crates.io-index" 1627 + checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 1628 + dependencies = [ 1629 + "windows-sys 0.52.0", 1630 + ] 1631 + 1632 + [[package]] 1633 + name = "num-bigint" 1634 + version = "0.4.6" 1635 + source = "registry+https://github.com/rust-lang/crates.io-index" 1636 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 1637 + dependencies = [ 1638 + "num-integer", 1639 + "num-traits", 1640 + ] 1641 + 1642 + [[package]] 1643 + name = "num-integer" 1644 + version = "0.1.46" 1645 + source = "registry+https://github.com/rust-lang/crates.io-index" 1646 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1647 + dependencies = [ 1648 + "num-traits", 1649 + ] 1650 + 1651 + [[package]] 1652 + name = "num-traits" 1653 + version = "0.2.19" 1654 + source = "registry+https://github.com/rust-lang/crates.io-index" 1655 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1656 + dependencies = [ 1657 + "autocfg", 1658 + ] 1659 + 1660 + [[package]] 1661 + name = "num_cpus" 1662 + version = "1.17.0" 1663 + source = "registry+https://github.com/rust-lang/crates.io-index" 1664 + checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 1665 + dependencies = [ 1666 + "hermit-abi", 1667 + "libc", 1668 + ] 1669 + 1670 + [[package]] 1671 + name = "object" 1672 + version = "0.36.7" 1673 + source = "registry+https://github.com/rust-lang/crates.io-index" 1674 + checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1675 + dependencies = [ 1676 + "memchr", 1677 + ] 1678 + 1679 + [[package]] 1680 + name = "once_cell" 1681 + version = "1.21.3" 1682 + source = "registry+https://github.com/rust-lang/crates.io-index" 1683 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1684 + dependencies = [ 1685 + "critical-section", 1686 + "portable-atomic", 1687 + ] 1688 + 1689 + [[package]] 1690 + name = "once_cell_polyfill" 1691 + version = "1.70.1" 1692 + source = "registry+https://github.com/rust-lang/crates.io-index" 1693 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1694 + 1695 + [[package]] 1696 + name = "openssl" 1697 + version = "0.10.73" 1698 + source = "registry+https://github.com/rust-lang/crates.io-index" 1699 + checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1700 + dependencies = [ 1701 + "bitflags", 1702 + "cfg-if", 1703 + "foreign-types", 1704 + "libc", 1705 + "once_cell", 1706 + "openssl-macros", 1707 + "openssl-sys", 1708 + ] 1709 + 1710 + [[package]] 1711 + name = "openssl-macros" 1712 + version = "0.1.1" 1713 + source = "registry+https://github.com/rust-lang/crates.io-index" 1714 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1715 + dependencies = [ 1716 + "proc-macro2", 1717 + "quote", 1718 + "syn", 1719 + ] 1720 + 1721 + [[package]] 1722 + name = "openssl-probe" 1723 + version = "0.1.6" 1724 + source = "registry+https://github.com/rust-lang/crates.io-index" 1725 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1726 + 1727 + [[package]] 1728 + name = "openssl-sys" 1729 + version = "0.9.109" 1730 + source = "registry+https://github.com/rust-lang/crates.io-index" 1731 + checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1732 + dependencies = [ 1733 + "cc", 1734 + "libc", 1735 + "pkg-config", 1736 + "vcpkg", 1737 + ] 1738 + 1739 + [[package]] 1740 + name = "p256" 1741 + version = "0.13.2" 1742 + source = "registry+https://github.com/rust-lang/crates.io-index" 1743 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 1744 + dependencies = [ 1745 + "ecdsa", 1746 + "elliptic-curve", 1747 + "primeorder", 1748 + "serdect", 1749 + "sha2", 1750 + ] 1751 + 1752 + [[package]] 1753 + name = "p384" 1754 + version = "0.13.1" 1755 + source = "registry+https://github.com/rust-lang/crates.io-index" 1756 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 1757 + dependencies = [ 1758 + "ecdsa", 1759 + "elliptic-curve", 1760 + "primeorder", 1761 + "serdect", 1762 + "sha2", 1763 + ] 1764 + 1765 + [[package]] 1766 + name = "parking_lot" 1767 + version = "0.12.4" 1768 + source = "registry+https://github.com/rust-lang/crates.io-index" 1769 + checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1770 + dependencies = [ 1771 + "lock_api", 1772 + "parking_lot_core", 1773 + ] 1774 + 1775 + [[package]] 1776 + name = "parking_lot_core" 1777 + version = "0.9.11" 1778 + source = "registry+https://github.com/rust-lang/crates.io-index" 1779 + checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1780 + dependencies = [ 1781 + "cfg-if", 1782 + "libc", 1783 + "redox_syscall", 1784 + "smallvec", 1785 + "windows-targets 0.52.6", 1786 + ] 1787 + 1788 + [[package]] 1789 + name = "pem-rfc7468" 1790 + version = "0.7.0" 1791 + source = "registry+https://github.com/rust-lang/crates.io-index" 1792 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1793 + dependencies = [ 1794 + "base64ct", 1795 + ] 1796 + 1797 + [[package]] 1798 + name = "percent-encoding" 1799 + version = "2.3.2" 1800 + source = "registry+https://github.com/rust-lang/crates.io-index" 1801 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1802 + 1803 + [[package]] 1804 + name = "pin-project-lite" 1805 + version = "0.2.16" 1806 + source = "registry+https://github.com/rust-lang/crates.io-index" 1807 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1808 + 1809 + [[package]] 1810 + name = "pin-utils" 1811 + version = "0.1.0" 1812 + source = "registry+https://github.com/rust-lang/crates.io-index" 1813 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1814 + 1815 + [[package]] 1816 + name = "pkcs8" 1817 + version = "0.10.2" 1818 + source = "registry+https://github.com/rust-lang/crates.io-index" 1819 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1820 + dependencies = [ 1821 + "der", 1822 + "spki", 1823 + ] 1824 + 1825 + [[package]] 1826 + name = "pkg-config" 1827 + version = "0.3.32" 1828 + source = "registry+https://github.com/rust-lang/crates.io-index" 1829 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1830 + 1831 + [[package]] 1832 + name = "portable-atomic" 1833 + version = "1.11.1" 1834 + source = "registry+https://github.com/rust-lang/crates.io-index" 1835 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 1836 + 1837 + [[package]] 1838 + name = "potential_utf" 1839 + version = "0.1.3" 1840 + source = "registry+https://github.com/rust-lang/crates.io-index" 1841 + checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 1842 + dependencies = [ 1843 + "zerovec", 1844 + ] 1845 + 1846 + [[package]] 1847 + name = "ppv-lite86" 1848 + version = "0.2.21" 1849 + source = "registry+https://github.com/rust-lang/crates.io-index" 1850 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1851 + dependencies = [ 1852 + "zerocopy", 1853 + ] 1854 + 1855 + [[package]] 1856 + name = "primeorder" 1857 + version = "0.13.6" 1858 + source = "registry+https://github.com/rust-lang/crates.io-index" 1859 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 1860 + dependencies = [ 1861 + "elliptic-curve", 1862 + "serdect", 1863 + ] 1864 + 1865 + [[package]] 1866 + name = "proc-macro2" 1867 + version = "1.0.101" 1868 + source = "registry+https://github.com/rust-lang/crates.io-index" 1869 + checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 1870 + dependencies = [ 1871 + "unicode-ident", 1872 + ] 1873 + 1874 + [[package]] 1875 + name = "quickdid" 1876 + version = "1.0.0-rc.1" 1877 + dependencies = [ 1878 + "anyhow", 1879 + "async-trait", 1880 + "atproto-identity", 1881 + "atproto-xrpcs", 1882 + "axum", 1883 + "bincode", 1884 + "chrono", 1885 + "clap", 1886 + "deadpool", 1887 + "deadpool-redis", 1888 + "http", 1889 + "metrohash", 1890 + "reqwest", 1891 + "serde", 1892 + "serde_json", 1893 + "thiserror 2.0.16", 1894 + "tokio", 1895 + "tokio-util", 1896 + "tracing", 1897 + "tracing-subscriber", 1898 + "uuid", 1899 + ] 1900 + 1901 + [[package]] 1902 + name = "quinn" 1903 + version = "0.11.9" 1904 + source = "registry+https://github.com/rust-lang/crates.io-index" 1905 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 1906 + dependencies = [ 1907 + "bytes", 1908 + "cfg_aliases", 1909 + "pin-project-lite", 1910 + "quinn-proto", 1911 + "quinn-udp", 1912 + "rustc-hash", 1913 + "rustls", 1914 + "socket2 0.6.0", 1915 + "thiserror 2.0.16", 1916 + "tokio", 1917 + "tracing", 1918 + "web-time", 1919 + ] 1920 + 1921 + [[package]] 1922 + name = "quinn-proto" 1923 + version = "0.11.13" 1924 + source = "registry+https://github.com/rust-lang/crates.io-index" 1925 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 1926 + dependencies = [ 1927 + "bytes", 1928 + "getrandom 0.3.3", 1929 + "lru-slab", 1930 + "rand 0.9.2", 1931 + "ring", 1932 + "rustc-hash", 1933 + "rustls", 1934 + "rustls-pki-types", 1935 + "slab", 1936 + "thiserror 2.0.16", 1937 + "tinyvec", 1938 + "tracing", 1939 + "web-time", 1940 + ] 1941 + 1942 + [[package]] 1943 + name = "quinn-udp" 1944 + version = "0.5.14" 1945 + source = "registry+https://github.com/rust-lang/crates.io-index" 1946 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 1947 + dependencies = [ 1948 + "cfg_aliases", 1949 + "libc", 1950 + "once_cell", 1951 + "socket2 0.6.0", 1952 + "tracing", 1953 + "windows-sys 0.60.2", 1954 + ] 1955 + 1956 + [[package]] 1957 + name = "quote" 1958 + version = "1.0.40" 1959 + source = "registry+https://github.com/rust-lang/crates.io-index" 1960 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1961 + dependencies = [ 1962 + "proc-macro2", 1963 + ] 1964 + 1965 + [[package]] 1966 + name = "r-efi" 1967 + version = "5.3.0" 1968 + source = "registry+https://github.com/rust-lang/crates.io-index" 1969 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1970 + 1971 + [[package]] 1972 + name = "rand" 1973 + version = "0.8.5" 1974 + source = "registry+https://github.com/rust-lang/crates.io-index" 1975 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1976 + dependencies = [ 1977 + "libc", 1978 + "rand_chacha 0.3.1", 1979 + "rand_core 0.6.4", 1980 + ] 1981 + 1982 + [[package]] 1983 + name = "rand" 1984 + version = "0.9.2" 1985 + source = "registry+https://github.com/rust-lang/crates.io-index" 1986 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 1987 + dependencies = [ 1988 + "rand_chacha 0.9.0", 1989 + "rand_core 0.9.3", 1990 + ] 1991 + 1992 + [[package]] 1993 + name = "rand_chacha" 1994 + version = "0.3.1" 1995 + source = "registry+https://github.com/rust-lang/crates.io-index" 1996 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1997 + dependencies = [ 1998 + "ppv-lite86", 1999 + "rand_core 0.6.4", 2000 + ] 2001 + 2002 + [[package]] 2003 + name = "rand_chacha" 2004 + version = "0.9.0" 2005 + source = "registry+https://github.com/rust-lang/crates.io-index" 2006 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2007 + dependencies = [ 2008 + "ppv-lite86", 2009 + "rand_core 0.9.3", 2010 + ] 2011 + 2012 + [[package]] 2013 + name = "rand_core" 2014 + version = "0.6.4" 2015 + source = "registry+https://github.com/rust-lang/crates.io-index" 2016 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2017 + dependencies = [ 2018 + "getrandom 0.2.16", 2019 + ] 2020 + 2021 + [[package]] 2022 + name = "rand_core" 2023 + version = "0.9.3" 2024 + source = "registry+https://github.com/rust-lang/crates.io-index" 2025 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2026 + dependencies = [ 2027 + "getrandom 0.3.3", 2028 + ] 2029 + 2030 + [[package]] 2031 + name = "redis" 2032 + version = "0.29.5" 2033 + source = "registry+https://github.com/rust-lang/crates.io-index" 2034 + checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f" 2035 + dependencies = [ 2036 + "arc-swap", 2037 + "backon", 2038 + "bytes", 2039 + "combine", 2040 + "futures-channel", 2041 + "futures-util", 2042 + "itoa", 2043 + "num-bigint", 2044 + "percent-encoding", 2045 + "pin-project-lite", 2046 + "rustls", 2047 + "rustls-native-certs", 2048 + "ryu", 2049 + "sha1_smol", 2050 + "socket2 0.5.10", 2051 + "tokio", 2052 + "tokio-rustls", 2053 + "tokio-util", 2054 + "url", 2055 + ] 2056 + 2057 + [[package]] 2058 + name = "redox_syscall" 2059 + version = "0.5.17" 2060 + source = "registry+https://github.com/rust-lang/crates.io-index" 2061 + checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 2062 + dependencies = [ 2063 + "bitflags", 2064 + ] 2065 + 2066 + [[package]] 2067 + name = "regex-automata" 2068 + version = "0.4.10" 2069 + source = "registry+https://github.com/rust-lang/crates.io-index" 2070 + checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" 2071 + dependencies = [ 2072 + "aho-corasick", 2073 + "memchr", 2074 + "regex-syntax", 2075 + ] 2076 + 2077 + [[package]] 2078 + name = "regex-syntax" 2079 + version = "0.8.6" 2080 + source = "registry+https://github.com/rust-lang/crates.io-index" 2081 + checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 2082 + 2083 + [[package]] 2084 + name = "reqwest" 2085 + version = "0.12.23" 2086 + source = "registry+https://github.com/rust-lang/crates.io-index" 2087 + checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 2088 + dependencies = [ 2089 + "base64", 2090 + "bytes", 2091 + "encoding_rs", 2092 + "futures-core", 2093 + "futures-util", 2094 + "h2", 2095 + "http", 2096 + "http-body", 2097 + "http-body-util", 2098 + "hyper", 2099 + "hyper-rustls", 2100 + "hyper-tls", 2101 + "hyper-util", 2102 + "js-sys", 2103 + "log", 2104 + "mime", 2105 + "mime_guess", 2106 + "native-tls", 2107 + "percent-encoding", 2108 + "pin-project-lite", 2109 + "quinn", 2110 + "rustls", 2111 + "rustls-pki-types", 2112 + "serde", 2113 + "serde_json", 2114 + "serde_urlencoded", 2115 + "sync_wrapper", 2116 + "tokio", 2117 + "tokio-native-tls", 2118 + "tokio-rustls", 2119 + "tower", 2120 + "tower-http", 2121 + "tower-service", 2122 + "url", 2123 + "wasm-bindgen", 2124 + "wasm-bindgen-futures", 2125 + "web-sys", 2126 + "webpki-roots", 2127 + ] 2128 + 2129 + [[package]] 2130 + name = "reqwest-chain" 2131 + version = "1.0.0" 2132 + source = "registry+https://github.com/rust-lang/crates.io-index" 2133 + checksum = "da5c014fb79a8227db44a0433d748107750d2550b7fca55c59a3d7ee7d2ee2b2" 2134 + dependencies = [ 2135 + "anyhow", 2136 + "async-trait", 2137 + "http", 2138 + "reqwest-middleware", 2139 + ] 2140 + 2141 + [[package]] 2142 + name = "reqwest-middleware" 2143 + version = "0.4.2" 2144 + source = "registry+https://github.com/rust-lang/crates.io-index" 2145 + checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" 2146 + dependencies = [ 2147 + "anyhow", 2148 + "async-trait", 2149 + "http", 2150 + "reqwest", 2151 + "serde", 2152 + "thiserror 1.0.69", 2153 + "tower-service", 2154 + ] 2155 + 2156 + [[package]] 2157 + name = "resolv-conf" 2158 + version = "0.7.4" 2159 + source = "registry+https://github.com/rust-lang/crates.io-index" 2160 + checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 2161 + 2162 + [[package]] 2163 + name = "rfc6979" 2164 + version = "0.4.0" 2165 + source = "registry+https://github.com/rust-lang/crates.io-index" 2166 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 2167 + dependencies = [ 2168 + "hmac", 2169 + "subtle", 2170 + ] 2171 + 2172 + [[package]] 2173 + name = "ring" 2174 + version = "0.17.14" 2175 + source = "registry+https://github.com/rust-lang/crates.io-index" 2176 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 2177 + dependencies = [ 2178 + "cc", 2179 + "cfg-if", 2180 + "getrandom 0.2.16", 2181 + "libc", 2182 + "untrusted", 2183 + "windows-sys 0.52.0", 2184 + ] 2185 + 2186 + [[package]] 2187 + name = "rustc-demangle" 2188 + version = "0.1.26" 2189 + source = "registry+https://github.com/rust-lang/crates.io-index" 2190 + checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 2191 + 2192 + [[package]] 2193 + name = "rustc-hash" 2194 + version = "2.1.1" 2195 + source = "registry+https://github.com/rust-lang/crates.io-index" 2196 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2197 + 2198 + [[package]] 2199 + name = "rustc_version" 2200 + version = "0.4.1" 2201 + source = "registry+https://github.com/rust-lang/crates.io-index" 2202 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2203 + dependencies = [ 2204 + "semver", 2205 + ] 2206 + 2207 + [[package]] 2208 + name = "rustix" 2209 + version = "1.0.8" 2210 + source = "registry+https://github.com/rust-lang/crates.io-index" 2211 + checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 2212 + dependencies = [ 2213 + "bitflags", 2214 + "errno", 2215 + "libc", 2216 + "linux-raw-sys", 2217 + "windows-sys 0.60.2", 2218 + ] 2219 + 2220 + [[package]] 2221 + name = "rustls" 2222 + version = "0.23.31" 2223 + source = "registry+https://github.com/rust-lang/crates.io-index" 2224 + checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 2225 + dependencies = [ 2226 + "once_cell", 2227 + "ring", 2228 + "rustls-pki-types", 2229 + "rustls-webpki", 2230 + "subtle", 2231 + "zeroize", 2232 + ] 2233 + 2234 + [[package]] 2235 + name = "rustls-native-certs" 2236 + version = "0.8.1" 2237 + source = "registry+https://github.com/rust-lang/crates.io-index" 2238 + checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 2239 + dependencies = [ 2240 + "openssl-probe", 2241 + "rustls-pki-types", 2242 + "schannel", 2243 + "security-framework 3.3.0", 2244 + ] 2245 + 2246 + [[package]] 2247 + name = "rustls-pki-types" 2248 + version = "1.12.0" 2249 + source = "registry+https://github.com/rust-lang/crates.io-index" 2250 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 2251 + dependencies = [ 2252 + "web-time", 2253 + "zeroize", 2254 + ] 2255 + 2256 + [[package]] 2257 + name = "rustls-webpki" 2258 + version = "0.103.4" 2259 + source = "registry+https://github.com/rust-lang/crates.io-index" 2260 + checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 2261 + dependencies = [ 2262 + "ring", 2263 + "rustls-pki-types", 2264 + "untrusted", 2265 + ] 2266 + 2267 + [[package]] 2268 + name = "rustversion" 2269 + version = "1.0.22" 2270 + source = "registry+https://github.com/rust-lang/crates.io-index" 2271 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 2272 + 2273 + [[package]] 2274 + name = "ryu" 2275 + version = "1.0.20" 2276 + source = "registry+https://github.com/rust-lang/crates.io-index" 2277 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 2278 + 2279 + [[package]] 2280 + name = "schannel" 2281 + version = "0.1.27" 2282 + source = "registry+https://github.com/rust-lang/crates.io-index" 2283 + checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 2284 + dependencies = [ 2285 + "windows-sys 0.59.0", 2286 + ] 2287 + 2288 + [[package]] 2289 + name = "scoped-tls" 2290 + version = "1.0.1" 2291 + source = "registry+https://github.com/rust-lang/crates.io-index" 2292 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 2293 + 2294 + [[package]] 2295 + name = "scopeguard" 2296 + version = "1.2.0" 2297 + source = "registry+https://github.com/rust-lang/crates.io-index" 2298 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2299 + 2300 + [[package]] 2301 + name = "sec1" 2302 + version = "0.7.3" 2303 + source = "registry+https://github.com/rust-lang/crates.io-index" 2304 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2305 + dependencies = [ 2306 + "base16ct", 2307 + "der", 2308 + "generic-array", 2309 + "pkcs8", 2310 + "serdect", 2311 + "subtle", 2312 + "zeroize", 2313 + ] 2314 + 2315 + [[package]] 2316 + name = "security-framework" 2317 + version = "2.11.1" 2318 + source = "registry+https://github.com/rust-lang/crates.io-index" 2319 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2320 + dependencies = [ 2321 + "bitflags", 2322 + "core-foundation 0.9.4", 2323 + "core-foundation-sys", 2324 + "libc", 2325 + "security-framework-sys", 2326 + ] 2327 + 2328 + [[package]] 2329 + name = "security-framework" 2330 + version = "3.3.0" 2331 + source = "registry+https://github.com/rust-lang/crates.io-index" 2332 + checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" 2333 + dependencies = [ 2334 + "bitflags", 2335 + "core-foundation 0.10.1", 2336 + "core-foundation-sys", 2337 + "libc", 2338 + "security-framework-sys", 2339 + ] 2340 + 2341 + [[package]] 2342 + name = "security-framework-sys" 2343 + version = "2.14.0" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 2346 + dependencies = [ 2347 + "core-foundation-sys", 2348 + "libc", 2349 + ] 2350 + 2351 + [[package]] 2352 + name = "semver" 2353 + version = "1.0.26" 2354 + source = "registry+https://github.com/rust-lang/crates.io-index" 2355 + checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 2356 + 2357 + [[package]] 2358 + name = "serde" 2359 + version = "1.0.219" 2360 + source = "registry+https://github.com/rust-lang/crates.io-index" 2361 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2362 + dependencies = [ 2363 + "serde_derive", 2364 + ] 2365 + 2366 + [[package]] 2367 + name = "serde_bytes" 2368 + version = "0.11.17" 2369 + source = "registry+https://github.com/rust-lang/crates.io-index" 2370 + checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 2371 + dependencies = [ 2372 + "serde", 2373 + ] 2374 + 2375 + [[package]] 2376 + name = "serde_derive" 2377 + version = "1.0.219" 2378 + source = "registry+https://github.com/rust-lang/crates.io-index" 2379 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2380 + dependencies = [ 2381 + "proc-macro2", 2382 + "quote", 2383 + "syn", 2384 + ] 2385 + 2386 + [[package]] 2387 + name = "serde_ipld_dagcbor" 2388 + version = "0.6.3" 2389 + source = "registry+https://github.com/rust-lang/crates.io-index" 2390 + checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 2391 + dependencies = [ 2392 + "cbor4ii", 2393 + "ipld-core", 2394 + "scopeguard", 2395 + "serde", 2396 + ] 2397 + 2398 + [[package]] 2399 + name = "serde_json" 2400 + version = "1.0.143" 2401 + source = "registry+https://github.com/rust-lang/crates.io-index" 2402 + checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" 2403 + dependencies = [ 2404 + "itoa", 2405 + "memchr", 2406 + "ryu", 2407 + "serde", 2408 + ] 2409 + 2410 + [[package]] 2411 + name = "serde_path_to_error" 2412 + version = "0.1.17" 2413 + source = "registry+https://github.com/rust-lang/crates.io-index" 2414 + checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 2415 + dependencies = [ 2416 + "itoa", 2417 + "serde", 2418 + ] 2419 + 2420 + [[package]] 2421 + name = "serde_urlencoded" 2422 + version = "0.7.1" 2423 + source = "registry+https://github.com/rust-lang/crates.io-index" 2424 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 2425 + dependencies = [ 2426 + "form_urlencoded", 2427 + "itoa", 2428 + "ryu", 2429 + "serde", 2430 + ] 2431 + 2432 + [[package]] 2433 + name = "serdect" 2434 + version = "0.2.0" 2435 + source = "registry+https://github.com/rust-lang/crates.io-index" 2436 + checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" 2437 + dependencies = [ 2438 + "base16ct", 2439 + "serde", 2440 + ] 2441 + 2442 + [[package]] 2443 + name = "sha1_smol" 2444 + version = "1.0.1" 2445 + source = "registry+https://github.com/rust-lang/crates.io-index" 2446 + checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" 2447 + 2448 + [[package]] 2449 + name = "sha2" 2450 + version = "0.10.9" 2451 + source = "registry+https://github.com/rust-lang/crates.io-index" 2452 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 2453 + dependencies = [ 2454 + "cfg-if", 2455 + "cpufeatures", 2456 + "digest", 2457 + ] 2458 + 2459 + [[package]] 2460 + name = "sharded-slab" 2461 + version = "0.1.7" 2462 + source = "registry+https://github.com/rust-lang/crates.io-index" 2463 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2464 + dependencies = [ 2465 + "lazy_static", 2466 + ] 2467 + 2468 + [[package]] 2469 + name = "shlex" 2470 + version = "1.3.0" 2471 + source = "registry+https://github.com/rust-lang/crates.io-index" 2472 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2473 + 2474 + [[package]] 2475 + name = "signal-hook-registry" 2476 + version = "1.4.6" 2477 + source = "registry+https://github.com/rust-lang/crates.io-index" 2478 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 2479 + dependencies = [ 2480 + "libc", 2481 + ] 2482 + 2483 + [[package]] 2484 + name = "signature" 2485 + version = "2.2.0" 2486 + source = "registry+https://github.com/rust-lang/crates.io-index" 2487 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2488 + dependencies = [ 2489 + "digest", 2490 + "rand_core 0.6.4", 2491 + ] 2492 + 2493 + [[package]] 2494 + name = "slab" 2495 + version = "0.4.11" 2496 + source = "registry+https://github.com/rust-lang/crates.io-index" 2497 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 2498 + 2499 + [[package]] 2500 + name = "smallvec" 2501 + version = "1.15.1" 2502 + source = "registry+https://github.com/rust-lang/crates.io-index" 2503 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2504 + 2505 + [[package]] 2506 + name = "socket2" 2507 + version = "0.5.10" 2508 + source = "registry+https://github.com/rust-lang/crates.io-index" 2509 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 2510 + dependencies = [ 2511 + "libc", 2512 + "windows-sys 0.52.0", 2513 + ] 2514 + 2515 + [[package]] 2516 + name = "socket2" 2517 + version = "0.6.0" 2518 + source = "registry+https://github.com/rust-lang/crates.io-index" 2519 + checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 2520 + dependencies = [ 2521 + "libc", 2522 + "windows-sys 0.59.0", 2523 + ] 2524 + 2525 + [[package]] 2526 + name = "spki" 2527 + version = "0.7.3" 2528 + source = "registry+https://github.com/rust-lang/crates.io-index" 2529 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2530 + dependencies = [ 2531 + "base64ct", 2532 + "der", 2533 + ] 2534 + 2535 + [[package]] 2536 + name = "stable_deref_trait" 2537 + version = "1.2.0" 2538 + source = "registry+https://github.com/rust-lang/crates.io-index" 2539 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2540 + 2541 + [[package]] 2542 + name = "strsim" 2543 + version = "0.11.1" 2544 + source = "registry+https://github.com/rust-lang/crates.io-index" 2545 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2546 + 2547 + [[package]] 2548 + name = "subtle" 2549 + version = "2.6.1" 2550 + source = "registry+https://github.com/rust-lang/crates.io-index" 2551 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2552 + 2553 + [[package]] 2554 + name = "syn" 2555 + version = "2.0.106" 2556 + source = "registry+https://github.com/rust-lang/crates.io-index" 2557 + checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 2558 + dependencies = [ 2559 + "proc-macro2", 2560 + "quote", 2561 + "unicode-ident", 2562 + ] 2563 + 2564 + [[package]] 2565 + name = "sync_wrapper" 2566 + version = "1.0.2" 2567 + source = "registry+https://github.com/rust-lang/crates.io-index" 2568 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2569 + dependencies = [ 2570 + "futures-core", 2571 + ] 2572 + 2573 + [[package]] 2574 + name = "synstructure" 2575 + version = "0.13.2" 2576 + source = "registry+https://github.com/rust-lang/crates.io-index" 2577 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2578 + dependencies = [ 2579 + "proc-macro2", 2580 + "quote", 2581 + "syn", 2582 + ] 2583 + 2584 + [[package]] 2585 + name = "system-configuration" 2586 + version = "0.6.1" 2587 + source = "registry+https://github.com/rust-lang/crates.io-index" 2588 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2589 + dependencies = [ 2590 + "bitflags", 2591 + "core-foundation 0.9.4", 2592 + "system-configuration-sys", 2593 + ] 2594 + 2595 + [[package]] 2596 + name = "system-configuration-sys" 2597 + version = "0.6.0" 2598 + source = "registry+https://github.com/rust-lang/crates.io-index" 2599 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2600 + dependencies = [ 2601 + "core-foundation-sys", 2602 + "libc", 2603 + ] 2604 + 2605 + [[package]] 2606 + name = "tagptr" 2607 + version = "0.2.0" 2608 + source = "registry+https://github.com/rust-lang/crates.io-index" 2609 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 2610 + 2611 + [[package]] 2612 + name = "tempfile" 2613 + version = "3.21.0" 2614 + source = "registry+https://github.com/rust-lang/crates.io-index" 2615 + checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 2616 + dependencies = [ 2617 + "fastrand", 2618 + "getrandom 0.3.3", 2619 + "once_cell", 2620 + "rustix", 2621 + "windows-sys 0.60.2", 2622 + ] 2623 + 2624 + [[package]] 2625 + name = "thiserror" 2626 + version = "1.0.69" 2627 + source = "registry+https://github.com/rust-lang/crates.io-index" 2628 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2629 + dependencies = [ 2630 + "thiserror-impl 1.0.69", 2631 + ] 2632 + 2633 + [[package]] 2634 + name = "thiserror" 2635 + version = "2.0.16" 2636 + source = "registry+https://github.com/rust-lang/crates.io-index" 2637 + checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 2638 + dependencies = [ 2639 + "thiserror-impl 2.0.16", 2640 + ] 2641 + 2642 + [[package]] 2643 + name = "thiserror-impl" 2644 + version = "1.0.69" 2645 + source = "registry+https://github.com/rust-lang/crates.io-index" 2646 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2647 + dependencies = [ 2648 + "proc-macro2", 2649 + "quote", 2650 + "syn", 2651 + ] 2652 + 2653 + [[package]] 2654 + name = "thiserror-impl" 2655 + version = "2.0.16" 2656 + source = "registry+https://github.com/rust-lang/crates.io-index" 2657 + checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 2658 + dependencies = [ 2659 + "proc-macro2", 2660 + "quote", 2661 + "syn", 2662 + ] 2663 + 2664 + [[package]] 2665 + name = "thread_local" 2666 + version = "1.1.9" 2667 + source = "registry+https://github.com/rust-lang/crates.io-index" 2668 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2669 + dependencies = [ 2670 + "cfg-if", 2671 + ] 2672 + 2673 + [[package]] 2674 + name = "tinystr" 2675 + version = "0.8.1" 2676 + source = "registry+https://github.com/rust-lang/crates.io-index" 2677 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2678 + dependencies = [ 2679 + "displaydoc", 2680 + "zerovec", 2681 + ] 2682 + 2683 + [[package]] 2684 + name = "tinyvec" 2685 + version = "1.10.0" 2686 + source = "registry+https://github.com/rust-lang/crates.io-index" 2687 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 2688 + dependencies = [ 2689 + "tinyvec_macros", 2690 + ] 2691 + 2692 + [[package]] 2693 + name = "tinyvec_macros" 2694 + version = "0.1.1" 2695 + source = "registry+https://github.com/rust-lang/crates.io-index" 2696 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2697 + 2698 + [[package]] 2699 + name = "tokio" 2700 + version = "1.47.1" 2701 + source = "registry+https://github.com/rust-lang/crates.io-index" 2702 + checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 2703 + dependencies = [ 2704 + "backtrace", 2705 + "bytes", 2706 + "io-uring", 2707 + "libc", 2708 + "mio", 2709 + "parking_lot", 2710 + "pin-project-lite", 2711 + "signal-hook-registry", 2712 + "slab", 2713 + "socket2 0.6.0", 2714 + "tokio-macros", 2715 + "windows-sys 0.59.0", 2716 + ] 2717 + 2718 + [[package]] 2719 + name = "tokio-macros" 2720 + version = "2.5.0" 2721 + source = "registry+https://github.com/rust-lang/crates.io-index" 2722 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2723 + dependencies = [ 2724 + "proc-macro2", 2725 + "quote", 2726 + "syn", 2727 + ] 2728 + 2729 + [[package]] 2730 + name = "tokio-native-tls" 2731 + version = "0.3.1" 2732 + source = "registry+https://github.com/rust-lang/crates.io-index" 2733 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2734 + dependencies = [ 2735 + "native-tls", 2736 + "tokio", 2737 + ] 2738 + 2739 + [[package]] 2740 + name = "tokio-rustls" 2741 + version = "0.26.2" 2742 + source = "registry+https://github.com/rust-lang/crates.io-index" 2743 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 2744 + dependencies = [ 2745 + "rustls", 2746 + "tokio", 2747 + ] 2748 + 2749 + [[package]] 2750 + name = "tokio-util" 2751 + version = "0.7.16" 2752 + source = "registry+https://github.com/rust-lang/crates.io-index" 2753 + checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 2754 + dependencies = [ 2755 + "bytes", 2756 + "futures-core", 2757 + "futures-sink", 2758 + "futures-util", 2759 + "pin-project-lite", 2760 + "tokio", 2761 + ] 2762 + 2763 + [[package]] 2764 + name = "tower" 2765 + version = "0.5.2" 2766 + source = "registry+https://github.com/rust-lang/crates.io-index" 2767 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2768 + dependencies = [ 2769 + "futures-core", 2770 + "futures-util", 2771 + "pin-project-lite", 2772 + "sync_wrapper", 2773 + "tokio", 2774 + "tower-layer", 2775 + "tower-service", 2776 + "tracing", 2777 + ] 2778 + 2779 + [[package]] 2780 + name = "tower-http" 2781 + version = "0.6.6" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2784 + dependencies = [ 2785 + "bitflags", 2786 + "bytes", 2787 + "futures-util", 2788 + "http", 2789 + "http-body", 2790 + "iri-string", 2791 + "pin-project-lite", 2792 + "tower", 2793 + "tower-layer", 2794 + "tower-service", 2795 + ] 2796 + 2797 + [[package]] 2798 + name = "tower-layer" 2799 + version = "0.3.3" 2800 + source = "registry+https://github.com/rust-lang/crates.io-index" 2801 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2802 + 2803 + [[package]] 2804 + name = "tower-service" 2805 + version = "0.3.3" 2806 + source = "registry+https://github.com/rust-lang/crates.io-index" 2807 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2808 + 2809 + [[package]] 2810 + name = "tracing" 2811 + version = "0.1.41" 2812 + source = "registry+https://github.com/rust-lang/crates.io-index" 2813 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2814 + dependencies = [ 2815 + "log", 2816 + "pin-project-lite", 2817 + "tracing-attributes", 2818 + "tracing-core", 2819 + ] 2820 + 2821 + [[package]] 2822 + name = "tracing-attributes" 2823 + version = "0.1.30" 2824 + source = "registry+https://github.com/rust-lang/crates.io-index" 2825 + checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 2826 + dependencies = [ 2827 + "proc-macro2", 2828 + "quote", 2829 + "syn", 2830 + ] 2831 + 2832 + [[package]] 2833 + name = "tracing-core" 2834 + version = "0.1.34" 2835 + source = "registry+https://github.com/rust-lang/crates.io-index" 2836 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2837 + dependencies = [ 2838 + "once_cell", 2839 + "valuable", 2840 + ] 2841 + 2842 + [[package]] 2843 + name = "tracing-log" 2844 + version = "0.2.0" 2845 + source = "registry+https://github.com/rust-lang/crates.io-index" 2846 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2847 + dependencies = [ 2848 + "log", 2849 + "once_cell", 2850 + "tracing-core", 2851 + ] 2852 + 2853 + [[package]] 2854 + name = "tracing-subscriber" 2855 + version = "0.3.20" 2856 + source = "registry+https://github.com/rust-lang/crates.io-index" 2857 + checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 2858 + dependencies = [ 2859 + "matchers", 2860 + "nu-ansi-term", 2861 + "once_cell", 2862 + "regex-automata", 2863 + "sharded-slab", 2864 + "smallvec", 2865 + "thread_local", 2866 + "tracing", 2867 + "tracing-core", 2868 + "tracing-log", 2869 + ] 2870 + 2871 + [[package]] 2872 + name = "try-lock" 2873 + version = "0.2.5" 2874 + source = "registry+https://github.com/rust-lang/crates.io-index" 2875 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2876 + 2877 + [[package]] 2878 + name = "typenum" 2879 + version = "1.18.0" 2880 + source = "registry+https://github.com/rust-lang/crates.io-index" 2881 + checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2882 + 2883 + [[package]] 2884 + name = "ulid" 2885 + version = "1.2.1" 2886 + source = "registry+https://github.com/rust-lang/crates.io-index" 2887 + checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" 2888 + dependencies = [ 2889 + "rand 0.9.2", 2890 + "web-time", 2891 + ] 2892 + 2893 + [[package]] 2894 + name = "unicase" 2895 + version = "2.8.1" 2896 + source = "registry+https://github.com/rust-lang/crates.io-index" 2897 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 2898 + 2899 + [[package]] 2900 + name = "unicode-ident" 2901 + version = "1.0.18" 2902 + source = "registry+https://github.com/rust-lang/crates.io-index" 2903 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2904 + 2905 + [[package]] 2906 + name = "unsigned-varint" 2907 + version = "0.8.0" 2908 + source = "registry+https://github.com/rust-lang/crates.io-index" 2909 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2910 + 2911 + [[package]] 2912 + name = "untrusted" 2913 + version = "0.9.0" 2914 + source = "registry+https://github.com/rust-lang/crates.io-index" 2915 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2916 + 2917 + [[package]] 2918 + name = "unty" 2919 + version = "0.0.4" 2920 + source = "registry+https://github.com/rust-lang/crates.io-index" 2921 + checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" 2922 + 2923 + [[package]] 2924 + name = "url" 2925 + version = "2.5.7" 2926 + source = "registry+https://github.com/rust-lang/crates.io-index" 2927 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 2928 + dependencies = [ 2929 + "form_urlencoded", 2930 + "idna", 2931 + "percent-encoding", 2932 + "serde", 2933 + ] 2934 + 2935 + [[package]] 2936 + name = "utf8_iter" 2937 + version = "1.0.4" 2938 + source = "registry+https://github.com/rust-lang/crates.io-index" 2939 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2940 + 2941 + [[package]] 2942 + name = "utf8parse" 2943 + version = "0.2.2" 2944 + source = "registry+https://github.com/rust-lang/crates.io-index" 2945 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2946 + 2947 + [[package]] 2948 + name = "uuid" 2949 + version = "1.18.1" 2950 + source = "registry+https://github.com/rust-lang/crates.io-index" 2951 + checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 2952 + dependencies = [ 2953 + "getrandom 0.3.3", 2954 + "js-sys", 2955 + "wasm-bindgen", 2956 + ] 2957 + 2958 + [[package]] 2959 + name = "valuable" 2960 + version = "0.1.1" 2961 + source = "registry+https://github.com/rust-lang/crates.io-index" 2962 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 2963 + 2964 + [[package]] 2965 + name = "vcpkg" 2966 + version = "0.2.15" 2967 + source = "registry+https://github.com/rust-lang/crates.io-index" 2968 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2969 + 2970 + [[package]] 2971 + name = "version_check" 2972 + version = "0.9.5" 2973 + source = "registry+https://github.com/rust-lang/crates.io-index" 2974 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2975 + 2976 + [[package]] 2977 + name = "virtue" 2978 + version = "0.0.18" 2979 + source = "registry+https://github.com/rust-lang/crates.io-index" 2980 + checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" 2981 + 2982 + [[package]] 2983 + name = "want" 2984 + version = "0.3.1" 2985 + source = "registry+https://github.com/rust-lang/crates.io-index" 2986 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2987 + dependencies = [ 2988 + "try-lock", 2989 + ] 2990 + 2991 + [[package]] 2992 + name = "wasi" 2993 + version = "0.11.1+wasi-snapshot-preview1" 2994 + source = "registry+https://github.com/rust-lang/crates.io-index" 2995 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2996 + 2997 + [[package]] 2998 + name = "wasi" 2999 + version = "0.14.3+wasi-0.2.4" 3000 + source = "registry+https://github.com/rust-lang/crates.io-index" 3001 + checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" 3002 + dependencies = [ 3003 + "wit-bindgen", 3004 + ] 3005 + 3006 + [[package]] 3007 + name = "wasm-bindgen" 3008 + version = "0.2.101" 3009 + source = "registry+https://github.com/rust-lang/crates.io-index" 3010 + checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" 3011 + dependencies = [ 3012 + "cfg-if", 3013 + "once_cell", 3014 + "rustversion", 3015 + "wasm-bindgen-macro", 3016 + "wasm-bindgen-shared", 3017 + ] 3018 + 3019 + [[package]] 3020 + name = "wasm-bindgen-backend" 3021 + version = "0.2.101" 3022 + source = "registry+https://github.com/rust-lang/crates.io-index" 3023 + checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" 3024 + dependencies = [ 3025 + "bumpalo", 3026 + "log", 3027 + "proc-macro2", 3028 + "quote", 3029 + "syn", 3030 + "wasm-bindgen-shared", 3031 + ] 3032 + 3033 + [[package]] 3034 + name = "wasm-bindgen-futures" 3035 + version = "0.4.51" 3036 + source = "registry+https://github.com/rust-lang/crates.io-index" 3037 + checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" 3038 + dependencies = [ 3039 + "cfg-if", 3040 + "js-sys", 3041 + "once_cell", 3042 + "wasm-bindgen", 3043 + "web-sys", 3044 + ] 3045 + 3046 + [[package]] 3047 + name = "wasm-bindgen-macro" 3048 + version = "0.2.101" 3049 + source = "registry+https://github.com/rust-lang/crates.io-index" 3050 + checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" 3051 + dependencies = [ 3052 + "quote", 3053 + "wasm-bindgen-macro-support", 3054 + ] 3055 + 3056 + [[package]] 3057 + name = "wasm-bindgen-macro-support" 3058 + version = "0.2.101" 3059 + source = "registry+https://github.com/rust-lang/crates.io-index" 3060 + checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" 3061 + dependencies = [ 3062 + "proc-macro2", 3063 + "quote", 3064 + "syn", 3065 + "wasm-bindgen-backend", 3066 + "wasm-bindgen-shared", 3067 + ] 3068 + 3069 + [[package]] 3070 + name = "wasm-bindgen-shared" 3071 + version = "0.2.101" 3072 + source = "registry+https://github.com/rust-lang/crates.io-index" 3073 + checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" 3074 + dependencies = [ 3075 + "unicode-ident", 3076 + ] 3077 + 3078 + [[package]] 3079 + name = "web-sys" 3080 + version = "0.3.78" 3081 + source = "registry+https://github.com/rust-lang/crates.io-index" 3082 + checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" 3083 + dependencies = [ 3084 + "js-sys", 3085 + "wasm-bindgen", 3086 + ] 3087 + 3088 + [[package]] 3089 + name = "web-time" 3090 + version = "1.1.0" 3091 + source = "registry+https://github.com/rust-lang/crates.io-index" 3092 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 3093 + dependencies = [ 3094 + "js-sys", 3095 + "wasm-bindgen", 3096 + ] 3097 + 3098 + [[package]] 3099 + name = "webpki-roots" 3100 + version = "1.0.2" 3101 + source = "registry+https://github.com/rust-lang/crates.io-index" 3102 + checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 3103 + dependencies = [ 3104 + "rustls-pki-types", 3105 + ] 3106 + 3107 + [[package]] 3108 + name = "widestring" 3109 + version = "1.2.0" 3110 + source = "registry+https://github.com/rust-lang/crates.io-index" 3111 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 3112 + 3113 + [[package]] 3114 + name = "windows" 3115 + version = "0.61.3" 3116 + source = "registry+https://github.com/rust-lang/crates.io-index" 3117 + checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 3118 + dependencies = [ 3119 + "windows-collections", 3120 + "windows-core", 3121 + "windows-future", 3122 + "windows-link", 3123 + "windows-numerics", 3124 + ] 3125 + 3126 + [[package]] 3127 + name = "windows-collections" 3128 + version = "0.2.0" 3129 + source = "registry+https://github.com/rust-lang/crates.io-index" 3130 + checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 3131 + dependencies = [ 3132 + "windows-core", 3133 + ] 3134 + 3135 + [[package]] 3136 + name = "windows-core" 3137 + version = "0.61.2" 3138 + source = "registry+https://github.com/rust-lang/crates.io-index" 3139 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 3140 + dependencies = [ 3141 + "windows-implement", 3142 + "windows-interface", 3143 + "windows-link", 3144 + "windows-result", 3145 + "windows-strings", 3146 + ] 3147 + 3148 + [[package]] 3149 + name = "windows-future" 3150 + version = "0.2.1" 3151 + source = "registry+https://github.com/rust-lang/crates.io-index" 3152 + checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 3153 + dependencies = [ 3154 + "windows-core", 3155 + "windows-link", 3156 + "windows-threading", 3157 + ] 3158 + 3159 + [[package]] 3160 + name = "windows-implement" 3161 + version = "0.60.0" 3162 + source = "registry+https://github.com/rust-lang/crates.io-index" 3163 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 3164 + dependencies = [ 3165 + "proc-macro2", 3166 + "quote", 3167 + "syn", 3168 + ] 3169 + 3170 + [[package]] 3171 + name = "windows-interface" 3172 + version = "0.59.1" 3173 + source = "registry+https://github.com/rust-lang/crates.io-index" 3174 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 3175 + dependencies = [ 3176 + "proc-macro2", 3177 + "quote", 3178 + "syn", 3179 + ] 3180 + 3181 + [[package]] 3182 + name = "windows-link" 3183 + version = "0.1.3" 3184 + source = "registry+https://github.com/rust-lang/crates.io-index" 3185 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3186 + 3187 + [[package]] 3188 + name = "windows-numerics" 3189 + version = "0.2.0" 3190 + source = "registry+https://github.com/rust-lang/crates.io-index" 3191 + checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 3192 + dependencies = [ 3193 + "windows-core", 3194 + "windows-link", 3195 + ] 3196 + 3197 + [[package]] 3198 + name = "windows-registry" 3199 + version = "0.5.3" 3200 + source = "registry+https://github.com/rust-lang/crates.io-index" 3201 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 3202 + dependencies = [ 3203 + "windows-link", 3204 + "windows-result", 3205 + "windows-strings", 3206 + ] 3207 + 3208 + [[package]] 3209 + name = "windows-result" 3210 + version = "0.3.4" 3211 + source = "registry+https://github.com/rust-lang/crates.io-index" 3212 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 3213 + dependencies = [ 3214 + "windows-link", 3215 + ] 3216 + 3217 + [[package]] 3218 + name = "windows-strings" 3219 + version = "0.4.2" 3220 + source = "registry+https://github.com/rust-lang/crates.io-index" 3221 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3222 + dependencies = [ 3223 + "windows-link", 3224 + ] 3225 + 3226 + [[package]] 3227 + name = "windows-sys" 3228 + version = "0.48.0" 3229 + source = "registry+https://github.com/rust-lang/crates.io-index" 3230 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3231 + dependencies = [ 3232 + "windows-targets 0.48.5", 3233 + ] 3234 + 3235 + [[package]] 3236 + name = "windows-sys" 3237 + version = "0.52.0" 3238 + source = "registry+https://github.com/rust-lang/crates.io-index" 3239 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3240 + dependencies = [ 3241 + "windows-targets 0.52.6", 3242 + ] 3243 + 3244 + [[package]] 3245 + name = "windows-sys" 3246 + version = "0.59.0" 3247 + source = "registry+https://github.com/rust-lang/crates.io-index" 3248 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3249 + dependencies = [ 3250 + "windows-targets 0.52.6", 3251 + ] 3252 + 3253 + [[package]] 3254 + name = "windows-sys" 3255 + version = "0.60.2" 3256 + source = "registry+https://github.com/rust-lang/crates.io-index" 3257 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3258 + dependencies = [ 3259 + "windows-targets 0.53.3", 3260 + ] 3261 + 3262 + [[package]] 3263 + name = "windows-targets" 3264 + version = "0.48.5" 3265 + source = "registry+https://github.com/rust-lang/crates.io-index" 3266 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3267 + dependencies = [ 3268 + "windows_aarch64_gnullvm 0.48.5", 3269 + "windows_aarch64_msvc 0.48.5", 3270 + "windows_i686_gnu 0.48.5", 3271 + "windows_i686_msvc 0.48.5", 3272 + "windows_x86_64_gnu 0.48.5", 3273 + "windows_x86_64_gnullvm 0.48.5", 3274 + "windows_x86_64_msvc 0.48.5", 3275 + ] 3276 + 3277 + [[package]] 3278 + name = "windows-targets" 3279 + version = "0.52.6" 3280 + source = "registry+https://github.com/rust-lang/crates.io-index" 3281 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3282 + dependencies = [ 3283 + "windows_aarch64_gnullvm 0.52.6", 3284 + "windows_aarch64_msvc 0.52.6", 3285 + "windows_i686_gnu 0.52.6", 3286 + "windows_i686_gnullvm 0.52.6", 3287 + "windows_i686_msvc 0.52.6", 3288 + "windows_x86_64_gnu 0.52.6", 3289 + "windows_x86_64_gnullvm 0.52.6", 3290 + "windows_x86_64_msvc 0.52.6", 3291 + ] 3292 + 3293 + [[package]] 3294 + name = "windows-targets" 3295 + version = "0.53.3" 3296 + source = "registry+https://github.com/rust-lang/crates.io-index" 3297 + checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 3298 + dependencies = [ 3299 + "windows-link", 3300 + "windows_aarch64_gnullvm 0.53.0", 3301 + "windows_aarch64_msvc 0.53.0", 3302 + "windows_i686_gnu 0.53.0", 3303 + "windows_i686_gnullvm 0.53.0", 3304 + "windows_i686_msvc 0.53.0", 3305 + "windows_x86_64_gnu 0.53.0", 3306 + "windows_x86_64_gnullvm 0.53.0", 3307 + "windows_x86_64_msvc 0.53.0", 3308 + ] 3309 + 3310 + [[package]] 3311 + name = "windows-threading" 3312 + version = "0.1.0" 3313 + source = "registry+https://github.com/rust-lang/crates.io-index" 3314 + checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 3315 + dependencies = [ 3316 + "windows-link", 3317 + ] 3318 + 3319 + [[package]] 3320 + name = "windows_aarch64_gnullvm" 3321 + version = "0.48.5" 3322 + source = "registry+https://github.com/rust-lang/crates.io-index" 3323 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3324 + 3325 + [[package]] 3326 + name = "windows_aarch64_gnullvm" 3327 + version = "0.52.6" 3328 + source = "registry+https://github.com/rust-lang/crates.io-index" 3329 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3330 + 3331 + [[package]] 3332 + name = "windows_aarch64_gnullvm" 3333 + version = "0.53.0" 3334 + source = "registry+https://github.com/rust-lang/crates.io-index" 3335 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 3336 + 3337 + [[package]] 3338 + name = "windows_aarch64_msvc" 3339 + version = "0.48.5" 3340 + source = "registry+https://github.com/rust-lang/crates.io-index" 3341 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3342 + 3343 + [[package]] 3344 + name = "windows_aarch64_msvc" 3345 + version = "0.52.6" 3346 + source = "registry+https://github.com/rust-lang/crates.io-index" 3347 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3348 + 3349 + [[package]] 3350 + name = "windows_aarch64_msvc" 3351 + version = "0.53.0" 3352 + source = "registry+https://github.com/rust-lang/crates.io-index" 3353 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 3354 + 3355 + [[package]] 3356 + name = "windows_i686_gnu" 3357 + version = "0.48.5" 3358 + source = "registry+https://github.com/rust-lang/crates.io-index" 3359 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3360 + 3361 + [[package]] 3362 + name = "windows_i686_gnu" 3363 + version = "0.52.6" 3364 + source = "registry+https://github.com/rust-lang/crates.io-index" 3365 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3366 + 3367 + [[package]] 3368 + name = "windows_i686_gnu" 3369 + version = "0.53.0" 3370 + source = "registry+https://github.com/rust-lang/crates.io-index" 3371 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 3372 + 3373 + [[package]] 3374 + name = "windows_i686_gnullvm" 3375 + version = "0.52.6" 3376 + source = "registry+https://github.com/rust-lang/crates.io-index" 3377 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 3378 + 3379 + [[package]] 3380 + name = "windows_i686_gnullvm" 3381 + version = "0.53.0" 3382 + source = "registry+https://github.com/rust-lang/crates.io-index" 3383 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 3384 + 3385 + [[package]] 3386 + name = "windows_i686_msvc" 3387 + version = "0.48.5" 3388 + source = "registry+https://github.com/rust-lang/crates.io-index" 3389 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3390 + 3391 + [[package]] 3392 + name = "windows_i686_msvc" 3393 + version = "0.52.6" 3394 + source = "registry+https://github.com/rust-lang/crates.io-index" 3395 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3396 + 3397 + [[package]] 3398 + name = "windows_i686_msvc" 3399 + version = "0.53.0" 3400 + source = "registry+https://github.com/rust-lang/crates.io-index" 3401 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 3402 + 3403 + [[package]] 3404 + name = "windows_x86_64_gnu" 3405 + version = "0.48.5" 3406 + source = "registry+https://github.com/rust-lang/crates.io-index" 3407 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3408 + 3409 + [[package]] 3410 + name = "windows_x86_64_gnu" 3411 + version = "0.52.6" 3412 + source = "registry+https://github.com/rust-lang/crates.io-index" 3413 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3414 + 3415 + [[package]] 3416 + name = "windows_x86_64_gnu" 3417 + version = "0.53.0" 3418 + source = "registry+https://github.com/rust-lang/crates.io-index" 3419 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 3420 + 3421 + [[package]] 3422 + name = "windows_x86_64_gnullvm" 3423 + version = "0.48.5" 3424 + source = "registry+https://github.com/rust-lang/crates.io-index" 3425 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3426 + 3427 + [[package]] 3428 + name = "windows_x86_64_gnullvm" 3429 + version = "0.52.6" 3430 + source = "registry+https://github.com/rust-lang/crates.io-index" 3431 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3432 + 3433 + [[package]] 3434 + name = "windows_x86_64_gnullvm" 3435 + version = "0.53.0" 3436 + source = "registry+https://github.com/rust-lang/crates.io-index" 3437 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 3438 + 3439 + [[package]] 3440 + name = "windows_x86_64_msvc" 3441 + version = "0.48.5" 3442 + source = "registry+https://github.com/rust-lang/crates.io-index" 3443 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3444 + 3445 + [[package]] 3446 + name = "windows_x86_64_msvc" 3447 + version = "0.52.6" 3448 + source = "registry+https://github.com/rust-lang/crates.io-index" 3449 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3450 + 3451 + [[package]] 3452 + name = "windows_x86_64_msvc" 3453 + version = "0.53.0" 3454 + source = "registry+https://github.com/rust-lang/crates.io-index" 3455 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 3456 + 3457 + [[package]] 3458 + name = "winreg" 3459 + version = "0.50.0" 3460 + source = "registry+https://github.com/rust-lang/crates.io-index" 3461 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 3462 + dependencies = [ 3463 + "cfg-if", 3464 + "windows-sys 0.48.0", 3465 + ] 3466 + 3467 + [[package]] 3468 + name = "wit-bindgen" 3469 + version = "0.45.0" 3470 + source = "registry+https://github.com/rust-lang/crates.io-index" 3471 + checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" 3472 + 3473 + [[package]] 3474 + name = "writeable" 3475 + version = "0.6.1" 3476 + source = "registry+https://github.com/rust-lang/crates.io-index" 3477 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 3478 + 3479 + [[package]] 3480 + name = "yoke" 3481 + version = "0.8.0" 3482 + source = "registry+https://github.com/rust-lang/crates.io-index" 3483 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 3484 + dependencies = [ 3485 + "serde", 3486 + "stable_deref_trait", 3487 + "yoke-derive", 3488 + "zerofrom", 3489 + ] 3490 + 3491 + [[package]] 3492 + name = "yoke-derive" 3493 + version = "0.8.0" 3494 + source = "registry+https://github.com/rust-lang/crates.io-index" 3495 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 3496 + dependencies = [ 3497 + "proc-macro2", 3498 + "quote", 3499 + "syn", 3500 + "synstructure", 3501 + ] 3502 + 3503 + [[package]] 3504 + name = "zerocopy" 3505 + version = "0.8.26" 3506 + source = "registry+https://github.com/rust-lang/crates.io-index" 3507 + checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 3508 + dependencies = [ 3509 + "zerocopy-derive", 3510 + ] 3511 + 3512 + [[package]] 3513 + name = "zerocopy-derive" 3514 + version = "0.8.26" 3515 + source = "registry+https://github.com/rust-lang/crates.io-index" 3516 + checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 3517 + dependencies = [ 3518 + "proc-macro2", 3519 + "quote", 3520 + "syn", 3521 + ] 3522 + 3523 + [[package]] 3524 + name = "zerofrom" 3525 + version = "0.1.6" 3526 + source = "registry+https://github.com/rust-lang/crates.io-index" 3527 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 3528 + dependencies = [ 3529 + "zerofrom-derive", 3530 + ] 3531 + 3532 + [[package]] 3533 + name = "zerofrom-derive" 3534 + version = "0.1.6" 3535 + source = "registry+https://github.com/rust-lang/crates.io-index" 3536 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 3537 + dependencies = [ 3538 + "proc-macro2", 3539 + "quote", 3540 + "syn", 3541 + "synstructure", 3542 + ] 3543 + 3544 + [[package]] 3545 + name = "zeroize" 3546 + version = "1.8.1" 3547 + source = "registry+https://github.com/rust-lang/crates.io-index" 3548 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 3549 + 3550 + [[package]] 3551 + name = "zerotrie" 3552 + version = "0.2.2" 3553 + source = "registry+https://github.com/rust-lang/crates.io-index" 3554 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 3555 + dependencies = [ 3556 + "displaydoc", 3557 + "yoke", 3558 + "zerofrom", 3559 + ] 3560 + 3561 + [[package]] 3562 + name = "zerovec" 3563 + version = "0.11.4" 3564 + source = "registry+https://github.com/rust-lang/crates.io-index" 3565 + checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 3566 + dependencies = [ 3567 + "yoke", 3568 + "zerofrom", 3569 + "zerovec-derive", 3570 + ] 3571 + 3572 + [[package]] 3573 + name = "zerovec-derive" 3574 + version = "0.11.1" 3575 + source = "registry+https://github.com/rust-lang/crates.io-index" 3576 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 3577 + dependencies = [ 3578 + "proc-macro2", 3579 + "quote", 3580 + "syn", 3581 + ]
+37
Cargo.toml
··· 1 + [package] 2 + name = "quickdid" 3 + version = "1.0.0-rc.1" 4 + edition = "2024" 5 + authors = ["Nick Gerakines <nick.gerakines@gmail.com>"] 6 + description = "A fast and scalable com.atproto.identity.resolveHandle service" 7 + repository = "https://tangled.sh/@smokesignal.events/quickdid" 8 + license = "MIT" 9 + publish = false 10 + default-run = "quickdid" 11 + 12 + [[bin]] 13 + name = "quickdid" 14 + path = "src/bin/quickdid.rs" 15 + 16 + [dependencies] 17 + anyhow = "1.0" 18 + async-trait = "0.1" 19 + atproto-identity = { version = "0.11.3" } 20 + atproto-xrpcs = { version = "0.11.3" } 21 + axum = { version = "0.8", features = ["macros"] } 22 + bincode = { version = "2.0.1", features = ["serde"] } 23 + chrono = "0.4" 24 + clap = { version = "4", features = ["derive", "env"] } 25 + deadpool = "0.12.2" 26 + deadpool-redis = { version = "0.20.0", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp", "script"] } 27 + http = "1.0" 28 + metrohash = "1.0.7" 29 + reqwest = { version = "0.12", features = ["json"] } 30 + serde = { version = "1.0", features = ["derive"] } 31 + serde_json = "1.0" 32 + thiserror = "2.0" 33 + tokio = { version = "1.35", features = ["full"] } 34 + tokio-util = { version = "0.7", features = ["rt"] } 35 + tracing = "0.1" 36 + tracing-subscriber = { version = "0.3", features = ["env-filter"] } 37 + uuid = { version = "1.11", features = ["v4"] }
+34
Dockerfile
··· 1 + # syntax=docker/dockerfile:1.4 2 + FROM rust:1.89-slim AS builder 3 + 4 + RUN apt-get update && apt-get install -y \ 5 + pkg-config \ 6 + libssl-dev \ 7 + && rm -rf /var/lib/apt/lists/* 8 + 9 + WORKDIR /app 10 + COPY Cargo.lock Cargo.toml ./ 11 + 12 + ARG GIT_HASH=0 13 + ENV GIT_HASH=$GIT_HASH 14 + 15 + COPY src ./src 16 + RUN cargo build --bin quickdid --release 17 + 18 + FROM gcr.io/distroless/cc-debian12 19 + 20 + LABEL org.opencontainers.image.title="quickdid" 21 + LABEL org.opencontainers.image.description="A fast and scalable com.atproto.identity.resolveHandle service" 22 + LABEL org.opencontainers.image.licenses="MIT" 23 + LABEL org.opencontainers.image.authors="Nick Gerakines <nick.gerakines@gmail.com>" 24 + LABEL org.opencontainers.image.source="https://tangled.sh/@smokesignal.events/quickdid" 25 + LABEL org.opencontainers.image.version="1.0.0-rc.1" 26 + 27 + WORKDIR /app 28 + COPY --from=builder /app/target/release/quickdid /app/quickdid 29 + 30 + ENV HTTP_PORT=8080 31 + ENV RUST_LOG=info 32 + ENV RUST_BACKTRACE=full 33 + 34 + ENTRYPOINT ["/app/quickdid"]
+111
README.md
··· 1 + # QuickDID 2 + 3 + QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides blazing-fast handle-to-DID resolution with intelligent caching strategies, supporting both in-memory and Redis-backed persistent caching with binary serialization for optimal storage efficiency. 4 + 5 + ## Features 6 + 7 + - **Fast Handle Resolution**: Resolves AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints 8 + - **Multi-Layer Caching**: In-memory caching with configurable TTL and Redis-backed persistent caching (90-day TTL) 9 + - **Binary Serialization**: Compact storage format reduces cache size by ~40% compared to JSON 10 + - **Queue Processing**: Asynchronous handle resolution with support for MPSC and Redis queue adapters 11 + - **AT Protocol Compatible**: Implements XRPC endpoints for seamless integration with AT Protocol infrastructure 12 + - **Production Ready**: Comprehensive error handling, health checks, and graceful shutdown support 13 + 14 + ## Building 15 + 16 + ### Prerequisites 17 + 18 + - Rust 1.70 or later 19 + - Redis (optional, for persistent caching and distributed queuing) 20 + 21 + ### Build Commands 22 + 23 + ```bash 24 + # Clone the repository 25 + git clone https://github.com/yourusername/quickdid.git 26 + cd quickdid 27 + 28 + # Build the project 29 + cargo build --release 30 + 31 + # Run tests 32 + cargo test 33 + 34 + # Run with debug logging 35 + RUST_LOG=debug cargo run 36 + ``` 37 + 38 + ## Minimum Configuration 39 + 40 + QuickDID requires the following environment variables to run: 41 + 42 + ### Required 43 + 44 + - `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`) 45 + - `SERVICE_KEY`: Private key for service identity in DID format (e.g., `did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK`) 46 + 47 + ### Example Minimal Setup 48 + 49 + ```bash 50 + HTTP_EXTERNAL=localhost:3007 \ 51 + SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \ 52 + cargo run 53 + ``` 54 + 55 + This will start QuickDID with: 56 + - HTTP server on port 8080 (default) 57 + - In-memory caching only 58 + - MPSC queue adapter for async processing 59 + - Connection to plc.directory for DID resolution 60 + 61 + ### Optional Configuration 62 + 63 + For production deployments, consider these additional environment variables: 64 + 65 + - `HTTP_PORT`: Server port (default: 8080) 66 + - `REDIS_URL`: Redis connection URL for persistent caching (e.g., `redis://localhost:6379`) 67 + - `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc) 68 + - `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory) 69 + - `RUST_LOG`: Logging level (e.g., debug, info, warn, error) 70 + 71 + ### Production Example 72 + 73 + ```bash 74 + HTTP_EXTERNAL=quickdid.example.com \ 75 + SERVICE_KEY=did:key:yourkeyhere \ 76 + HTTP_PORT=3000 \ 77 + REDIS_URL=redis://localhost:6379 \ 78 + QUEUE_ADAPTER=redis \ 79 + RUST_LOG=info \ 80 + ./target/release/quickdid 81 + ``` 82 + 83 + ## API Endpoints 84 + 85 + - `GET /_health` - Health check endpoint 86 + - `GET /xrpc/com.atproto.identity.resolveHandle` - Resolve handle to DID 87 + - `GET /.well-known/atproto-did` - Serve DID document for the service 88 + 89 + ## License 90 + 91 + This project is open source and available under the MIT License. 92 + 93 + Copyright (c) 2025 Nick Gerakines 94 + 95 + Permission is hereby granted, free of charge, to any person obtaining a copy 96 + of this software and associated documentation files (the "Software"), to deal 97 + in the Software without restriction, including without limitation the rights 98 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 + copies of the Software, and to permit persons to whom the Software is 100 + furnished to do so, subject to the following conditions: 101 + 102 + The above copyright notice and this permission notice shall be included in all 103 + copies or substantial portions of the Software. 104 + 105 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 111 + SOFTWARE.
+138
docs/redis-handle-resolver.md
··· 1 + # Redis-Backed Handle Resolver 2 + 3 + The QuickDID service now supports Redis-backed caching for handle resolution, providing persistent caching with 90-day expiration times. 4 + 5 + ## Features 6 + 7 + - **90-Day Cache TTL**: Resolved handles are cached in Redis for 90 days, significantly reducing resolution latency 8 + - **Error Caching**: Failed resolutions are also cached (as empty strings) to prevent repeated failed lookups 9 + - **Automatic Fallback**: If Redis is unavailable, the service falls back to in-memory caching 10 + - **Configurable Key Prefix**: Supports custom Redis key prefixes for multi-tenant deployments 11 + 12 + ## Configuration 13 + 14 + Set the `REDIS_URL` environment variable to enable Redis caching: 15 + 16 + ```bash 17 + export REDIS_URL="redis://localhost:6379" 18 + # or with authentication 19 + export REDIS_URL="redis://username:password@localhost:6379/0" 20 + ``` 21 + 22 + When Redis is configured: 23 + - Handle resolutions are cached with 90-day expiration 24 + - Cache keys use the format: `handle:{handle_name}` 25 + - Failed resolutions are cached as empty strings 26 + 27 + When Redis is not configured: 28 + - Falls back to in-memory caching with 10-minute TTL 29 + - Cache is not persisted between service restarts 30 + 31 + ## Architecture 32 + 33 + ``` 34 + ┌─────────────────┐ 35 + │ HTTP Request │ 36 + └────────┬────────┘ 37 + 38 + 39 + ┌─────────────────┐ 40 + │ Handle Resolver │ 41 + │ Endpoint │ 42 + └────────┬────────┘ 43 + 44 + 45 + ┌─────────────────────┐ Cache Miss ┌───────────────────┐ 46 + │ RedisHandleResolver │ ─────────────────► │ BaseHandleResolver│ 47 + │ │ │ │ 48 + │ - Check Redis │ │ - DNS TXT lookup │ 49 + │ - 90-day TTL │ ◄───────────────── │ - Well-known │ 50 + │ - Cache result │ Resolution │ endpoint │ 51 + └─────────┬───────────┘ └───────────────────┘ 52 + 53 + │ Cache Hit 54 + 55 + ┌─────────────────┐ 56 + │ Redis Cache │ 57 + │ │ 58 + │ handle:alice -> │ 59 + │ did:plc:xyz... │ 60 + └─────────────────┘ 61 + ``` 62 + 63 + ## Implementation Details 64 + 65 + ### RedisHandleResolver 66 + 67 + The `RedisHandleResolver` struct wraps a base handle resolver and adds Redis caching: 68 + 69 + ```rust 70 + pub struct RedisHandleResolver { 71 + inner: Arc<dyn HandleResolver>, 72 + pool: RedisPool, 73 + key_prefix: String, 74 + } 75 + ``` 76 + 77 + ### Cache Behavior 78 + 79 + 1. **Cache Hit**: Returns DID immediately from Redis 80 + 2. **Cache Miss**: Resolves through inner resolver, caches result 81 + 3. **Resolution Error**: Caches empty string to prevent repeated failures 82 + 4. **Redis Error**: Falls back to inner resolver without caching 83 + 84 + ### Key Format 85 + 86 + Redis keys follow this pattern: 87 + - Success: `handle:alice.bsky.social` → `did:plc:xyz123...` 88 + - Error: `handle:invalid.handle` → `""` (empty string) 89 + 90 + ## Testing 91 + 92 + Tests can be run with a local Redis instance: 93 + 94 + ```bash 95 + # Start Redis locally 96 + docker run -d -p 6379:6379 redis:latest 97 + 98 + # Run tests with Redis 99 + export TEST_REDIS_URL="redis://localhost:6379" 100 + cargo test 101 + 102 + # Tests will skip if TEST_REDIS_URL is not set 103 + cargo test 104 + ``` 105 + 106 + ## Performance Considerations 107 + 108 + - **Cache Hits**: Near-instantaneous (<1ms typical) 109 + - **Cache Misses**: Same as base resolver (DNS/HTTP lookups) 110 + - **Memory Usage**: Minimal, only stores handle → DID mappings 111 + - **Network Overhead**: Single Redis round-trip per resolution 112 + 113 + ## Monitoring 114 + 115 + The service logs cache operations at various levels: 116 + 117 + - `DEBUG`: Cache hits/misses 118 + - `INFO`: Redis pool creation and resolver selection 119 + - `WARN`: Redis connection failures and fallbacks 120 + 121 + Example logs: 122 + ``` 123 + INFO Using Redis-backed handle resolver with 90-day cache TTL 124 + DEBUG Cache miss for handle alice.bsky.social, resolving... 125 + DEBUG Caching successful resolution for handle alice.bsky.social: did:plc:xyz123 126 + DEBUG Cache hit for handle alice.bsky.social: did:plc:xyz123 127 + ``` 128 + 129 + ## Migration 130 + 131 + To migrate from in-memory to Redis caching: 132 + 133 + 1. Deploy Redis instance 134 + 2. Set `REDIS_URL` environment variable 135 + 3. Restart QuickDID service 136 + 4. Service will automatically use Redis for new resolutions 137 + 138 + No data migration needed - cache will populate on first resolution.
+368
src/bin/quickdid.rs
··· 1 + use anyhow::Result; 2 + use async_trait::async_trait; 3 + use atproto_identity::{ 4 + config::{CertificateBundles, DnsNameservers}, 5 + key::{identify_key, to_public, KeyData, KeyProvider}, 6 + resolve::HickoryDnsResolver, 7 + }; 8 + use clap::Parser; 9 + use quickdid::{ 10 + cache::create_redis_pool, 11 + config::{Args, Config}, 12 + handle_resolver::{BaseHandleResolver, CachingHandleResolver, RedisHandleResolver}, 13 + handle_resolver_task::{HandleResolverTask, HandleResolverTaskConfig}, 14 + http::{create_router, server::AppContext, server::InnerAppContext}, 15 + queue_adapter::{ 16 + HandleResolutionWork, MpscQueueAdapter, NoopQueueAdapter, QueueAdapter, RedisQueueAdapter, 17 + }, 18 + task_manager::spawn_cancellable_task, 19 + }; 20 + use serde_json::json; 21 + use std::{collections::HashMap, sync::Arc}; 22 + use tokio::signal; 23 + use tokio_util::{sync::CancellationToken, task::TaskTracker}; 24 + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 25 + 26 + #[derive(Clone)] 27 + pub struct SimpleKeyProvider { 28 + keys: HashMap<String, KeyData>, 29 + } 30 + 31 + impl SimpleKeyProvider { 32 + pub fn new() -> Self { 33 + Self { 34 + keys: HashMap::new(), 35 + } 36 + } 37 + } 38 + 39 + #[async_trait] 40 + impl KeyProvider for SimpleKeyProvider { 41 + async fn get_private_key_by_id(&self, key_id: &str) -> anyhow::Result<Option<KeyData>> { 42 + Ok(self.keys.get(key_id).cloned()) 43 + } 44 + } 45 + 46 + #[tokio::main] 47 + async fn main() -> Result<()> { 48 + // Initialize tracing 49 + tracing_subscriber::registry() 50 + .with( 51 + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { 52 + "quickdid=info,atproto_identity=debug,atproto_xrpcs=debug".into() 53 + }), 54 + ) 55 + .with(tracing_subscriber::fmt::layer()) 56 + .init(); 57 + 58 + let args = Args::parse(); 59 + let config = Config::from_args(args)?; 60 + 61 + tracing::info!("Starting QuickDID service on port {}", config.http_port); 62 + tracing::info!("Service DID: {}", config.service_did); 63 + 64 + // Parse certificate bundles if provided 65 + let certificate_bundles: CertificateBundles = config 66 + .certificate_bundles 67 + .clone() 68 + .unwrap_or_default() 69 + .try_into()?; 70 + 71 + // Parse DNS nameservers if provided 72 + let dns_nameservers: DnsNameservers = config 73 + .dns_nameservers 74 + .clone() 75 + .unwrap_or_default() 76 + .try_into()?; 77 + 78 + // Build HTTP client 79 + let mut client_builder = reqwest::Client::builder(); 80 + for ca_certificate in certificate_bundles.as_ref() { 81 + let cert = std::fs::read(ca_certificate)?; 82 + let cert = reqwest::Certificate::from_pem(&cert)?; 83 + client_builder = client_builder.add_root_certificate(cert); 84 + } 85 + client_builder = client_builder.user_agent(&config.user_agent); 86 + let http_client = client_builder.build()?; 87 + 88 + // Create DNS resolver 89 + let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref()); 90 + 91 + // Process service key 92 + let private_service_key_data = identify_key(&config.service_key)?; 93 + let public_service_key_data = to_public(&private_service_key_data)?; 94 + let public_service_key = public_service_key_data.to_string(); 95 + 96 + // Create service DID document 97 + let service_document = json!({ 98 + "@context": vec!["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], 99 + "id": config.service_did.clone(), 100 + "verificationMethod": [{ 101 + "id": format!("{}#atproto", config.service_did), 102 + "type": "Multikey", 103 + "controller": config.service_did.clone(), 104 + "publicKeyMultibase": public_service_key 105 + }], 106 + "service": [] 107 + }); 108 + 109 + // Create DNS resolver Arc for sharing 110 + let dns_resolver_arc = Arc::new(dns_resolver); 111 + 112 + // Create base handle resolver 113 + let base_handle_resolver = Arc::new(BaseHandleResolver { 114 + dns_resolver: dns_resolver_arc.clone(), 115 + http_client: http_client.clone(), 116 + plc_hostname: config.plc_hostname.clone(), 117 + }); 118 + 119 + // Create Redis pool if configured 120 + let redis_pool = if let Some(redis_url) = &config.redis_url { 121 + match create_redis_pool(redis_url) { 122 + Ok(pool) => { 123 + tracing::info!("Redis pool created for handle resolver cache"); 124 + Some(pool) 125 + } 126 + Err(e) => { 127 + tracing::warn!("Failed to create Redis pool for handle resolver: {}", e); 128 + None 129 + } 130 + } 131 + } else { 132 + None 133 + }; 134 + 135 + // Create handle resolver with Redis caching if available, otherwise use in-memory caching 136 + let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> = 137 + if let Some(pool) = redis_pool { 138 + tracing::info!("Using Redis-backed handle resolver with 90-day cache TTL"); 139 + Arc::new(RedisHandleResolver::new(base_handle_resolver, pool)) 140 + } else { 141 + tracing::info!("Using in-memory handle resolver with 10-minute cache TTL"); 142 + Arc::new(CachingHandleResolver::new( 143 + base_handle_resolver, 144 + 600, // 10 minutes TTL for in-memory cache 145 + )) 146 + }; 147 + 148 + // Create task tracker and cancellation token 149 + let tracker = TaskTracker::new(); 150 + let token = CancellationToken::new(); 151 + 152 + // Setup background handle resolution task and get the queue adapter 153 + let handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>> = { 154 + // Create queue adapter based on configuration 155 + let adapter: Arc<dyn QueueAdapter<HandleResolutionWork>> = match config 156 + .queue_adapter 157 + .as_str() 158 + { 159 + "redis" => { 160 + // Use queue-specific Redis URL, fall back to general Redis URL 161 + let queue_redis_url = config 162 + .queue_redis_url 163 + .as_ref() 164 + .or(config.redis_url.as_ref()); 165 + 166 + match queue_redis_url { 167 + Some(url) => match create_redis_pool(url) { 168 + Ok(pool) => { 169 + tracing::info!( 170 + "Creating Redis queue adapter with prefix: {}", 171 + config.queue_redis_prefix 172 + ); 173 + Arc::new(RedisQueueAdapter::<HandleResolutionWork>::with_config( 174 + pool, 175 + config.queue_worker_id.clone(), 176 + config.queue_redis_prefix.clone(), 177 + 5, // 5 second timeout for blocking operations 178 + )) 179 + } 180 + Err(e) => { 181 + tracing::error!("Failed to create Redis pool for queue adapter: {}", e); 182 + tracing::warn!("Falling back to MPSC queue adapter"); 183 + // Fall back to MPSC if Redis fails 184 + let (handle_sender, handle_receiver) = 185 + tokio::sync::mpsc::channel::<HandleResolutionWork>( 186 + config.queue_buffer_size, 187 + ); 188 + Arc::new(MpscQueueAdapter::from_channel( 189 + handle_sender, 190 + handle_receiver, 191 + )) 192 + } 193 + }, 194 + None => { 195 + tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter"); 196 + Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new()) 197 + } 198 + } 199 + } 200 + "mpsc" => { 201 + // Use MPSC adapter 202 + tracing::info!( 203 + "Using MPSC queue adapter with buffer size: {}", 204 + config.queue_buffer_size 205 + ); 206 + let (handle_sender, handle_receiver) = 207 + tokio::sync::mpsc::channel::<HandleResolutionWork>(config.queue_buffer_size); 208 + Arc::new(MpscQueueAdapter::from_channel( 209 + handle_sender, 210 + handle_receiver, 211 + )) 212 + } 213 + "noop" | "none" => { 214 + // Use no-op adapter 215 + tracing::info!("Using no-op queue adapter (queuing disabled)"); 216 + Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new()) 217 + } 218 + _ => { 219 + // Default to no-op adapter for unknown types 220 + tracing::warn!( 221 + "Unknown queue adapter type '{}', using no-op adapter", 222 + config.queue_adapter 223 + ); 224 + Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new()) 225 + } 226 + }; 227 + 228 + // Keep a reference to the adapter for the AppContext 229 + let adapter_for_context = adapter.clone(); 230 + 231 + // Only spawn handle resolver task if not using noop adapter 232 + if !matches!(config.queue_adapter.as_str(), "noop" | "none") { 233 + // Create handle resolver task configuration 234 + let handle_task_config = HandleResolverTaskConfig { 235 + default_timeout_ms: 10000, 236 + }; 237 + 238 + // Create and start handle resolver task 239 + let handle_task = HandleResolverTask::with_config( 240 + adapter, 241 + handle_resolver.clone(), 242 + token.clone(), 243 + handle_task_config, 244 + ); 245 + 246 + // Spawn the handle resolver task 247 + spawn_cancellable_task( 248 + &tracker, 249 + token.clone(), 250 + "handle_resolver", 251 + |cancel_token| async move { 252 + tokio::select! { 253 + result = handle_task.run() => { 254 + if let Err(e) = result { 255 + tracing::error!(error = ?e, "Handle resolver task failed"); 256 + Err(anyhow::anyhow!(e)) 257 + } else { 258 + Ok(()) 259 + } 260 + } 261 + _ = cancel_token.cancelled() => { 262 + tracing::info!("Handle resolver task cancelled"); 263 + Ok(()) 264 + } 265 + } 266 + }, 267 + ); 268 + 269 + tracing::info!( 270 + "Background handle resolution task started with {} adapter", 271 + config.queue_adapter 272 + ); 273 + } else { 274 + tracing::info!("Background handle resolution task disabled (using no-op adapter)"); 275 + } 276 + 277 + // Return the adapter to be used in AppContext 278 + adapter_for_context 279 + }; 280 + 281 + // Create app context with the queue adapter 282 + let app_context = AppContext(Arc::new(InnerAppContext { 283 + http_client: http_client.clone(), 284 + service_document, 285 + service_did: config.service_did.clone(), 286 + handle_resolver: handle_resolver.clone(), 287 + handle_queue, 288 + })); 289 + 290 + // Create router 291 + let router = create_router(app_context); 292 + 293 + // Setup signal handler 294 + { 295 + let signal_tracker = tracker.clone(); 296 + let signal_token = token.clone(); 297 + 298 + // Spawn signal handler without using the managed task helper since it's special 299 + tracing::info!("Starting signal handler task"); 300 + tokio::spawn(async move { 301 + let ctrl_c = async { 302 + signal::ctrl_c() 303 + .await 304 + .expect("failed to install Ctrl+C handler"); 305 + }; 306 + 307 + #[cfg(unix)] 308 + let terminate = async { 309 + signal::unix::signal(signal::unix::SignalKind::terminate()) 310 + .expect("failed to install signal handler") 311 + .recv() 312 + .await; 313 + }; 314 + 315 + #[cfg(not(unix))] 316 + let terminate = std::future::pending::<()>(); 317 + 318 + tokio::select! { 319 + () = signal_token.cancelled() => { 320 + tracing::info!("Signal handler task shutting down gracefully"); 321 + }, 322 + _ = terminate => { 323 + tracing::info!("Received SIGTERM signal, initiating shutdown"); 324 + }, 325 + _ = ctrl_c => { 326 + tracing::info!("Received Ctrl+C signal, initiating shutdown"); 327 + }, 328 + } 329 + 330 + signal_tracker.close(); 331 + signal_token.cancel(); 332 + tracing::info!("Signal handler task completed"); 333 + }); 334 + } 335 + 336 + // Start HTTP server with cancellation support 337 + let bind_address = format!("0.0.0.0:{}", config.http_port); 338 + spawn_cancellable_task( 339 + &tracker, 340 + token.clone(), 341 + "http", 342 + move |cancel_token| async move { 343 + let listener = tokio::net::TcpListener::bind(&bind_address) 344 + .await 345 + .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", bind_address, e))?; 346 + 347 + tracing::info!("QuickDID service listening on {}", bind_address); 348 + 349 + let shutdown_token = cancel_token.clone(); 350 + axum::serve(listener, router) 351 + .with_graceful_shutdown(async move { 352 + shutdown_token.cancelled().await; 353 + }) 354 + .await 355 + .map_err(|e| anyhow::anyhow!("HTTP server error: {}", e))?; 356 + 357 + Ok(()) 358 + }, 359 + ); 360 + 361 + // Wait for all tasks to complete 362 + tracing::info!("Waiting for all tasks to complete..."); 363 + tracker.wait().await; 364 + 365 + tracing::info!("All tasks completed, application shutting down"); 366 + 367 + Ok(()) 368 + }
+11
src/cache.rs
··· 1 + //! Redis cache utilities for QuickDID 2 + 3 + use anyhow::Result; 4 + use deadpool_redis::{Config, Pool, Runtime}; 5 + 6 + /// Create a Redis connection pool from a Redis URL 7 + pub fn create_redis_pool(redis_url: &str) -> Result<Pool> { 8 + let config = Config::from_url(redis_url); 9 + let pool = config.create_pool(Some(Runtime::Tokio1))?; 10 + Ok(pool) 11 + }
+199
src/config.rs
··· 1 + use anyhow::Result; 2 + use atproto_identity::config::optional_env; 3 + use clap::Parser; 4 + 5 + #[derive(Parser, Clone)] 6 + #[command( 7 + name = "quickdid", 8 + about = "QuickDID - AT Protocol Identity Resolver Service", 9 + long_about = " 10 + A fast identity resolution service for the AT Protocol ecosystem. 11 + This service provides identity resolution endpoints and handle resolution 12 + capabilities with in-memory caching. 13 + 14 + FEATURES: 15 + - AT Protocol identity resolution and DID document management 16 + - Handle resolution with in-memory caching 17 + - DID:web identity publishing via .well-known endpoints 18 + - Health check endpoint 19 + 20 + ENVIRONMENT VARIABLES: 21 + SERVICE_KEY Private key for service identity (required) 22 + HTTP_EXTERNAL External hostname for service endpoints (required) 23 + HTTP_PORT HTTP server port (default: 8080) 24 + PLC_HOSTNAME PLC directory hostname (default: plc.directory) 25 + USER_AGENT HTTP User-Agent header (auto-generated) 26 + DNS_NAMESERVERS Custom DNS nameservers (optional) 27 + CERTIFICATE_BUNDLES Additional CA certificates (optional) 28 + REDIS_URL Redis URL for handle resolution caching (optional) 29 + QUEUE_ADAPTER Queue adapter type: 'mpsc' or 'redis' (default: mpsc) 30 + QUEUE_REDIS_URL Redis URL for queue adapter (uses REDIS_URL if not set) 31 + QUEUE_REDIS_PREFIX Redis key prefix for queues (default: queue:handleresolver:) 32 + QUEUE_WORKER_ID Worker ID for Redis queue (random UUID if not set) 33 + QUEUE_BUFFER_SIZE Buffer size for MPSC queue (default: 1000) 34 + " 35 + )] 36 + pub struct Args { 37 + #[arg(long, env = "HTTP_PORT", default_value = "8080")] 38 + pub http_port: String, 39 + 40 + #[arg(long, env = "PLC_HOSTNAME", default_value = "plc.directory")] 41 + pub plc_hostname: String, 42 + 43 + #[arg(long, env = "HTTP_EXTERNAL")] 44 + pub http_external: Option<String>, 45 + 46 + #[arg(long, env = "SERVICE_KEY")] 47 + pub service_key: Option<String>, 48 + 49 + #[arg(long, env = "USER_AGENT")] 50 + pub user_agent: Option<String>, 51 + 52 + #[arg(long, env = "DNS_NAMESERVERS")] 53 + pub dns_nameservers: Option<String>, 54 + 55 + #[arg(long, env = "CERTIFICATE_BUNDLES")] 56 + pub certificate_bundles: Option<String>, 57 + 58 + #[arg(long, env = "REDIS_URL")] 59 + pub redis_url: Option<String>, 60 + 61 + #[arg(long, env = "QUEUE_ADAPTER", default_value = "mpsc")] 62 + pub queue_adapter: String, 63 + 64 + #[arg(long, env = "QUEUE_REDIS_URL")] 65 + pub queue_redis_url: Option<String>, 66 + 67 + #[arg( 68 + long, 69 + env = "QUEUE_REDIS_PREFIX", 70 + default_value = "queue:handleresolver:" 71 + )] 72 + pub queue_redis_prefix: String, 73 + 74 + #[arg(long, env = "QUEUE_WORKER_ID")] 75 + pub queue_worker_id: Option<String>, 76 + 77 + #[arg(long, env = "QUEUE_BUFFER_SIZE", default_value = "1000")] 78 + pub queue_buffer_size: usize, 79 + } 80 + 81 + #[derive(Clone)] 82 + pub struct Config { 83 + pub http_port: String, 84 + pub plc_hostname: String, 85 + pub http_external: String, 86 + pub service_key: String, 87 + pub user_agent: String, 88 + pub service_did: String, 89 + pub dns_nameservers: Option<String>, 90 + pub certificate_bundles: Option<String>, 91 + pub redis_url: Option<String>, 92 + pub queue_adapter: String, 93 + pub queue_redis_url: Option<String>, 94 + pub queue_redis_prefix: String, 95 + pub queue_worker_id: Option<String>, 96 + pub queue_buffer_size: usize, 97 + } 98 + 99 + impl Config { 100 + pub fn from_args(args: Args) -> Result<Self> { 101 + let http_external = args 102 + .http_external 103 + .or_else(|| { 104 + let env_val = optional_env("HTTP_EXTERNAL"); 105 + if env_val.is_empty() { 106 + None 107 + } else { 108 + Some(env_val) 109 + } 110 + }) 111 + .ok_or_else(|| anyhow::anyhow!("HTTP_EXTERNAL is required"))?; 112 + 113 + let service_key = args 114 + .service_key 115 + .or_else(|| { 116 + let env_val = optional_env("SERVICE_KEY"); 117 + if env_val.is_empty() { 118 + None 119 + } else { 120 + Some(env_val) 121 + } 122 + }) 123 + .ok_or_else(|| anyhow::anyhow!("SERVICE_KEY is required"))?; 124 + 125 + let default_user_agent = 126 + format!("quickdid/0.1.0 (+https://github.com/smokesignal.events/quickdid)"); 127 + 128 + let user_agent = args 129 + .user_agent 130 + .or_else(|| { 131 + let env_val = optional_env("USER_AGENT"); 132 + if env_val.is_empty() { 133 + None 134 + } else { 135 + Some(env_val) 136 + } 137 + }) 138 + .unwrap_or(default_user_agent); 139 + 140 + let service_did = if http_external.contains(':') { 141 + let encoded_external = http_external.replace(':', "%3A"); 142 + format!("did:web:{}", encoded_external) 143 + } else { 144 + format!("did:web:{}", http_external) 145 + }; 146 + 147 + Ok(Config { 148 + http_port: args.http_port, 149 + plc_hostname: args.plc_hostname, 150 + http_external, 151 + service_key, 152 + user_agent, 153 + service_did, 154 + dns_nameservers: args.dns_nameservers.or_else(|| { 155 + let env_val = optional_env("DNS_NAMESERVERS"); 156 + if env_val.is_empty() { 157 + None 158 + } else { 159 + Some(env_val) 160 + } 161 + }), 162 + certificate_bundles: args.certificate_bundles.or_else(|| { 163 + let env_val = optional_env("CERTIFICATE_BUNDLES"); 164 + if env_val.is_empty() { 165 + None 166 + } else { 167 + Some(env_val) 168 + } 169 + }), 170 + redis_url: args.redis_url.or_else(|| { 171 + let env_val = optional_env("REDIS_URL"); 172 + if env_val.is_empty() { 173 + None 174 + } else { 175 + Some(env_val) 176 + } 177 + }), 178 + queue_adapter: args.queue_adapter, 179 + queue_redis_url: args.queue_redis_url.or_else(|| { 180 + let env_val = optional_env("QUEUE_REDIS_URL"); 181 + if env_val.is_empty() { 182 + None 183 + } else { 184 + Some(env_val) 185 + } 186 + }), 187 + queue_redis_prefix: args.queue_redis_prefix, 188 + queue_worker_id: args.queue_worker_id.or_else(|| { 189 + let env_val = optional_env("QUEUE_WORKER_ID"); 190 + if env_val.is_empty() { 191 + None 192 + } else { 193 + Some(env_val) 194 + } 195 + }), 196 + queue_buffer_size: args.queue_buffer_size, 197 + }) 198 + } 199 + }
+272
src/handle_resolution_result.rs
··· 1 + //! Optimized structure for storing handle resolution results. 2 + //! 3 + //! This module provides a compact binary representation of handle resolution 4 + //! results using bincode serialization. The structure efficiently stores 5 + //! the resolution timestamp, DID method type, and the DID payload. 6 + 7 + use serde::{Deserialize, Serialize}; 8 + use std::time::{SystemTime, UNIX_EPOCH}; 9 + 10 + /// Represents the type of DID method from a resolution 11 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 12 + #[repr(i16)] 13 + pub enum DidMethodType { 14 + /// Handle was not resolved 15 + NotResolved = 0, 16 + /// DID uses the web method (did:web:...) 17 + Web = 1, 18 + /// DID uses the PLC method (did:plc:...) 19 + Plc = 2, 20 + /// DID uses another method 21 + Other = 3, 22 + } 23 + 24 + /// Optimized structure for storing handle resolution results 25 + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 26 + pub struct HandleResolutionResult { 27 + /// Unix timestamp (seconds since epoch) when the handle was resolved 28 + pub timestamp: u64, 29 + /// The type of DID method (stored as 4-byte integer) 30 + pub method_type: DidMethodType, 31 + /// The DID payload (without prefix for web/plc methods) 32 + pub payload: String, 33 + } 34 + 35 + impl HandleResolutionResult { 36 + /// Create a new resolution result for a successfully resolved handle 37 + pub fn success(did: &str) -> Self { 38 + let timestamp = SystemTime::now() 39 + .duration_since(UNIX_EPOCH) 40 + .expect("Time went backwards") 41 + .as_secs(); 42 + 43 + let (method_type, payload) = Self::parse_did(did); 44 + 45 + Self { 46 + timestamp, 47 + method_type, 48 + payload, 49 + } 50 + } 51 + 52 + /// Create a new resolution result for a failed resolution 53 + pub fn not_resolved() -> Self { 54 + let timestamp = SystemTime::now() 55 + .duration_since(UNIX_EPOCH) 56 + .expect("Time went backwards") 57 + .as_secs(); 58 + 59 + Self { 60 + timestamp, 61 + method_type: DidMethodType::NotResolved, 62 + payload: String::new(), 63 + } 64 + } 65 + 66 + /// Create a resolution result with a specific timestamp (for testing or replay) 67 + pub fn with_timestamp(did: Option<&str>, timestamp: u64) -> Self { 68 + match did { 69 + Some(did) => { 70 + let (method_type, payload) = Self::parse_did(did); 71 + Self { 72 + timestamp, 73 + method_type, 74 + payload, 75 + } 76 + } 77 + None => Self { 78 + timestamp, 79 + method_type: DidMethodType::NotResolved, 80 + payload: String::new(), 81 + }, 82 + } 83 + } 84 + 85 + /// Parse a DID string to extract method type and payload 86 + fn parse_did(did: &str) -> (DidMethodType, String) { 87 + if let Some(stripped) = did.strip_prefix("did:web:") { 88 + (DidMethodType::Web, stripped.to_string()) 89 + } else if let Some(stripped) = did.strip_prefix("did:plc:") { 90 + (DidMethodType::Plc, stripped.to_string()) 91 + } else if did.starts_with("did:") { 92 + // Other DID method - store the full DID 93 + (DidMethodType::Other, did.to_string()) 94 + } else { 95 + // Not a valid DID format - treat as other 96 + (DidMethodType::Other, did.to_string()) 97 + } 98 + } 99 + 100 + /// Reconstruct the full DID from the stored data 101 + pub fn to_did(&self) -> Option<String> { 102 + match self.method_type { 103 + DidMethodType::NotResolved => None, 104 + DidMethodType::Web => Some(format!("did:web:{}", self.payload)), 105 + DidMethodType::Plc => Some(format!("did:plc:{}", self.payload)), 106 + DidMethodType::Other => Some(self.payload.clone()), 107 + } 108 + } 109 + 110 + /// Serialize the result to bytes using bincode 111 + pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> { 112 + bincode::serde::encode_to_vec(self, bincode::config::standard()) 113 + } 114 + 115 + /// Deserialize from bytes using bincode 116 + pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> { 117 + bincode::serde::decode_from_slice(bytes, bincode::config::standard()) 118 + .map(|(result, _)| result) 119 + } 120 + } 121 + 122 + #[cfg(test)] 123 + mod tests { 124 + use super::*; 125 + 126 + #[test] 127 + fn test_parse_did_web() { 128 + let did = "did:web:example.com"; 129 + let result = HandleResolutionResult::success(did); 130 + assert_eq!(result.method_type, DidMethodType::Web); 131 + assert_eq!(result.payload, "example.com"); 132 + assert_eq!(result.to_did(), Some(did.to_string())); 133 + } 134 + 135 + #[test] 136 + fn test_parse_did_plc() { 137 + let did = "did:plc:abcdef123456"; 138 + let result = HandleResolutionResult::success(did); 139 + assert_eq!(result.method_type, DidMethodType::Plc); 140 + assert_eq!(result.payload, "abcdef123456"); 141 + assert_eq!(result.to_did(), Some(did.to_string())); 142 + } 143 + 144 + #[test] 145 + fn test_parse_did_other() { 146 + let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; 147 + let result = HandleResolutionResult::success(did); 148 + assert_eq!(result.method_type, DidMethodType::Other); 149 + assert_eq!(result.payload, did); 150 + assert_eq!(result.to_did(), Some(did.to_string())); 151 + } 152 + 153 + #[test] 154 + fn test_not_resolved() { 155 + let result = HandleResolutionResult::not_resolved(); 156 + assert_eq!(result.method_type, DidMethodType::NotResolved); 157 + assert_eq!(result.payload, ""); 158 + assert_eq!(result.to_did(), None); 159 + } 160 + 161 + #[test] 162 + fn test_serialization_deserialization() { 163 + // Test did:web 164 + let original = 165 + HandleResolutionResult::with_timestamp(Some("did:web:example.com:user"), 1234567890); 166 + let bytes = original.to_bytes().unwrap(); 167 + let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); 168 + assert_eq!(original, deserialized); 169 + assert_eq!(deserialized.timestamp, 1234567890); 170 + assert_eq!(deserialized.method_type, DidMethodType::Web); 171 + assert_eq!(deserialized.payload, "example.com:user"); 172 + 173 + // Test did:plc 174 + let original = HandleResolutionResult::with_timestamp( 175 + Some("did:plc:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr"), 176 + 9876543210, 177 + ); 178 + let bytes = original.to_bytes().unwrap(); 179 + let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); 180 + assert_eq!(original, deserialized); 181 + assert_eq!(deserialized.timestamp, 9876543210); 182 + assert_eq!(deserialized.method_type, DidMethodType::Plc); 183 + assert_eq!( 184 + deserialized.payload, 185 + "z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr" 186 + ); 187 + 188 + // Test not resolved 189 + let original = HandleResolutionResult::with_timestamp(None, 1111111111); 190 + let bytes = original.to_bytes().unwrap(); 191 + let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); 192 + assert_eq!(original, deserialized); 193 + assert_eq!(deserialized.timestamp, 1111111111); 194 + assert_eq!(deserialized.method_type, DidMethodType::NotResolved); 195 + assert_eq!(deserialized.payload, ""); 196 + } 197 + 198 + #[test] 199 + fn test_binary_size() { 200 + // Test that serialized size is reasonable 201 + let result = 202 + HandleResolutionResult::with_timestamp(Some("did:plc:abcdef123456"), 1234567890); 203 + let bytes = result.to_bytes().unwrap(); 204 + // Should be relatively compact: 8 bytes (u64) + 4 bytes (i32) + string length + metadata 205 + assert!( 206 + bytes.len() < 100, 207 + "Serialized size is too large: {} bytes", 208 + bytes.len() 209 + ); 210 + 211 + // Verify the size for not resolved (should be minimal) 212 + let not_resolved = HandleResolutionResult::not_resolved(); 213 + let bytes = not_resolved.to_bytes().unwrap(); 214 + assert!( 215 + bytes.len() < 50, 216 + "Not resolved size is too large: {} bytes", 217 + bytes.len() 218 + ); 219 + } 220 + 221 + #[test] 222 + fn test_did_web_with_port() { 223 + let did = "did:web:localhost:3000"; 224 + let result = HandleResolutionResult::success(did); 225 + assert_eq!(result.method_type, DidMethodType::Web); 226 + assert_eq!(result.payload, "localhost:3000"); 227 + assert_eq!(result.to_did(), Some(did.to_string())); 228 + } 229 + 230 + #[test] 231 + fn test_did_web_with_path() { 232 + let did = "did:web:example.com:path:to:did"; 233 + let result = HandleResolutionResult::success(did); 234 + assert_eq!(result.method_type, DidMethodType::Web); 235 + assert_eq!(result.payload, "example.com:path:to:did"); 236 + assert_eq!(result.to_did(), Some(did.to_string())); 237 + } 238 + 239 + #[test] 240 + fn test_invalid_did_format() { 241 + let did = "not-a-did"; 242 + let result = HandleResolutionResult::success(did); 243 + assert_eq!(result.method_type, DidMethodType::Other); 244 + assert_eq!(result.payload, did); 245 + assert_eq!(result.to_did(), Some(did.to_string())); 246 + } 247 + 248 + #[test] 249 + fn test_round_trip_all_types() { 250 + let test_cases = vec![ 251 + ("did:web:example.com", DidMethodType::Web, "example.com"), 252 + ("did:plc:xyz789", DidMethodType::Plc, "xyz789"), 253 + ( 254 + "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", 255 + DidMethodType::Other, 256 + "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6", 257 + ), 258 + ]; 259 + 260 + for (did, expected_type, expected_payload) in test_cases { 261 + let result = HandleResolutionResult::success(did); 262 + assert_eq!(result.method_type, expected_type); 263 + assert_eq!(result.payload, expected_payload); 264 + 265 + // Test serialization round-trip 266 + let bytes = result.to_bytes().unwrap(); 267 + let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap(); 268 + assert_eq!(result, deserialized); 269 + assert_eq!(deserialized.to_did(), Some(did.to_string())); 270 + } 271 + } 272 + }
+410
src/handle_resolver.rs
··· 1 + use crate::handle_resolution_result::HandleResolutionResult; 2 + use async_trait::async_trait; 3 + use atproto_identity::resolve::{resolve_subject, DnsResolver}; 4 + use chrono::Utc; 5 + use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool}; 6 + use metrohash::MetroHash64; 7 + use reqwest::Client; 8 + use std::collections::HashMap; 9 + use std::hash::Hasher as _; 10 + use std::sync::Arc; 11 + use thiserror::Error; 12 + use tokio::sync::RwLock; 13 + 14 + /// Errors that can occur during handle resolution 15 + #[derive(Error, Debug)] 16 + pub enum HandleResolverError { 17 + #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")] 18 + ResolutionFailed(String), 19 + 20 + #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")] 21 + HandleNotFoundCached(String), 22 + 23 + #[error("error-quickdid-resolve-3 Handle not found (cached)")] 24 + HandleNotFound, 25 + 26 + #[error("error-quickdid-resolve-4 Mock resolution failure")] 27 + MockResolutionFailure, 28 + } 29 + 30 + #[async_trait] 31 + pub trait HandleResolver: Send + Sync { 32 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>; 33 + } 34 + 35 + pub struct BaseHandleResolver { 36 + /// DNS resolver for handle-to-DID resolution via TXT records. 37 + pub dns_resolver: Arc<dyn DnsResolver>, 38 + /// HTTP client for DID document retrieval and well-known endpoint queries. 39 + pub http_client: Client, 40 + /// Hostname of the PLC directory server for `did:plc` resolution. 41 + pub plc_hostname: String, 42 + } 43 + 44 + #[async_trait] 45 + impl HandleResolver for BaseHandleResolver { 46 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 47 + resolve_subject(&self.http_client, &*self.dns_resolver, s) 48 + .await 49 + .map_err(|e| HandleResolverError::ResolutionFailed(e.to_string())) 50 + } 51 + } 52 + 53 + #[derive(Clone, Debug)] 54 + pub enum ResolveHandleResult { 55 + Found(u64, String), 56 + NotFound(u64, String), 57 + } 58 + 59 + pub struct CachingHandleResolver { 60 + inner: Arc<dyn HandleResolver>, 61 + cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>, 62 + ttl_seconds: u64, 63 + } 64 + 65 + impl CachingHandleResolver { 66 + pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self { 67 + Self { 68 + inner, 69 + cache: Arc::new(RwLock::new(HashMap::new())), 70 + ttl_seconds, 71 + } 72 + } 73 + 74 + fn current_timestamp() -> u64 { 75 + Utc::now().timestamp() as u64 76 + } 77 + 78 + fn is_expired(&self, timestamp: u64) -> bool { 79 + let current = Self::current_timestamp(); 80 + current > timestamp && (current - timestamp) > self.ttl_seconds 81 + } 82 + } 83 + 84 + #[async_trait] 85 + impl HandleResolver for CachingHandleResolver { 86 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 87 + let handle = s.to_string(); 88 + 89 + // Check cache first 90 + { 91 + let cache = self.cache.read().await; 92 + if let Some(cached) = cache.get(&handle) { 93 + match cached { 94 + ResolveHandleResult::Found(timestamp, did) => { 95 + if !self.is_expired(*timestamp) { 96 + tracing::debug!("Cache hit for handle {}: {}", handle, did); 97 + return Ok(did.clone()); 98 + } 99 + tracing::debug!("Cache entry expired for handle {}", handle); 100 + } 101 + ResolveHandleResult::NotFound(timestamp, error) => { 102 + if !self.is_expired(*timestamp) { 103 + tracing::debug!( 104 + "Cache hit (not found) for handle {}: {}", 105 + handle, 106 + error 107 + ); 108 + return Err(HandleResolverError::HandleNotFoundCached(error.clone())); 109 + } 110 + tracing::debug!("Cache entry expired for handle {}", handle); 111 + } 112 + } 113 + } 114 + } 115 + 116 + // Not in cache or expired, resolve through inner resolver 117 + tracing::debug!("Cache miss for handle {}, resolving...", handle); 118 + let result = self.inner.resolve(s).await; 119 + let timestamp = Self::current_timestamp(); 120 + 121 + // Store in cache 122 + { 123 + let mut cache = self.cache.write().await; 124 + match &result { 125 + Ok(did) => { 126 + cache.insert( 127 + handle.clone(), 128 + ResolveHandleResult::Found(timestamp, did.clone()), 129 + ); 130 + tracing::debug!( 131 + "Cached successful resolution for handle {}: {}", 132 + handle, 133 + did 134 + ); 135 + } 136 + Err(e) => { 137 + cache.insert( 138 + handle.clone(), 139 + ResolveHandleResult::NotFound(timestamp, e.to_string()), 140 + ); 141 + tracing::debug!("Cached failed resolution for handle {}: {}", handle, e); 142 + } 143 + } 144 + } 145 + 146 + result 147 + } 148 + } 149 + 150 + /// Redis-backed caching handle resolver that caches resolution results in Redis 151 + /// with a 90-day expiration time. 152 + pub struct RedisHandleResolver { 153 + /// Base handle resolver to perform actual resolution 154 + inner: Arc<dyn HandleResolver>, 155 + /// Redis connection pool 156 + pool: RedisPool, 157 + /// Redis key prefix for handle resolution cache 158 + key_prefix: String, 159 + } 160 + 161 + impl RedisHandleResolver { 162 + /// Create a new Redis-backed handle resolver 163 + pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self { 164 + Self::with_prefix(inner, pool, "handle:".to_string()) 165 + } 166 + 167 + /// Create a new Redis-backed handle resolver with a custom key prefix 168 + pub fn with_prefix( 169 + inner: Arc<dyn HandleResolver>, 170 + pool: RedisPool, 171 + key_prefix: String, 172 + ) -> Self { 173 + Self { 174 + inner, 175 + pool, 176 + key_prefix, 177 + } 178 + } 179 + 180 + /// Generate the Redis key for a handle 181 + fn make_key(&self, handle: &str) -> String { 182 + let mut h = MetroHash64::default(); 183 + h.write(handle.as_bytes()); 184 + format!("{}{}", self.key_prefix, h.finish()) 185 + } 186 + 187 + /// Get the TTL in seconds (90 days) 188 + fn ttl_seconds() -> u64 { 189 + 90 * 24 * 60 * 60 // 90 days in seconds 190 + } 191 + } 192 + 193 + #[async_trait] 194 + impl HandleResolver for RedisHandleResolver { 195 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 196 + let handle = s.to_string(); 197 + let key = self.make_key(&handle); 198 + 199 + // Try to get from Redis cache first 200 + match self.pool.get().await { 201 + Ok(mut conn) => { 202 + // Check if the key exists in Redis (stored as binary data) 203 + let cached: Option<Vec<u8>> = match conn.get(&key).await { 204 + Ok(value) => value, 205 + Err(e) => { 206 + tracing::warn!("Failed to get handle from Redis cache: {}", e); 207 + None 208 + } 209 + }; 210 + 211 + if let Some(cached_bytes) = cached { 212 + // Deserialize the cached result 213 + match HandleResolutionResult::from_bytes(&cached_bytes) { 214 + Ok(cached_result) => { 215 + if let Some(did) = cached_result.to_did() { 216 + tracing::debug!("Cache hit for handle {}: {}", handle, did); 217 + return Ok(did); 218 + } else { 219 + tracing::debug!("Cache hit (not resolved) for handle {}", handle); 220 + return Err(HandleResolverError::HandleNotFound); 221 + } 222 + } 223 + Err(e) => { 224 + tracing::warn!( 225 + "Failed to deserialize cached result for handle {}: {}", 226 + handle, 227 + e 228 + ); 229 + // Fall through to re-resolve if deserialization fails 230 + } 231 + } 232 + } 233 + 234 + // Not in cache, resolve through inner resolver 235 + tracing::debug!("Cache miss for handle {}, resolving...", handle); 236 + let result = self.inner.resolve(s).await; 237 + 238 + // Create and serialize resolution result 239 + let resolution_result = match &result { 240 + Ok(did) => { 241 + tracing::debug!( 242 + "Caching successful resolution for handle {}: {}", 243 + handle, 244 + did 245 + ); 246 + HandleResolutionResult::success(did) 247 + } 248 + Err(e) => { 249 + tracing::debug!("Caching failed resolution for handle {}: {}", handle, e); 250 + HandleResolutionResult::not_resolved() 251 + } 252 + }; 253 + 254 + // Serialize to bytes 255 + match resolution_result.to_bytes() { 256 + Ok(bytes) => { 257 + // Set with expiration (ignore errors to not fail the resolution) 258 + if let Err(e) = conn 259 + .set_ex::<_, _, ()>(&key, bytes, Self::ttl_seconds()) 260 + .await 261 + { 262 + tracing::warn!("Failed to cache handle resolution in Redis: {}", e); 263 + } 264 + } 265 + Err(e) => { 266 + tracing::warn!( 267 + "Failed to serialize resolution result for handle {}: {}", 268 + handle, 269 + e 270 + ); 271 + } 272 + } 273 + 274 + result 275 + } 276 + Err(e) => { 277 + // Redis connection failed, fall back to inner resolver 278 + tracing::warn!( 279 + "Failed to get Redis connection, falling back to uncached resolution: {}", 280 + e 281 + ); 282 + self.inner.resolve(s).await 283 + } 284 + } 285 + } 286 + } 287 + 288 + #[cfg(test)] 289 + mod tests { 290 + use super::*; 291 + use crate::cache::create_redis_pool; 292 + 293 + // Mock handle resolver for testing 294 + #[derive(Clone)] 295 + struct MockHandleResolver { 296 + should_fail: bool, 297 + expected_did: String, 298 + } 299 + 300 + #[async_trait] 301 + impl HandleResolver for MockHandleResolver { 302 + async fn resolve(&self, _handle: &str) -> Result<String, HandleResolverError> { 303 + if self.should_fail { 304 + Err(HandleResolverError::MockResolutionFailure) 305 + } else { 306 + Ok(self.expected_did.clone()) 307 + } 308 + } 309 + } 310 + 311 + #[tokio::test] 312 + async fn test_redis_handle_resolver_cache_hit() { 313 + // This test requires Redis to be running 314 + // Set TEST_REDIS_URL environment variable to run this test 315 + let redis_url = match std::env::var("TEST_REDIS_URL") { 316 + Ok(url) => url, 317 + Err(_) => { 318 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 319 + return; 320 + } 321 + }; 322 + 323 + let pool = match create_redis_pool(&redis_url) { 324 + Ok(p) => p, 325 + Err(e) => { 326 + eprintln!("Failed to create Redis pool: {}", e); 327 + return; 328 + } 329 + }; 330 + 331 + // Create mock resolver 332 + let mock_resolver = Arc::new(MockHandleResolver { 333 + should_fail: false, 334 + expected_did: "did:plc:testuser123".to_string(), 335 + }); 336 + 337 + // Create Redis-backed resolver with a unique key prefix for testing 338 + let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4()); 339 + let redis_resolver = 340 + RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone()); 341 + 342 + let test_handle = "alice.bsky.social"; 343 + 344 + // First resolution - should call inner resolver 345 + let result1 = redis_resolver.resolve(test_handle).await.unwrap(); 346 + assert_eq!(result1, "did:plc:testuser123"); 347 + 348 + // Second resolution - should hit cache 349 + let result2 = redis_resolver.resolve(test_handle).await.unwrap(); 350 + assert_eq!(result2, "did:plc:testuser123"); 351 + 352 + // Clean up test data 353 + if let Ok(mut conn) = pool.get().await { 354 + // Need to compute the hashed key like RedisHandleResolver does 355 + let mut h = MetroHash64::default(); 356 + h.write(test_handle.as_bytes()); 357 + let key = format!("{}{}", test_prefix, h.finish()); 358 + let _: Result<(), _> = conn.del(key).await; 359 + } 360 + } 361 + 362 + #[tokio::test] 363 + async fn test_redis_handle_resolver_cache_error() { 364 + let redis_url = match std::env::var("TEST_REDIS_URL") { 365 + Ok(url) => url, 366 + Err(_) => { 367 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 368 + return; 369 + } 370 + }; 371 + 372 + let pool = match create_redis_pool(&redis_url) { 373 + Ok(p) => p, 374 + Err(e) => { 375 + eprintln!("Failed to create Redis pool: {}", e); 376 + return; 377 + } 378 + }; 379 + 380 + // Create mock resolver that fails 381 + let mock_resolver = Arc::new(MockHandleResolver { 382 + should_fail: true, 383 + expected_did: String::new(), 384 + }); 385 + 386 + // Create Redis-backed resolver with a unique key prefix for testing 387 + let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4()); 388 + let redis_resolver = 389 + RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone()); 390 + 391 + let test_handle = "error.bsky.social"; 392 + 393 + // First resolution - should fail and cache empty string 394 + let result1 = redis_resolver.resolve(test_handle).await; 395 + assert!(result1.is_err()); 396 + 397 + // Second resolution - should hit cache with error 398 + let result2 = redis_resolver.resolve(test_handle).await; 399 + assert!(result2.is_err()); 400 + 401 + // Clean up test data 402 + if let Ok(mut conn) = pool.get().await { 403 + // Need to compute the hashed key like RedisHandleResolver does 404 + let mut h = MetroHash64::default(); 405 + h.write(test_handle.as_bytes()); 406 + let key = format!("{}{}", test_prefix, h.finish()); 407 + let _: Result<(), _> = conn.del(key).await; 408 + } 409 + } 410 + }
+369
src/handle_resolver_task.rs
··· 1 + //! Background task for asynchronous handle resolution 2 + //! 3 + //! This module implements a background task that processes handle resolution requests 4 + //! asynchronously through a work queue. The design supports multiple queue backends 5 + //! and ensures resolved handles are cached for efficient subsequent lookups. 6 + 7 + use crate::handle_resolver::HandleResolver; 8 + use crate::queue_adapter::{HandleResolutionWork, QueueAdapter}; 9 + use anyhow::Result; 10 + use std::sync::Arc; 11 + use std::time::Duration; 12 + use thiserror::Error; 13 + use tokio_util::sync::CancellationToken; 14 + use tracing::{debug, error, info, instrument}; 15 + 16 + /// Handle resolver task errors 17 + #[derive(Error, Debug)] 18 + pub enum HandleResolverError { 19 + #[error("Queue adapter health check failed: adapter is not healthy")] 20 + QueueAdapterUnhealthy, 21 + 22 + #[error("Handle resolution failed: {0}")] 23 + ResolutionFailed(String), 24 + 25 + #[error("Resolution timeout: exceeded {timeout_ms}ms")] 26 + ResolutionTimeout { timeout_ms: u64 }, 27 + } 28 + 29 + /// Result of processing a handle resolution work item 30 + #[derive(Debug, Clone)] 31 + pub struct HandleResolutionResult { 32 + /// The work item that was processed 33 + pub work: HandleResolutionWork, 34 + 35 + /// Whether the handle was successfully resolved 36 + pub success: bool, 37 + 38 + /// The resolved DID if successful 39 + pub did: Option<String>, 40 + 41 + /// Error message if failed 42 + pub error: Option<String>, 43 + 44 + /// Processing duration in milliseconds 45 + pub duration_ms: u64, 46 + 47 + /// Timestamp when processing completed 48 + pub completed_at: chrono::DateTime<chrono::Utc>, 49 + } 50 + 51 + /// Configuration for the handle resolver task processor 52 + #[derive(Clone, Debug)] 53 + pub struct HandleResolverTaskConfig { 54 + /// Default timeout for resolution requests in milliseconds 55 + pub default_timeout_ms: u64, 56 + } 57 + 58 + impl Default for HandleResolverTaskConfig { 59 + fn default() -> Self { 60 + Self { 61 + default_timeout_ms: 10000, // 10 seconds 62 + } 63 + } 64 + } 65 + 66 + /// Metrics for handle resolution processing 67 + #[derive(Debug, Default)] 68 + pub struct HandleResolverMetrics { 69 + pub total_processed: std::sync::atomic::AtomicU64, 70 + pub total_succeeded: std::sync::atomic::AtomicU64, 71 + pub total_failed: std::sync::atomic::AtomicU64, 72 + pub total_cached: std::sync::atomic::AtomicU64, 73 + } 74 + 75 + /// Handle resolver task processor 76 + pub struct HandleResolverTask { 77 + adapter: Arc<dyn QueueAdapter<HandleResolutionWork>>, 78 + handle_resolver: Arc<dyn HandleResolver>, 79 + cancel_token: CancellationToken, 80 + config: HandleResolverTaskConfig, 81 + metrics: Arc<HandleResolverMetrics>, 82 + } 83 + 84 + impl HandleResolverTask { 85 + /// Create a new handle resolver task processor 86 + pub fn new( 87 + adapter: Arc<dyn QueueAdapter<HandleResolutionWork>>, 88 + handle_resolver: Arc<dyn HandleResolver>, 89 + cancel_token: CancellationToken, 90 + ) -> Self { 91 + let config = HandleResolverTaskConfig::default(); 92 + Self { 93 + adapter, 94 + handle_resolver, 95 + cancel_token, 96 + config, 97 + metrics: Arc::new(HandleResolverMetrics::default()), 98 + } 99 + } 100 + 101 + /// Create a new handle resolver task processor with custom configuration 102 + pub fn with_config( 103 + adapter: Arc<dyn QueueAdapter<HandleResolutionWork>>, 104 + handle_resolver: Arc<dyn HandleResolver>, 105 + cancel_token: CancellationToken, 106 + config: HandleResolverTaskConfig, 107 + ) -> Self { 108 + Self { 109 + adapter, 110 + handle_resolver, 111 + cancel_token, 112 + config, 113 + metrics: Arc::new(HandleResolverMetrics::default()), 114 + } 115 + } 116 + 117 + /// Get a reference to the metrics 118 + pub fn metrics(&self) -> &HandleResolverMetrics { 119 + &self.metrics 120 + } 121 + 122 + /// Run the handle resolver task processor 123 + #[instrument(skip(self))] 124 + pub async fn run(self) -> Result<(), HandleResolverError> { 125 + info!("Handle resolver task processor started"); 126 + 127 + // Check adapter health before starting 128 + if !self.adapter.is_healthy().await { 129 + return Err(HandleResolverError::QueueAdapterUnhealthy); 130 + } 131 + 132 + loop { 133 + tokio::select! { 134 + _ = self.cancel_token.cancelled() => { 135 + info!("Handle resolver task processor shutting down"); 136 + break; 137 + } 138 + work = self.adapter.pull() => { 139 + if let Some(work) = work { 140 + // Process the handle resolution directly 141 + self.process_handle_resolution(work.clone()).await; 142 + // Acknowledge work after processing (ignore errors) 143 + let _ = self.adapter.ack(&work).await; 144 + } 145 + } 146 + } 147 + } 148 + 149 + // All work has been processed 150 + info!("All handle resolutions completed"); 151 + 152 + info!( 153 + total_processed = self 154 + .metrics 155 + .total_processed 156 + .load(std::sync::atomic::Ordering::Relaxed), 157 + total_succeeded = self 158 + .metrics 159 + .total_succeeded 160 + .load(std::sync::atomic::Ordering::Relaxed), 161 + total_failed = self 162 + .metrics 163 + .total_failed 164 + .load(std::sync::atomic::Ordering::Relaxed), 165 + total_cached = self 166 + .metrics 167 + .total_cached 168 + .load(std::sync::atomic::Ordering::Relaxed), 169 + "Handle resolver task processor stopped" 170 + ); 171 + 172 + Ok(()) 173 + } 174 + 175 + /// Process a single handle resolution work item 176 + #[instrument(skip(self), fields( 177 + handle = %work.handle, 178 + ))] 179 + async fn process_handle_resolution(&self, work: HandleResolutionWork) { 180 + let start_time = std::time::Instant::now(); 181 + 182 + debug!("Processing handle resolution: {}", work.handle); 183 + 184 + // Perform the handle resolution 185 + let result = self.resolve_handle(&work).await; 186 + 187 + let duration_ms = start_time.elapsed().as_millis() as u64; 188 + 189 + // Update metrics 190 + self.metrics 191 + .total_processed 192 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 193 + 194 + match result { 195 + Ok(response) => { 196 + self.metrics 197 + .total_succeeded 198 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 199 + 200 + if response.did.is_some() { 201 + self.metrics 202 + .total_cached 203 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 204 + } 205 + 206 + info!( 207 + handle = %work.handle, 208 + did = ?response.did, 209 + duration_ms = duration_ms, 210 + "Handle resolved successfully" 211 + ); 212 + } 213 + Err(e) => { 214 + self.metrics 215 + .total_failed 216 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 217 + 218 + error!( 219 + handle = %work.handle, 220 + error = %e, 221 + duration_ms = duration_ms, 222 + "Handle resolution failed" 223 + ); 224 + } 225 + } 226 + } 227 + 228 + /// Resolve a handle using the configured handle resolver 229 + async fn resolve_handle( 230 + &self, 231 + work: &HandleResolutionWork, 232 + ) -> Result<HandleResolutionResult, HandleResolverError> { 233 + let timeout_duration = Duration::from_millis(self.config.default_timeout_ms); 234 + 235 + // Use tokio::time::timeout to enforce timeout 236 + let resolution_future = self.handle_resolver.resolve(&work.handle); 237 + 238 + let did = match tokio::time::timeout(timeout_duration, resolution_future).await { 239 + Ok(result) => match result { 240 + Ok(did) => did, 241 + Err(e) => { 242 + return Err(HandleResolverError::ResolutionFailed(e.to_string())); 243 + } 244 + }, 245 + Err(_) => { 246 + return Err(HandleResolverError::ResolutionTimeout { 247 + timeout_ms: timeout_duration.as_millis() as u64, 248 + }); 249 + } 250 + }; 251 + 252 + Ok(HandleResolutionResult { 253 + work: work.clone(), 254 + success: true, 255 + did: Some(did), 256 + error: None, 257 + duration_ms: 0, // Will be set by caller 258 + completed_at: chrono::Utc::now(), 259 + }) 260 + } 261 + } 262 + 263 + #[cfg(test)] 264 + mod tests { 265 + use super::*; 266 + use crate::queue_adapter::MpscQueueAdapter; 267 + use async_trait::async_trait; 268 + use std::sync::Arc; 269 + use tokio::sync::mpsc; 270 + 271 + // Mock handle resolver for testing 272 + #[derive(Clone)] 273 + struct MockHandleResolver { 274 + should_fail: bool, 275 + } 276 + 277 + #[async_trait] 278 + impl HandleResolver for MockHandleResolver { 279 + async fn resolve(&self, handle: &str) -> Result<String, crate::handle_resolver::HandleResolverError> { 280 + if self.should_fail { 281 + Err(crate::handle_resolver::HandleResolverError::MockResolutionFailure) 282 + } else { 283 + Ok(format!("did:plc:{}", handle.replace('.', ""))) 284 + } 285 + } 286 + } 287 + 288 + #[tokio::test] 289 + async fn test_handle_resolver_task_successful_resolution() { 290 + // Create channels and adapter 291 + let (sender, receiver) = mpsc::channel(10); 292 + let adapter = Arc::new(MpscQueueAdapter::from_channel(sender.clone(), receiver)); 293 + 294 + // Create mock handle resolver 295 + let handle_resolver = Arc::new(MockHandleResolver { should_fail: false }); 296 + 297 + // Create cancellation token 298 + let cancel_token = CancellationToken::new(); 299 + 300 + // Create task with custom config 301 + let config = HandleResolverTaskConfig { 302 + default_timeout_ms: 5000, 303 + }; 304 + 305 + let task = HandleResolverTask::with_config( 306 + adapter.clone(), 307 + handle_resolver, 308 + cancel_token.clone(), 309 + config, 310 + ); 311 + 312 + // Create handle resolution work 313 + let work = HandleResolutionWork::new("alice.example.com".to_string()); 314 + 315 + // Send work to queue 316 + sender.send(work).await.unwrap(); 317 + 318 + // Get metrics reference before moving task 319 + let metrics = task.metrics.clone(); 320 + 321 + // Run task for a short time 322 + let task_handle = tokio::spawn(async move { task.run().await }); 323 + 324 + // Wait a bit for processing 325 + tokio::time::sleep(Duration::from_millis(500)).await; 326 + 327 + // Cancel the task 328 + cancel_token.cancel(); 329 + 330 + // Wait for task to complete 331 + let _ = task_handle.await; 332 + 333 + // Verify metrics 334 + assert_eq!( 335 + metrics 336 + .total_processed 337 + .load(std::sync::atomic::Ordering::Relaxed), 338 + 1 339 + ); 340 + assert_eq!( 341 + metrics 342 + .total_succeeded 343 + .load(std::sync::atomic::Ordering::Relaxed), 344 + 1 345 + ); 346 + } 347 + 348 + #[tokio::test] 349 + async fn test_handle_resolver_metrics() { 350 + use std::sync::atomic::Ordering; 351 + 352 + let metrics = HandleResolverMetrics::default(); 353 + 354 + // Test initial values 355 + assert_eq!(metrics.total_processed.load(Ordering::Relaxed), 0); 356 + assert_eq!(metrics.total_succeeded.load(Ordering::Relaxed), 0); 357 + assert_eq!(metrics.total_failed.load(Ordering::Relaxed), 0); 358 + assert_eq!(metrics.total_cached.load(Ordering::Relaxed), 0); 359 + 360 + // Test incrementing 361 + metrics.total_processed.fetch_add(1, Ordering::Relaxed); 362 + metrics.total_succeeded.fetch_add(1, Ordering::Relaxed); 363 + metrics.total_cached.fetch_add(1, Ordering::Relaxed); 364 + 365 + assert_eq!(metrics.total_processed.load(Ordering::Relaxed), 1); 366 + assert_eq!(metrics.total_succeeded.load(Ordering::Relaxed), 1); 367 + assert_eq!(metrics.total_cached.load(Ordering::Relaxed), 1); 368 + } 369 + }
+116
src/http/handle_xrpc_resolve_handle.rs
··· 1 + use std::sync::Arc; 2 + 3 + use crate::{ 4 + handle_resolver::HandleResolver, 5 + queue_adapter::{HandleResolutionWork, QueueAdapter}, 6 + }; 7 + 8 + use atproto_identity::resolve::{parse_input, InputType}; 9 + use axum::{ 10 + extract::{Query, State}, 11 + http::StatusCode, 12 + response::{IntoResponse, Json}, 13 + }; 14 + use serde::{Deserialize, Serialize}; 15 + 16 + #[derive(Deserialize)] 17 + pub struct ResolveHandleParams { 18 + handle: Option<String>, 19 + queue: Option<String>, 20 + validate: Option<String>, 21 + } 22 + 23 + #[derive(Serialize)] 24 + pub struct ResolveHandleResponse { 25 + did: String, 26 + } 27 + 28 + #[derive(Serialize)] 29 + pub struct ErrorResponse { 30 + error: String, 31 + message: String, 32 + } 33 + 34 + pub async fn handle_xrpc_resolve_handle( 35 + Query(params): Query<ResolveHandleParams>, 36 + State(handle_resolver): State<Arc<dyn HandleResolver>>, 37 + State(queue): State<Arc<dyn QueueAdapter<HandleResolutionWork>>>, 38 + ) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> { 39 + // Validate that handle is provided 40 + let handle = match params.handle { 41 + Some(h) => h, 42 + None => { 43 + return Err(( 44 + StatusCode::BAD_REQUEST, 45 + Json(ErrorResponse { 46 + error: "InvalidRequest".to_string(), 47 + message: "Error: Params must have the property \"handle\"".to_string(), 48 + }), 49 + )); 50 + } 51 + }; 52 + 53 + // Validate that the input is a handle and not a DID 54 + let handle = match parse_input(&handle) { 55 + Ok(InputType::Handle(value)) => value, 56 + Ok(InputType::Plc(_)) | Ok(InputType::Web(_)) => { 57 + // It's a DID, not a handle 58 + return Err(( 59 + StatusCode::BAD_REQUEST, 60 + Json(ErrorResponse { 61 + error: "InvalidRequest".to_string(), 62 + message: "Error: handle must be a valid handle".to_string(), 63 + }), 64 + )); 65 + } 66 + Err(_) => { 67 + return Err(( 68 + StatusCode::BAD_REQUEST, 69 + Json(ErrorResponse { 70 + error: "InvalidRequest".to_string(), 71 + message: "Error: handle must be a valid handle".to_string(), 72 + }), 73 + )); 74 + } 75 + }; 76 + 77 + if params.validate.is_some() { 78 + return Ok(StatusCode::NO_CONTENT.into_response()); 79 + } 80 + 81 + if params.queue.is_some() { 82 + // Create work item 83 + let work = HandleResolutionWork::new(handle.clone()); 84 + 85 + // Queue the work 86 + match queue.push(work).await { 87 + Ok(()) => { 88 + tracing::debug!("Queued handle resolution for {}", handle); 89 + } 90 + Err(e) => { 91 + tracing::error!("Failed to queue handle resolution: {}", e); 92 + } 93 + } 94 + 95 + return Ok(StatusCode::NO_CONTENT.into_response()); 96 + } 97 + 98 + tracing::debug!("Resolving handle: {}", handle); 99 + 100 + match handle_resolver.resolve(&handle).await { 101 + Ok(did) => { 102 + tracing::debug!("Found cached DID for handle {}: {}", handle, did); 103 + return Ok(Json(ResolveHandleResponse { did }).into_response()); 104 + } 105 + Err(_) => { 106 + // {"error":"InvalidRequest","message":"Unable to resolve handle"} 107 + return Err(( 108 + StatusCode::BAD_REQUEST, 109 + Json(ErrorResponse { 110 + error: "InvalidRequest".to_string(), 111 + message: "Unable to resolve handle".to_string(), 112 + }), 113 + )); 114 + } 115 + } 116 + }
+4
src/http/mod.rs
··· 1 + pub mod handle_xrpc_resolve_handle; 2 + pub mod server; 3 + 4 + pub use server::create_router;
+94
src/http/server.rs
··· 1 + use crate::handle_resolver::HandleResolver; 2 + use crate::queue_adapter::{HandleResolutionWork, QueueAdapter}; 3 + use axum::{ 4 + extract::State, 5 + response::{Html, IntoResponse, Json, Response}, 6 + routing::get, 7 + Router, 8 + }; 9 + use http::StatusCode; 10 + use serde_json::json; 11 + use std::{ops::Deref, sync::Arc}; 12 + 13 + pub struct InnerAppContext { 14 + pub http_client: reqwest::Client, 15 + pub service_document: serde_json::Value, 16 + pub service_did: String, 17 + pub handle_resolver: Arc<dyn HandleResolver>, 18 + pub handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 19 + } 20 + 21 + #[derive(Clone)] 22 + pub struct AppContext(pub Arc<InnerAppContext>); 23 + 24 + impl Deref for AppContext { 25 + type Target = InnerAppContext; 26 + 27 + fn deref(&self) -> &Self::Target { 28 + &self.0 29 + } 30 + } 31 + 32 + use axum::extract::FromRef; 33 + 34 + macro_rules! impl_from_ref { 35 + ($context:ty, $field:ident, $type:ty) => { 36 + impl FromRef<$context> for $type { 37 + fn from_ref(context: &$context) -> Self { 38 + context.0.$field.clone() 39 + } 40 + } 41 + }; 42 + } 43 + 44 + impl_from_ref!(AppContext, handle_resolver, Arc<dyn HandleResolver>); 45 + impl_from_ref!( 46 + AppContext, 47 + handle_queue, 48 + Arc<dyn QueueAdapter<HandleResolutionWork>> 49 + ); 50 + 51 + pub fn create_router(app_context: AppContext) -> Router { 52 + Router::new() 53 + .route("/", get(handle_index)) 54 + .route("/.well-known/did.json", get(handle_wellknown_did_json)) 55 + .route( 56 + "/.well-known/atproto-did", 57 + get(handle_wellknown_atproto_did), 58 + ) 59 + .route("/xrpc/_health", get(handle_xrpc_health)) 60 + .route( 61 + "/xrpc/com.atproto.identity.resolveHandle", 62 + get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle), 63 + ) 64 + .with_state(app_context) 65 + } 66 + 67 + async fn handle_index() -> Html<&'static str> { 68 + Html( 69 + r#"<!DOCTYPE html> 70 + <html> 71 + <head> 72 + <title>QuickDID</title> 73 + </head> 74 + <body> 75 + <h1>QuickDID</h1> 76 + <p>AT Protocol Identity Resolution Service</p> 77 + </body> 78 + </html>"#, 79 + ) 80 + } 81 + 82 + async fn handle_wellknown_did_json(State(context): State<AppContext>) -> Json<serde_json::Value> { 83 + Json(context.service_document.clone()) 84 + } 85 + 86 + async fn handle_wellknown_atproto_did(State(context): State<AppContext>) -> Response { 87 + (StatusCode::OK, context.service_did.clone()).into_response() 88 + } 89 + 90 + async fn handle_xrpc_health() -> Json<serde_json::Value> { 91 + Json(json!({ 92 + "version": "0.1.0", 93 + })) 94 + }
+8
src/lib.rs
··· 1 + pub mod cache; 2 + pub mod config; 3 + pub mod handle_resolution_result; 4 + pub mod handle_resolver; 5 + pub mod handle_resolver_task; 6 + pub mod http; 7 + pub mod queue_adapter; 8 + pub mod task_manager;
+3
src/main.rs
··· 1 + fn main() { 2 + println!("Hello, world!"); 3 + }
+873
src/queue_adapter.rs
··· 1 + //! Generic queue adapter system for work queue abstraction. 2 + //! 3 + //! This module provides a generic trait and implementations for queue adapters 4 + //! that can be used with any work type for handle resolution and other tasks. 5 + 6 + use async_trait::async_trait; 7 + use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool}; 8 + use serde::{Deserialize, Serialize}; 9 + use std::sync::Arc; 10 + use thiserror::Error; 11 + use tokio::sync::{mpsc, Mutex}; 12 + use tracing::{debug, error, warn}; 13 + 14 + /// Queue operation errors 15 + #[derive(Error, Debug)] 16 + pub enum QueueError { 17 + #[error("error-quickdid-queue-1 Failed to push to queue: {0}")] 18 + PushFailed(String), 19 + 20 + #[error("error-quickdid-queue-2 Queue is full")] 21 + QueueFull, 22 + 23 + #[error("error-quickdid-queue-3 Queue is closed")] 24 + QueueClosed, 25 + 26 + #[error("error-quickdid-queue-4 Redis connection failed: {0}")] 27 + RedisConnectionFailed(String), 28 + 29 + #[error("error-quickdid-queue-5 Redis operation failed: {operation}: {details}")] 30 + RedisOperationFailed { operation: String, details: String }, 31 + 32 + #[error("error-quickdid-queue-6 Serialization failed: {0}")] 33 + SerializationFailed(String), 34 + 35 + #[error("error-quickdid-queue-7 Deserialization failed: {0}")] 36 + DeserializationFailed(String), 37 + 38 + #[error("error-quickdid-queue-8 Item not found in worker queue during acknowledgment")] 39 + AckItemNotFound, 40 + } 41 + 42 + type Result<T> = std::result::Result<T, QueueError>; 43 + 44 + /// Generic trait for queue adapters that can work with any work type. 45 + /// 46 + /// This trait provides a common interface for different queue implementations 47 + /// (MPSC, Redis, PostgreSQL, etc.) allowing them to be used interchangeably. 48 + #[async_trait] 49 + pub trait QueueAdapter<T>: Send + Sync 50 + where 51 + T: Send + Sync + 'static, 52 + { 53 + /// Pull the next work item from the queue. 54 + /// 55 + /// Returns None if the queue is closed or empty (depending on implementation). 56 + async fn pull(&self) -> Option<T>; 57 + 58 + /// Push a work item to the queue. 59 + /// 60 + /// Returns an error if the queue is full or closed. 61 + async fn push(&self, work: T) -> Result<()>; 62 + 63 + /// Acknowledge that a work item has been successfully processed. 64 + /// 65 + /// This is used by reliable queue implementations to remove the item 66 + /// from a temporary processing queue. Implementations that don't require 67 + /// acknowledgment (like MPSC) can use the default no-op implementation. 68 + async fn ack(&self, _item: &T) -> Result<()> { 69 + // Default no-op implementation for queues that don't need acknowledgment 70 + Ok(()) 71 + } 72 + 73 + /// Try to push a work item without blocking. 74 + /// 75 + /// Returns an error if the queue is full or closed. 76 + async fn try_push(&self, work: T) -> Result<()> { 77 + // Default implementation uses regular push 78 + self.push(work).await 79 + } 80 + 81 + /// Get the current queue depth if available. 82 + /// 83 + /// Returns None if the implementation doesn't support queue depth. 84 + async fn depth(&self) -> Option<usize> { 85 + None 86 + } 87 + 88 + /// Check if the queue is healthy. 89 + /// 90 + /// Used for health checks and monitoring. 91 + async fn is_healthy(&self) -> bool { 92 + true 93 + } 94 + } 95 + 96 + /// MPSC channel-based queue adapter implementation. 97 + /// 98 + /// This adapter uses tokio's multi-producer, single-consumer channel 99 + /// for in-memory queuing of work items. It's suitable for single-instance 100 + /// deployments with moderate throughput requirements. 101 + pub struct MpscQueueAdapter<T> 102 + where 103 + T: Send + Sync + 'static, 104 + { 105 + receiver: Arc<Mutex<mpsc::Receiver<T>>>, 106 + sender: mpsc::Sender<T>, 107 + } 108 + 109 + impl<T> MpscQueueAdapter<T> 110 + where 111 + T: Send + Sync + 'static, 112 + { 113 + /// Create a new MPSC queue adapter with the specified buffer size. 114 + pub fn new(buffer: usize) -> Self { 115 + let (sender, receiver) = mpsc::channel(buffer); 116 + Self { 117 + receiver: Arc::new(Mutex::new(receiver)), 118 + sender, 119 + } 120 + } 121 + 122 + /// Create an adapter from existing MPSC channels (for backward compatibility). 123 + pub fn from_channel(sender: mpsc::Sender<T>, receiver: mpsc::Receiver<T>) -> Self { 124 + Self { 125 + receiver: Arc::new(Mutex::new(receiver)), 126 + sender, 127 + } 128 + } 129 + 130 + /// Get a clone of the sender for producer use. 131 + pub fn sender(&self) -> mpsc::Sender<T> { 132 + self.sender.clone() 133 + } 134 + } 135 + 136 + #[async_trait] 137 + impl<T> QueueAdapter<T> for MpscQueueAdapter<T> 138 + where 139 + T: Send + Sync + 'static, 140 + { 141 + async fn pull(&self) -> Option<T> { 142 + let mut receiver = self.receiver.lock().await; 143 + receiver.recv().await 144 + } 145 + 146 + async fn push(&self, work: T) -> Result<()> { 147 + self.sender 148 + .send(work) 149 + .await 150 + .map_err(|e| QueueError::PushFailed(e.to_string())) 151 + } 152 + 153 + async fn try_push(&self, work: T) -> Result<()> { 154 + self.sender.try_send(work).map_err(|e| match e { 155 + mpsc::error::TrySendError::Full(_) => QueueError::QueueFull, 156 + mpsc::error::TrySendError::Closed(_) => QueueError::QueueClosed, 157 + }) 158 + } 159 + 160 + async fn depth(&self) -> Option<usize> { 161 + // Note: This is an approximation as mpsc doesn't provide exact depth 162 + Some(self.sender.max_capacity() - self.sender.capacity()) 163 + } 164 + 165 + async fn is_healthy(&self) -> bool { 166 + !self.sender.is_closed() 167 + } 168 + } 169 + 170 + /// Work item for handle resolution tasks 171 + #[derive(Debug, Clone, Serialize, Deserialize)] 172 + pub struct HandleResolutionWork { 173 + /// The handle to resolve 174 + pub handle: String, 175 + } 176 + 177 + impl HandleResolutionWork { 178 + /// Create a new handle resolution work item 179 + pub fn new(handle: String) -> Self { 180 + Self { handle } 181 + } 182 + } 183 + 184 + /// Generic work type for different kinds of background tasks 185 + #[derive(Debug, Clone, Serialize, Deserialize)] 186 + pub enum WorkItem { 187 + /// Handle resolution work 188 + HandleResolution(HandleResolutionWork), 189 + // Future work types can be added here 190 + } 191 + 192 + impl WorkItem { 193 + /// Get a unique identifier for this work item 194 + pub fn id(&self) -> String { 195 + match self { 196 + WorkItem::HandleResolution(work) => work.handle.clone(), 197 + } 198 + } 199 + } 200 + 201 + /// Redis-backed queue adapter implementation. 202 + /// 203 + /// This adapter uses Redis lists with a reliable queue pattern: 204 + /// - LPUSH to push items to the primary queue 205 + /// - RPOPLPUSH to atomically move items from primary to worker queue 206 + /// - LREM to acknowledge processed items from worker queue 207 + /// 208 + /// This ensures at-least-once delivery semantics and allows for recovery 209 + /// of in-flight items if a worker crashes. 210 + pub struct RedisQueueAdapter<T> 211 + where 212 + T: Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static, 213 + { 214 + /// Redis connection pool 215 + pool: RedisPool, 216 + /// Unique worker ID for this adapter instance 217 + worker_id: String, 218 + /// Key prefix for all queues (default: "queue:handleresolver:") 219 + key_prefix: String, 220 + /// Timeout for blocking RPOPLPUSH operations 221 + timeout_seconds: u64, 222 + /// Type marker for generic parameter 223 + _phantom: std::marker::PhantomData<T>, 224 + } 225 + 226 + impl<T> RedisQueueAdapter<T> 227 + where 228 + T: Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static, 229 + { 230 + /// Create a new Redis queue adapter with default settings 231 + pub fn new(pool: RedisPool) -> Self { 232 + Self::with_config( 233 + pool, 234 + None, 235 + "queue:handleresolver:".to_string(), 236 + 5, // 5 second timeout for blocking operations 237 + ) 238 + } 239 + 240 + /// Create a new Redis queue adapter with custom configuration 241 + pub fn with_config( 242 + pool: RedisPool, 243 + worker_id: Option<String>, 244 + key_prefix: String, 245 + timeout_seconds: u64, 246 + ) -> Self { 247 + let worker_id = worker_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); 248 + Self { 249 + pool, 250 + worker_id, 251 + key_prefix, 252 + timeout_seconds, 253 + _phantom: std::marker::PhantomData, 254 + } 255 + } 256 + 257 + /// Get the primary queue key 258 + fn primary_queue_key(&self) -> String { 259 + format!("{}primary", self.key_prefix) 260 + } 261 + 262 + /// Get the worker-specific temporary queue key 263 + fn worker_queue_key(&self) -> String { 264 + format!("{}{}", self.key_prefix, self.worker_id) 265 + } 266 + 267 + /// Clean up the worker queue on shutdown 268 + pub async fn cleanup(&self) -> Result<()> { 269 + let mut conn = self 270 + .pool 271 + .get() 272 + .await 273 + .map_err(|e| QueueError::RedisConnectionFailed(e.to_string()))?; 274 + 275 + let worker_key = self.worker_queue_key(); 276 + 277 + // Move all items from worker queue back to primary queue 278 + loop { 279 + let item: Option<Vec<u8>> = conn 280 + .rpoplpush(&worker_key, self.primary_queue_key()) 281 + .await 282 + .map_err(|e| QueueError::RedisOperationFailed { 283 + operation: "RPOPLPUSH".to_string(), 284 + details: e.to_string(), 285 + })?; 286 + 287 + if item.is_none() { 288 + break; 289 + } 290 + } 291 + 292 + debug!( 293 + worker_id = %self.worker_id, 294 + "Cleaned up worker queue" 295 + ); 296 + 297 + Ok(()) 298 + } 299 + } 300 + 301 + #[async_trait] 302 + impl<T> QueueAdapter<T> for RedisQueueAdapter<T> 303 + where 304 + T: Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static, 305 + { 306 + async fn pull(&self) -> Option<T> { 307 + match self.pool.get().await { 308 + Ok(mut conn) => { 309 + let primary_key = self.primary_queue_key(); 310 + let worker_key = self.worker_queue_key(); 311 + 312 + // Use blocking RPOPLPUSH to atomically move item from primary to worker queue 313 + let data: Option<Vec<u8>> = match conn 314 + .brpoplpush(&primary_key, &worker_key, self.timeout_seconds as f64) 315 + .await 316 + { 317 + Ok(data) => data, 318 + Err(e) => { 319 + error!("Failed to pull from queue: {}", e); 320 + return None; 321 + } 322 + }; 323 + 324 + if let Some(data) = data { 325 + // Deserialize the item 326 + match serde_json::from_slice(&data) { 327 + Ok(item) => { 328 + debug!( 329 + worker_id = %self.worker_id, 330 + "Pulled item from queue" 331 + ); 332 + Some(item) 333 + } 334 + Err(e) => { 335 + error!("Failed to deserialize item: {}", e); 336 + // Remove the corrupted item from worker queue 337 + let _: std::result::Result<(), _> = 338 + conn.lrem(&worker_key, 1, &data).await; 339 + None 340 + } 341 + } 342 + } else { 343 + None 344 + } 345 + } 346 + Err(e) => { 347 + error!("Failed to get Redis connection: {}", e); 348 + None 349 + } 350 + } 351 + } 352 + 353 + async fn push(&self, work: T) -> Result<()> { 354 + let mut conn = self 355 + .pool 356 + .get() 357 + .await 358 + .map_err(|e| QueueError::RedisConnectionFailed(e.to_string()))?; 359 + 360 + let data = serde_json::to_vec(&work) 361 + .map_err(|e| QueueError::SerializationFailed(e.to_string()))?; 362 + 363 + let primary_key = self.primary_queue_key(); 364 + 365 + conn.lpush::<_, _, ()>(&primary_key, data) 366 + .await 367 + .map_err(|e| QueueError::RedisOperationFailed { 368 + operation: "LPUSH".to_string(), 369 + details: e.to_string(), 370 + })?; 371 + 372 + debug!("Pushed item to queue"); 373 + Ok(()) 374 + } 375 + 376 + async fn ack(&self, item: &T) -> Result<()> { 377 + let mut conn = self 378 + .pool 379 + .get() 380 + .await 381 + .map_err(|e| QueueError::RedisConnectionFailed(e.to_string()))?; 382 + 383 + let data = 384 + serde_json::to_vec(item).map_err(|e| QueueError::SerializationFailed(e.to_string()))?; 385 + 386 + let worker_key = self.worker_queue_key(); 387 + 388 + // Remove exactly one occurrence of this item from the worker queue 389 + let removed: i32 = conn.lrem(&worker_key, 1, &data).await.map_err(|e| { 390 + QueueError::RedisOperationFailed { 391 + operation: "LREM".to_string(), 392 + details: e.to_string(), 393 + } 394 + })?; 395 + 396 + if removed == 0 { 397 + warn!( 398 + worker_id = %self.worker_id, 399 + "Item not found in worker queue during acknowledgment" 400 + ); 401 + } else { 402 + debug!( 403 + worker_id = %self.worker_id, 404 + "Acknowledged item" 405 + ); 406 + } 407 + 408 + Ok(()) 409 + } 410 + 411 + async fn depth(&self) -> Option<usize> { 412 + match self.pool.get().await { 413 + Ok(mut conn) => { 414 + let primary_key = self.primary_queue_key(); 415 + match conn.llen::<_, usize>(&primary_key).await { 416 + Ok(len) => Some(len), 417 + Err(e) => { 418 + error!("Failed to get queue depth: {}", e); 419 + None 420 + } 421 + } 422 + } 423 + Err(e) => { 424 + error!("Failed to get Redis connection: {}", e); 425 + None 426 + } 427 + } 428 + } 429 + 430 + async fn is_healthy(&self) -> bool { 431 + match self.pool.get().await { 432 + Ok(mut conn) => { 433 + // Ping Redis to check health 434 + match deadpool_redis::redis::cmd("PING") 435 + .query_async::<String>(&mut conn) 436 + .await 437 + { 438 + Ok(response) => response == "PONG", 439 + Err(_) => false, 440 + } 441 + } 442 + Err(_) => false, 443 + } 444 + } 445 + } 446 + 447 + /// No-operation queue adapter that discards all work items. 448 + /// 449 + /// This adapter is useful for configurations where queuing is disabled 450 + /// or as a fallback when other queue adapters fail to initialize. 451 + pub struct NoopQueueAdapter<T> 452 + where 453 + T: Send + Sync + 'static, 454 + { 455 + _phantom: std::marker::PhantomData<T>, 456 + } 457 + 458 + impl<T> NoopQueueAdapter<T> 459 + where 460 + T: Send + Sync + 'static, 461 + { 462 + /// Create a new no-op queue adapter 463 + pub fn new() -> Self { 464 + Self { 465 + _phantom: std::marker::PhantomData, 466 + } 467 + } 468 + } 469 + 470 + impl<T> Default for NoopQueueAdapter<T> 471 + where 472 + T: Send + Sync + 'static, 473 + { 474 + fn default() -> Self { 475 + Self::new() 476 + } 477 + } 478 + 479 + #[async_trait] 480 + impl<T> QueueAdapter<T> for NoopQueueAdapter<T> 481 + where 482 + T: Send + Sync + 'static, 483 + { 484 + async fn pull(&self) -> Option<T> { 485 + // Never returns any work 486 + tokio::time::sleep(std::time::Duration::from_secs(60)).await; 487 + None 488 + } 489 + 490 + async fn push(&self, _work: T) -> Result<()> { 491 + // Silently discard the work 492 + Ok(()) 493 + } 494 + 495 + async fn ack(&self, _item: &T) -> Result<()> { 496 + // No-op 497 + Ok(()) 498 + } 499 + 500 + async fn try_push(&self, _work: T) -> Result<()> { 501 + // Silently discard the work 502 + Ok(()) 503 + } 504 + 505 + async fn depth(&self) -> Option<usize> { 506 + // Always empty 507 + Some(0) 508 + } 509 + 510 + async fn is_healthy(&self) -> bool { 511 + // Always healthy 512 + true 513 + } 514 + } 515 + 516 + /// Worker that processes items from a queue adapter 517 + pub struct QueueWorker<T, A> 518 + where 519 + T: Send + Sync + 'static, 520 + A: QueueAdapter<T>, 521 + { 522 + adapter: Arc<A>, 523 + name: String, 524 + _phantom: std::marker::PhantomData<T>, 525 + } 526 + 527 + impl<T, A> QueueWorker<T, A> 528 + where 529 + T: Send + Sync + 'static, 530 + A: QueueAdapter<T> + 'static, 531 + { 532 + /// Create a new queue worker 533 + pub fn new(adapter: Arc<A>, name: String) -> Self { 534 + Self { 535 + adapter, 536 + name, 537 + _phantom: std::marker::PhantomData, 538 + } 539 + } 540 + 541 + /// Run the worker with a custom processor function 542 + pub async fn run<F, Fut>(self, processor: F) -> std::result::Result<(), QueueError> 543 + where 544 + F: Fn(T) -> Fut + Send + Sync + 'static, 545 + Fut: std::future::Future<Output = std::result::Result<(), QueueError>> + Send, 546 + { 547 + debug!(worker = %self.name, "Starting queue worker"); 548 + 549 + loop { 550 + match self.adapter.pull().await { 551 + Some(work) => { 552 + debug!(worker = %self.name, "Processing work item"); 553 + 554 + match processor(work).await { 555 + Ok(()) => { 556 + debug!(worker = %self.name, "Work item processed successfully"); 557 + } 558 + Err(e) => { 559 + error!(worker = %self.name, error = ?e, "Failed to process work item"); 560 + } 561 + } 562 + } 563 + None => { 564 + // Queue is closed or empty 565 + debug!(worker = %self.name, "No work available, worker shutting down"); 566 + break; 567 + } 568 + } 569 + } 570 + 571 + debug!(worker = %self.name, "Queue worker stopped"); 572 + Ok(()) 573 + } 574 + 575 + /// Run the worker with cancellation support 576 + pub async fn run_with_cancellation<F, Fut>( 577 + self, 578 + processor: F, 579 + cancel_token: tokio_util::sync::CancellationToken, 580 + ) -> std::result::Result<(), QueueError> 581 + where 582 + F: Fn(T) -> Fut + Send + Sync + 'static, 583 + Fut: std::future::Future<Output = std::result::Result<(), QueueError>> + Send, 584 + { 585 + debug!(worker = %self.name, "Starting queue worker with cancellation support"); 586 + 587 + loop { 588 + tokio::select! { 589 + work = self.adapter.pull() => { 590 + match work { 591 + Some(item) => { 592 + debug!(worker = %self.name, "Processing work item"); 593 + 594 + match processor(item).await { 595 + Ok(()) => { 596 + debug!(worker = %self.name, "Work item processed successfully"); 597 + } 598 + Err(e) => { 599 + error!(worker = %self.name, error = ?e, "Failed to process work item"); 600 + } 601 + } 602 + } 603 + None => { 604 + debug!(worker = %self.name, "No work available, worker shutting down"); 605 + break; 606 + } 607 + } 608 + } 609 + () = cancel_token.cancelled() => { 610 + debug!(worker = %self.name, "Worker cancelled, shutting down"); 611 + break; 612 + } 613 + } 614 + } 615 + 616 + debug!(worker = %self.name, "Queue worker stopped"); 617 + Ok(()) 618 + } 619 + } 620 + 621 + #[cfg(test)] 622 + mod tests { 623 + use super::*; 624 + 625 + #[tokio::test] 626 + async fn test_mpsc_queue_adapter_push_pull() { 627 + let adapter = Arc::new(MpscQueueAdapter::<String>::new(10)); 628 + 629 + // Test push 630 + adapter.push("test".to_string()).await.unwrap(); 631 + 632 + // Test pull 633 + let pulled = adapter.pull().await; 634 + assert!(pulled.is_some()); 635 + assert_eq!(pulled.unwrap(), "test"); 636 + } 637 + 638 + #[tokio::test] 639 + async fn test_handle_resolution_work() { 640 + let work = HandleResolutionWork::new("alice.example.com".to_string()); 641 + 642 + assert_eq!(work.handle, "alice.example.com"); 643 + } 644 + 645 + #[tokio::test] 646 + async fn test_work_item_id() { 647 + let work = HandleResolutionWork::new("example.com".to_string()); 648 + 649 + let work_item = WorkItem::HandleResolution(work); 650 + assert_eq!(work_item.id(), "example.com"); 651 + } 652 + 653 + #[tokio::test] 654 + #[ignore = "Test hangs due to implementation issue"] 655 + async fn test_queue_worker() { 656 + let adapter = Arc::new(MpscQueueAdapter::<String>::new(10)); 657 + let worker_adapter = adapter.clone(); 658 + 659 + // Push some work 660 + adapter.push("item1".to_string()).await.unwrap(); 661 + adapter.push("item2".to_string()).await.unwrap(); 662 + 663 + // Drop the sender to signal completion 664 + drop(adapter); 665 + 666 + let worker = QueueWorker::new(worker_adapter, "test-worker".to_string()); 667 + 668 + let processed_items = Vec::new(); 669 + let items_clone = Arc::new(Mutex::new(processed_items)); 670 + let items_ref = items_clone.clone(); 671 + 672 + worker 673 + .run(move |item| { 674 + let items = items_ref.clone(); 675 + async move { 676 + let mut items = items.lock().await; 677 + items.push(item); 678 + Ok(()) 679 + } 680 + }) 681 + .await 682 + .unwrap(); 683 + 684 + let final_items = items_clone.lock().await; 685 + assert_eq!(final_items.len(), 2); 686 + assert!(final_items.contains(&"item1".to_string())); 687 + assert!(final_items.contains(&"item2".to_string())); 688 + } 689 + 690 + #[tokio::test] 691 + async fn test_redis_queue_adapter_push_pull() { 692 + // This test requires Redis to be running 693 + let redis_url = match std::env::var("TEST_REDIS_URL") { 694 + Ok(url) => url, 695 + Err(_) => { 696 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 697 + return; 698 + } 699 + }; 700 + 701 + // Import create_redis_pool 702 + use crate::cache::create_redis_pool; 703 + 704 + let pool = match create_redis_pool(&redis_url) { 705 + Ok(p) => p, 706 + Err(e) => { 707 + eprintln!("Failed to create Redis pool: {}", e); 708 + return; 709 + } 710 + }; 711 + 712 + // Create adapter with unique prefix for testing 713 + let test_prefix = format!("test:queue:{}:", uuid::Uuid::new_v4()); 714 + let adapter = Arc::new(RedisQueueAdapter::<String>::with_config( 715 + pool.clone(), 716 + Some("test-worker".to_string()), 717 + test_prefix.clone(), 718 + 1, // 1 second timeout for tests 719 + )); 720 + 721 + // Test push 722 + adapter.push("test-item".to_string()).await.unwrap(); 723 + 724 + // Test pull 725 + let pulled = adapter.pull().await; 726 + assert!(pulled.is_some()); 727 + assert_eq!(pulled.unwrap(), "test-item"); 728 + 729 + // Test ack 730 + adapter 731 + .ack(&"test-item".to_string()) 732 + .await 733 + .expect("Ack should succeed"); 734 + 735 + // Clean up test data 736 + adapter.cleanup().await.unwrap(); 737 + } 738 + 739 + #[tokio::test] 740 + async fn test_redis_queue_adapter_reliable_queue() { 741 + let redis_url = match std::env::var("TEST_REDIS_URL") { 742 + Ok(url) => url, 743 + Err(_) => { 744 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 745 + return; 746 + } 747 + }; 748 + 749 + use crate::cache::create_redis_pool; 750 + 751 + let pool = match create_redis_pool(&redis_url) { 752 + Ok(p) => p, 753 + Err(e) => { 754 + eprintln!("Failed to create Redis pool: {}", e); 755 + return; 756 + } 757 + }; 758 + 759 + let test_prefix = format!("test:queue:{}:", uuid::Uuid::new_v4()); 760 + let worker_id = "test-worker-reliable"; 761 + 762 + // Create first adapter 763 + let adapter1 = Arc::new(RedisQueueAdapter::<String>::with_config( 764 + pool.clone(), 765 + Some(worker_id.to_string()), 766 + test_prefix.clone(), 767 + 1, 768 + )); 769 + 770 + // Push multiple items 771 + adapter1.push("item1".to_string()).await.unwrap(); 772 + adapter1.push("item2".to_string()).await.unwrap(); 773 + adapter1.push("item3".to_string()).await.unwrap(); 774 + 775 + // Pull but don't ack (simulating worker crash) 776 + let item1 = adapter1.pull().await; 777 + assert!(item1.is_some()); 778 + assert_eq!(item1.unwrap(), "item1"); 779 + 780 + // Create second adapter with same worker_id (simulating restart) 781 + let adapter2 = Arc::new(RedisQueueAdapter::<String>::with_config( 782 + pool.clone(), 783 + Some(worker_id.to_string()), 784 + test_prefix.clone(), 785 + 1, 786 + )); 787 + 788 + // Clean up should move unacked item back to primary queue 789 + adapter2.cleanup().await.unwrap(); 790 + 791 + // Now pull should get item1 again (recovered from worker queue) 792 + let recovered = adapter2.pull().await; 793 + assert!(recovered.is_some()); 794 + // Note: The item might be item1 or item2 depending on Redis list order after cleanup 795 + 796 + // Clean up all test data 797 + adapter2.cleanup().await.unwrap(); 798 + } 799 + 800 + #[tokio::test] 801 + async fn test_redis_queue_adapter_depth() { 802 + let redis_url = match std::env::var("TEST_REDIS_URL") { 803 + Ok(url) => url, 804 + Err(_) => { 805 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 806 + return; 807 + } 808 + }; 809 + 810 + use crate::cache::create_redis_pool; 811 + 812 + let pool = match create_redis_pool(&redis_url) { 813 + Ok(p) => p, 814 + Err(e) => { 815 + eprintln!("Failed to create Redis pool: {}", e); 816 + return; 817 + } 818 + }; 819 + 820 + let test_prefix = format!("test:queue:{}:", uuid::Uuid::new_v4()); 821 + let adapter = Arc::new(RedisQueueAdapter::<String>::with_config( 822 + pool.clone(), 823 + None, 824 + test_prefix.clone(), 825 + 1, 826 + )); 827 + 828 + // Initially empty 829 + let depth = adapter.depth().await; 830 + assert_eq!(depth, Some(0)); 831 + 832 + // Push items and check depth 833 + adapter.push("item1".to_string()).await.unwrap(); 834 + assert_eq!(adapter.depth().await, Some(1)); 835 + 836 + adapter.push("item2".to_string()).await.unwrap(); 837 + assert_eq!(adapter.depth().await, Some(2)); 838 + 839 + // Pull and check depth decreases 840 + let _ = adapter.pull().await; 841 + // Note: depth checks primary queue, not worker queue 842 + assert_eq!(adapter.depth().await, Some(1)); 843 + 844 + // Clean up 845 + adapter.cleanup().await.unwrap(); 846 + } 847 + 848 + #[tokio::test] 849 + async fn test_redis_queue_adapter_health() { 850 + let redis_url = match std::env::var("TEST_REDIS_URL") { 851 + Ok(url) => url, 852 + Err(_) => { 853 + eprintln!("Skipping test: Set TEST_REDIS_URL to run Redis tests"); 854 + return; 855 + } 856 + }; 857 + 858 + use crate::cache::create_redis_pool; 859 + 860 + let pool = match create_redis_pool(&redis_url) { 861 + Ok(p) => p, 862 + Err(e) => { 863 + eprintln!("Failed to create Redis pool: {}", e); 864 + return; 865 + } 866 + }; 867 + 868 + let adapter = RedisQueueAdapter::<String>::new(pool); 869 + 870 + // Should be healthy if Redis is running 871 + assert!(adapter.is_healthy().await); 872 + } 873 + }
+159
src/task_manager.rs
··· 1 + //! Task management utilities for consistent background task handling 2 + //! 3 + //! This module provides helpers for spawning and managing background tasks with: 4 + //! - Consistent start/stop logging 5 + //! - Automatic shutdown on task failure 6 + //! - Graceful shutdown support 7 + 8 + use std::future::Future; 9 + use tokio_util::{sync::CancellationToken, task::TaskTracker}; 10 + use tracing::{error, info}; 11 + 12 + /// Spawn a background task with consistent lifecycle management 13 + /// 14 + /// This function: 15 + /// 1. Logs when the task starts 16 + /// 2. Logs when the task completes (success or failure) 17 + /// 3. Triggers application shutdown on task failure 18 + /// 4. Supports graceful shutdown via cancellation token 19 + pub fn spawn_managed_task<F>( 20 + tracker: &TaskTracker, 21 + app_token: CancellationToken, 22 + task_name: &str, 23 + task_future: F, 24 + ) where 25 + F: Future<Output = anyhow::Result<()>> + Send + 'static, 26 + { 27 + info!(task = task_name, "Starting background task"); 28 + 29 + let task_token = app_token.clone(); 30 + 31 + let inner_task_name = task_name.to_string(); 32 + 33 + tracker.spawn(async move { 34 + // Run the task and handle its result 35 + match task_future.await { 36 + Ok(()) => { 37 + info!( 38 + task = inner_task_name, 39 + "Background task completed successfully" 40 + ); 41 + } 42 + Err(e) => { 43 + error!(task = inner_task_name, error = ?e, "Background task failed unexpectedly"); 44 + // Trigger application shutdown on task failure 45 + task_token.cancel(); 46 + } 47 + } 48 + }); 49 + } 50 + 51 + /// Spawn a background task with cancellation support 52 + /// 53 + /// This version allows the task to be cancelled via the token and handles 54 + /// both graceful shutdown and unexpected failures 55 + pub fn spawn_cancellable_task<F, Fut>( 56 + tracker: &TaskTracker, 57 + app_token: CancellationToken, 58 + task_name: &str, 59 + task_builder: F, 60 + ) where 61 + F: FnOnce(CancellationToken) -> Fut + Send + 'static, 62 + Fut: Future<Output = anyhow::Result<()>> + Send + 'static, 63 + { 64 + info!(task = task_name, "Starting cancellable background task"); 65 + 66 + let task_token = app_token.clone(); 67 + let cancel_token = app_token.clone(); 68 + 69 + let inner_task_name = task_name.to_string(); 70 + 71 + tracker.spawn(async move { 72 + tokio::select! { 73 + result = task_builder(cancel_token.clone()) => { 74 + match result { 75 + Ok(()) => { 76 + info!(task = inner_task_name, "Background task completed successfully"); 77 + } 78 + Err(e) => { 79 + error!(error = ?e, task = inner_task_name, "Background task failed unexpectedly"); 80 + // Trigger application shutdown on task failure 81 + task_token.cancel(); 82 + } 83 + } 84 + } 85 + () = task_token.cancelled() => { 86 + info!(task = inner_task_name, "Background task shutting down gracefully"); 87 + } 88 + } 89 + }); 90 + } 91 + 92 + /// Helper for tasks that need both cancellation and custom shutdown logic 93 + pub fn spawn_task_with_shutdown<F, S>( 94 + tracker: &TaskTracker, 95 + app_token: CancellationToken, 96 + task_name: &str, 97 + task_future: F, 98 + shutdown_handler: S, 99 + ) where 100 + F: Future<Output = anyhow::Result<()>> + Send + 'static, 101 + S: Future<Output = ()> + Send + 'static, 102 + { 103 + info!( 104 + task = task_name, 105 + "Starting background task with custom shutdown" 106 + ); 107 + 108 + let task_token = app_token.clone(); 109 + 110 + let inner_task_name = task_name.to_string(); 111 + 112 + tracker.spawn(async move { 113 + tokio::select! { 114 + result = task_future => { 115 + match result { 116 + Ok(()) => { 117 + info!(task = inner_task_name, "Background task completed successfully"); 118 + } 119 + Err(e) => { 120 + error!(task = inner_task_name, error = ?e, "Background task failed unexpectedly"); 121 + // Trigger application shutdown on task failure 122 + task_token.cancel(); 123 + } 124 + } 125 + } 126 + () = task_token.cancelled() => { 127 + info!(task = inner_task_name, "Background task shutting down gracefully"); 128 + shutdown_handler.await; 129 + info!(task = inner_task_name, "Background task shutdown complete"); 130 + } 131 + } 132 + }); 133 + } 134 + 135 + /// Macro for consistent task error handling within a task 136 + #[macro_export] 137 + macro_rules! task_try { 138 + ($expr:expr, $task_name:expr) => { 139 + match $expr { 140 + Ok(val) => val, 141 + Err(e) => { 142 + tracing::error!(task = $task_name, error = ?e, "Task operation failed"); 143 + return Err(e.into()); 144 + } 145 + } 146 + }; 147 + } 148 + 149 + /// Macro for logging task checkpoints 150 + #[macro_export] 151 + macro_rules! task_checkpoint { 152 + ($task_name:expr, $checkpoint:expr) => { 153 + tracing::debug!( 154 + task = $task_name, 155 + checkpoint = $checkpoint, 156 + "Task checkpoint reached" 157 + ); 158 + }; 159 + }