Microservice to bring 2FA to self hosted PDSes

Compare changes

Choose any two refs to compare.

+4
.dockerignore
··· 1 + target 2 + /target 3 + **/.idea 4 + .idea
+279
AUTH.md
··· 1 + # PDS Gatekeeper Authentication Middleware 2 + 3 + This document describes the authentication middleware system for pds-gatekeeper, which provides flexible authorization rules based on DIDs, handles, and OAuth scopes. 4 + 5 + ## Overview 6 + 7 + The auth middleware validates incoming requests by: 8 + 9 + 1. **Extracting** the DID and scopes from a JWT Bearer token 10 + 2. **Resolving** the DID to a handle using jacquard-identity 11 + 3. **Validating** against configured authorization rules 12 + 4. **Returning** appropriate HTTP errors (401/403) on failure 13 + 14 + ## Quick Start 15 + 16 + ```rust 17 + use axum::middleware::from_fn_with_state; 18 + use crate::auth::{auth_middleware, handle_ends_with, scope_equals, with_rules, AuthRules}; 19 + 20 + let app = Router::new() 21 + // Simple: require handle from specific domain 22 + .route("/xrpc/community.blacksky.feed.get", 23 + get(handler).layer(from_fn_with_state( 24 + handle_ends_with(".blacksky.team", &state), 25 + auth_middleware 26 + ))) 27 + 28 + // Simple: require specific OAuth scope 29 + .route("/xrpc/com.atproto.repo.createRecord", 30 + post(handler).layer(from_fn_with_state( 31 + scope_equals("repo:app.bsky.feed.post", &state), 32 + auth_middleware 33 + ))) 34 + 35 + .with_state(state); 36 + ``` 37 + 38 + ## ATProto OAuth Scopes Reference 39 + 40 + | Scope | Description | 41 + |-------|-------------| 42 + | `atproto` | Base scope, required for all OAuth clients | 43 + | `transition:generic` | Full repository access (equivalent to app passwords) | 44 + | `repo:<collection>` | Access to specific collection (e.g., `repo:app.bsky.feed.post`) | 45 + | `identity:handle` | Permits handle changes | 46 + | `identity:*` | Full DID document control | 47 + | `account:email` | Read email addresses | 48 + | `account:repo?action=manage` | Import repository data | 49 + | `blob:*/*` | Upload any blob type | 50 + | `blob?accept=image/*` | Upload only images | 51 + 52 + See [Marvin's Guide to OAuth Scopes](https://marvins-guide.leaflet.pub/3mbfvey7sok26) for complete details. 53 + 54 + ## Helper Functions 55 + 56 + ### Identity Helpers 57 + 58 + | Function | Description | 59 + |----------|-------------| 60 + | `handle_ends_with(suffix, state)` | Handle must end with suffix | 61 + | `handle_ends_with_any(suffixes, state)` | Handle must end with any suffix (OR) | 62 + | `did_equals(did, state)` | DID must match exactly | 63 + | `did_equals_any(dids, state)` | DID must match any value (OR) | 64 + 65 + ### Scope Helpers 66 + 67 + | Function | Description | 68 + |----------|-------------| 69 + | `scope_equals(scope, state)` | Must have specific scope | 70 + | `scope_any(scopes, state)` | Must have any of the scopes (OR) | 71 + | `scope_all(scopes, state)` | Must have all scopes (AND) | 72 + 73 + ### Combined Helpers (Identity + Scope) 74 + 75 + | Function | Description | 76 + |----------|-------------| 77 + | `handle_ends_with_and_scope(suffix, scope, state)` | Handle suffix AND scope | 78 + | `handle_ends_with_and_scopes(suffix, scopes, state)` | Handle suffix AND all scopes | 79 + | `did_with_scope(did, scope, state)` | DID match AND scope | 80 + | `did_with_scopes(did, scopes, state)` | DID match AND all scopes | 81 + 82 + ### Custom Rules 83 + 84 + For complex authorization logic, use `with_rules()`: 85 + 86 + ```rust 87 + with_rules(AuthRules::Any(vec![ 88 + AuthRules::DidEquals("did:plc:rnpkyqnmsw4ipey6eotbdnnf".into()), 89 + AuthRules::All(vec![ 90 + AuthRules::HandleEndsWith(".mod.team".into()), 91 + AuthRules::ScopeEquals("account:email".into()), 92 + ]), 93 + ]), &state) 94 + ``` 95 + 96 + ## Realistic PDS Endpoint Examples 97 + 98 + ### Admin Endpoints 99 + 100 + Based on `com.atproto.admin.*` endpoints from the ATProto PDS: 101 + 102 + ```rust 103 + // com.atproto.admin.deleteAccount 104 + // Admin-only: specific DID with full access scope 105 + .route("/xrpc/com.atproto.admin.deleteAccount", 106 + post(delete_account).layer(from_fn_with_state( 107 + did_with_scope("did:plc:rnpkyqnmsw4ipey6eotbdnnf", "transition:generic", &state), 108 + auth_middleware 109 + ))) 110 + 111 + // com.atproto.admin.getAccountInfo 112 + // Either admin DID OR (moderator handle + account scope) 113 + .route("/xrpc/com.atproto.admin.getAccountInfo", 114 + get(get_account_info).layer(from_fn_with_state( 115 + with_rules(AuthRules::Any(vec![ 116 + AuthRules::DidEquals("did:plc:rnpkyqnmsw4ipey6eotbdnnf".into()), 117 + AuthRules::All(vec![ 118 + AuthRules::HandleEndsWith(".mod.team".into()), 119 + AuthRules::ScopeEquals("account:email".into()), 120 + ]), 121 + ]), &state), 122 + auth_middleware 123 + ))) 124 + 125 + // com.atproto.admin.updateAccountEmail 126 + // Admin DID with account management scope 127 + .route("/xrpc/com.atproto.admin.updateAccountEmail", 128 + post(update_email).layer(from_fn_with_state( 129 + did_with_scopes( 130 + "did:plc:rnpkyqnmsw4ipey6eotbdnnf", 131 + ["account:email", "account:repo?action=manage"], 132 + &state 133 + ), 134 + auth_middleware 135 + ))) 136 + 137 + // com.atproto.admin.updateAccountHandle 138 + // Admin with identity control 139 + .route("/xrpc/com.atproto.admin.updateAccountHandle", 140 + post(update_handle).layer(from_fn_with_state( 141 + did_with_scope("did:plc:rnpkyqnmsw4ipey6eotbdnnf", "identity:*", &state), 142 + auth_middleware 143 + ))) 144 + ``` 145 + 146 + ### Repository Endpoints 147 + 148 + ```rust 149 + // com.atproto.repo.createRecord 150 + // Scoped write access to specific collection 151 + .route("/xrpc/com.atproto.repo.createRecord", 152 + post(create_record).layer(from_fn_with_state( 153 + scope_equals("repo:app.bsky.feed.post", &state), 154 + auth_middleware 155 + ))) 156 + 157 + // com.atproto.repo.putRecord 158 + // Either specific collection scope OR full access 159 + .route("/xrpc/com.atproto.repo.putRecord", 160 + post(put_record).layer(from_fn_with_state( 161 + scope_any(["repo:app.bsky.feed.post", "transition:generic"], &state), 162 + auth_middleware 163 + ))) 164 + 165 + // com.atproto.repo.uploadBlob 166 + // Blob upload with media type restriction (scope-based) 167 + .route("/xrpc/com.atproto.repo.uploadBlob", 168 + post(upload_blob).layer(from_fn_with_state( 169 + scope_any(["blob:*/*", "blob?accept=image/*", "transition:generic"], &state), 170 + auth_middleware 171 + ))) 172 + ``` 173 + 174 + ### Community/Custom Endpoints 175 + 176 + ```rust 177 + // Community feed generator - restricted to team members with full access 178 + .route("/xrpc/community.blacksky.feed.generator", 179 + post(generator).layer(from_fn_with_state( 180 + handle_ends_with_and_scope(".blacksky.team", "transition:generic", &state), 181 + auth_middleware 182 + ))) 183 + 184 + // Multi-community endpoint 185 + .route("/xrpc/community.shared.moderation.report", 186 + post(report).layer(from_fn_with_state( 187 + with_rules(AuthRules::All(vec![ 188 + AuthRules::HandleEndsWithAny(vec![ 189 + ".blacksky.team".into(), 190 + ".bsky.team".into(), 191 + ".mod.social".into(), 192 + ]), 193 + AuthRules::ScopeEquals("atproto".into()), 194 + ]), &state), 195 + auth_middleware 196 + ))) 197 + 198 + // VIP access - specific DIDs only 199 + .route("/xrpc/community.blacksky.vip.access", 200 + get(vip_handler).layer(from_fn_with_state( 201 + did_equals_any([ 202 + "did:plc:rnpkyqnmsw4ipey6eotbdnnf", 203 + "did:plc:abc123def456ghi789jklmno", 204 + "did:plc:xyz987uvw654rst321qponml", 205 + ], &state), 206 + auth_middleware 207 + ))) 208 + ``` 209 + 210 + ## Building Complex Authorization Rules 211 + 212 + The `AuthRules` enum supports arbitrary nesting: 213 + 214 + ```rust 215 + // Complex: Admin OR (Team member with write scope) OR (Moderator with read-only) 216 + let rules = AuthRules::Any(vec![ 217 + // Admin bypass 218 + AuthRules::DidEquals("did:plc:rnpkyqnmsw4ipey6eotbdnnf".into()), 219 + 220 + // Team member with write access 221 + AuthRules::All(vec![ 222 + AuthRules::HandleEndsWith(".blacksky.team".into()), 223 + AuthRules::ScopeEquals("transition:generic".into()), 224 + ]), 225 + 226 + // Moderator with limited scope 227 + AuthRules::All(vec![ 228 + AuthRules::HandleEndsWith(".mod.team".into()), 229 + AuthRules::ScopeEqualsAny(vec![ 230 + "account:email".into(), 231 + "atproto".into(), 232 + ]), 233 + ]), 234 + ]); 235 + ``` 236 + 237 + ## Error Responses 238 + 239 + | Status | Error Code | Description | 240 + |--------|------------|-------------| 241 + | `401` | `AuthRequired` | No Authorization header provided | 242 + | `401` | `InvalidToken` | JWT validation failed (expired, invalid signature, malformed) | 243 + | `403` | `AccessDenied` | Valid authentication but authorization rules rejected | 244 + | `500` | `ResolutionError` | Failed to resolve DID to handle | 245 + 246 + Response format: 247 + ```json 248 + { 249 + "error": "AccessDenied", 250 + "message": "Access denied by authorization rules" 251 + } 252 + ``` 253 + 254 + ## JWT Token Format 255 + 256 + The middleware expects JWT tokens with these claims: 257 + 258 + ```json 259 + { 260 + "sub": "did:plc:rnpkyqnmsw4ipey6eotbdnnf", 261 + "scope": "atproto transition:generic repo:app.bsky.feed.post", 262 + "iat": 1704067200, 263 + "exp": 1704153600 264 + } 265 + ``` 266 + 267 + - `sub` (required): The user's DID 268 + - `scope` (optional): Space-separated OAuth scopes per [RFC 6749](https://tools.ietf.org/html/rfc6749) 269 + 270 + ## Handle Resolution 271 + 272 + DIDs are resolved to handles using the jacquard-identity `PublicResolver`: 273 + 274 + 1. Check the `HandleCache` for a cached result 275 + 2. If miss, resolve the DID document via PLC directory 276 + 3. Extract handle from `alsoKnownAs` field (format: `at://handle.example.com`) 277 + 4. Cache the result (1 hour TTL default) 278 + 279 + This allows rules like `HandleEndsWith(".blacksky.team")` to work even though the JWT only contains the DID.
+1998 -498
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 - name = "addr2line" 7 - version = "0.24.2" 6 + name = "abnf" 7 + version = "0.13.0" 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 - checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 9 + checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" 10 10 dependencies = [ 11 - "gimli", 11 + "abnf-core", 12 + "nom 7.1.3", 13 + ] 14 + 15 + [[package]] 16 + name = "abnf-core" 17 + version = "0.5.0" 18 + source = "registry+https://github.com/rust-lang/crates.io-index" 19 + checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" 20 + dependencies = [ 21 + "nom 7.1.3", 12 22 ] 13 23 14 24 [[package]] ··· 31 41 32 42 [[package]] 33 43 name = "aho-corasick" 34 - version = "1.1.3" 44 + version = "1.1.4" 35 45 source = "registry+https://github.com/rust-lang/crates.io-index" 36 - checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 46 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 37 47 dependencies = [ 38 48 "memchr", 39 49 ] 40 50 41 51 [[package]] 52 + name = "aliasable" 53 + version = "0.1.3" 54 + source = "registry+https://github.com/rust-lang/crates.io-index" 55 + checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 56 + 57 + [[package]] 42 58 name = "allocator-api2" 43 59 version = "0.2.21" 44 60 source = "registry+https://github.com/rust-lang/crates.io-index" 45 61 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 46 62 47 63 [[package]] 48 - name = "android-tzdata" 49 - version = "0.1.1" 50 - source = "registry+https://github.com/rust-lang/crates.io-index" 51 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 - 53 - [[package]] 54 64 name = "android_system_properties" 55 65 version = "0.1.5" 56 66 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 61 71 62 72 [[package]] 63 73 name = "anyhow" 64 - version = "1.0.99" 74 + version = "1.0.100" 65 75 source = "registry+https://github.com/rust-lang/crates.io-index" 66 - checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 76 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 77 + 78 + [[package]] 79 + name = "ar_archive_writer" 80 + version = "0.2.0" 81 + source = "registry+https://github.com/rust-lang/crates.io-index" 82 + checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" 83 + dependencies = [ 84 + "object", 85 + ] 67 86 68 87 [[package]] 69 88 name = "async-compression" 70 - version = "0.4.27" 89 + version = "0.4.36" 71 90 source = "registry+https://github.com/rust-lang/crates.io-index" 72 - checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" 91 + checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" 73 92 dependencies = [ 93 + "compression-codecs", 94 + "compression-core", 74 95 "futures-core", 75 - "memchr", 76 96 "pin-project-lite", 77 97 "tokio", 78 - "zstd", 79 - "zstd-safe", 80 98 ] 81 99 82 100 [[package]] ··· 87 105 dependencies = [ 88 106 "proc-macro2", 89 107 "quote", 90 - "syn", 108 + "syn 2.0.112", 91 109 ] 92 110 93 111 [[package]] ··· 100 118 ] 101 119 102 120 [[package]] 121 + name = "atomic-polyfill" 122 + version = "1.0.3" 123 + source = "registry+https://github.com/rust-lang/crates.io-index" 124 + checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 125 + dependencies = [ 126 + "critical-section", 127 + ] 128 + 129 + [[package]] 103 130 name = "atomic-waker" 104 131 version = "1.1.2" 105 132 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 112 139 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 113 140 114 141 [[package]] 142 + name = "aws-lc-rs" 143 + version = "1.15.2" 144 + source = "registry+https://github.com/rust-lang/crates.io-index" 145 + checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" 146 + dependencies = [ 147 + "aws-lc-sys", 148 + "untrusted 0.7.1", 149 + "zeroize", 150 + ] 151 + 152 + [[package]] 153 + name = "aws-lc-sys" 154 + version = "0.35.0" 155 + source = "registry+https://github.com/rust-lang/crates.io-index" 156 + checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" 157 + dependencies = [ 158 + "cc", 159 + "cmake", 160 + "dunce", 161 + "fs_extra", 162 + ] 163 + 164 + [[package]] 115 165 name = "axum" 116 - version = "0.8.4" 166 + version = "0.8.8" 117 167 source = "registry+https://github.com/rust-lang/crates.io-index" 118 - checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 168 + checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" 119 169 dependencies = [ 120 170 "axum-core", 121 171 "axum-macros", ··· 133 183 "mime", 134 184 "percent-encoding", 135 185 "pin-project-lite", 136 - "rustversion", 137 - "serde", 186 + "serde_core", 138 187 "serde_json", 139 188 "serde_path_to_error", 140 189 "serde_urlencoded", ··· 148 197 149 198 [[package]] 150 199 name = "axum-core" 151 - version = "0.5.2" 200 + version = "0.5.6" 152 201 source = "registry+https://github.com/rust-lang/crates.io-index" 153 - checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 202 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" 154 203 dependencies = [ 155 204 "bytes", 156 205 "futures-core", ··· 159 208 "http-body-util", 160 209 "mime", 161 210 "pin-project-lite", 162 - "rustversion", 163 211 "sync_wrapper", 164 212 "tower-layer", 165 213 "tower-service", ··· 174 222 dependencies = [ 175 223 "proc-macro2", 176 224 "quote", 177 - "syn", 225 + "syn 2.0.112", 178 226 ] 179 227 180 228 [[package]] ··· 186 234 "axum", 187 235 "handlebars", 188 236 "serde", 189 - "thiserror 2.0.14", 237 + "thiserror 2.0.17", 190 238 ] 191 239 192 240 [[package]] 193 - name = "backtrace" 194 - version = "0.3.75" 241 + name = "base-x" 242 + version = "0.2.11" 243 + source = "registry+https://github.com/rust-lang/crates.io-index" 244 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 245 + 246 + [[package]] 247 + name = "base16ct" 248 + version = "0.2.0" 195 249 source = "registry+https://github.com/rust-lang/crates.io-index" 196 - checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 250 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 251 + 252 + [[package]] 253 + name = "base256emoji" 254 + version = "1.0.2" 255 + source = "registry+https://github.com/rust-lang/crates.io-index" 256 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 197 257 dependencies = [ 198 - "addr2line", 199 - "cfg-if", 200 - "libc", 201 - "miniz_oxide", 202 - "object", 203 - "rustc-demangle", 204 - "windows-targets 0.52.6", 258 + "const-str", 259 + "match-lookup", 205 260 ] 206 261 207 262 [[package]] ··· 212 267 213 268 [[package]] 214 269 name = "base64ct" 215 - version = "1.8.0" 270 + version = "1.8.1" 216 271 source = "registry+https://github.com/rust-lang/crates.io-index" 217 - checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 272 + checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" 218 273 219 274 [[package]] 220 275 name = "bitflags" 221 - version = "2.9.1" 276 + version = "2.10.0" 222 277 source = "registry+https://github.com/rust-lang/crates.io-index" 223 - checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 278 + checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 224 279 dependencies = [ 225 - "serde", 280 + "serde_core", 226 281 ] 227 282 228 283 [[package]] ··· 235 290 ] 236 291 237 292 [[package]] 293 + name = "bon" 294 + version = "3.8.1" 295 + source = "registry+https://github.com/rust-lang/crates.io-index" 296 + checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" 297 + dependencies = [ 298 + "bon-macros", 299 + "rustversion", 300 + ] 301 + 302 + [[package]] 303 + name = "bon-macros" 304 + version = "3.8.1" 305 + source = "registry+https://github.com/rust-lang/crates.io-index" 306 + checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" 307 + dependencies = [ 308 + "darling 0.21.3", 309 + "ident_case", 310 + "prettyplease", 311 + "proc-macro2", 312 + "quote", 313 + "rustversion", 314 + "syn 2.0.112", 315 + ] 316 + 317 + [[package]] 318 + name = "borsh" 319 + version = "1.6.0" 320 + source = "registry+https://github.com/rust-lang/crates.io-index" 321 + checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" 322 + dependencies = [ 323 + "cfg_aliases", 324 + ] 325 + 326 + [[package]] 238 327 name = "bstr" 239 - version = "1.12.0" 328 + version = "1.12.1" 240 329 source = "registry+https://github.com/rust-lang/crates.io-index" 241 - checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 330 + checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 242 331 dependencies = [ 243 332 "memchr", 244 333 "serde", 245 334 ] 246 335 247 336 [[package]] 337 + name = "btree-range-map" 338 + version = "0.7.2" 339 + source = "registry+https://github.com/rust-lang/crates.io-index" 340 + checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" 341 + dependencies = [ 342 + "btree-slab", 343 + "cc-traits", 344 + "range-traits", 345 + "serde", 346 + "slab", 347 + ] 348 + 349 + [[package]] 350 + name = "btree-slab" 351 + version = "0.6.1" 352 + source = "registry+https://github.com/rust-lang/crates.io-index" 353 + checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" 354 + dependencies = [ 355 + "cc-traits", 356 + "slab", 357 + "smallvec", 358 + ] 359 + 360 + [[package]] 248 361 name = "bumpalo" 249 - version = "3.19.0" 362 + version = "3.19.1" 250 363 source = "registry+https://github.com/rust-lang/crates.io-index" 251 - checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 364 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 252 365 253 366 [[package]] 254 367 name = "byteorder" ··· 258 371 259 372 [[package]] 260 373 name = "bytes" 261 - version = "1.10.1" 374 + version = "1.11.0" 375 + source = "registry+https://github.com/rust-lang/crates.io-index" 376 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 377 + dependencies = [ 378 + "serde", 379 + ] 380 + 381 + [[package]] 382 + name = "cbor4ii" 383 + version = "0.2.14" 262 384 source = "registry+https://github.com/rust-lang/crates.io-index" 263 - checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 385 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 386 + dependencies = [ 387 + "serde", 388 + ] 264 389 265 390 [[package]] 266 391 name = "cc" 267 - version = "1.2.32" 392 + version = "1.2.51" 268 393 source = "registry+https://github.com/rust-lang/crates.io-index" 269 - checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" 394 + checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" 270 395 dependencies = [ 396 + "find-msvc-tools", 271 397 "jobserver", 272 398 "libc", 273 399 "shlex", 274 400 ] 275 401 276 402 [[package]] 403 + name = "cc-traits" 404 + version = "2.0.0" 405 + source = "registry+https://github.com/rust-lang/crates.io-index" 406 + checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" 407 + dependencies = [ 408 + "slab", 409 + ] 410 + 411 + [[package]] 277 412 name = "cfg-if" 278 - version = "1.0.1" 413 + version = "1.0.4" 279 414 source = "registry+https://github.com/rust-lang/crates.io-index" 280 - checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 415 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 416 + 417 + [[package]] 418 + name = "cfg_aliases" 419 + version = "0.2.1" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 281 422 282 423 [[package]] 283 424 name = "chrono" 284 - version = "0.4.41" 425 + version = "0.4.42" 285 426 source = "registry+https://github.com/rust-lang/crates.io-index" 286 - checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 427 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 287 428 dependencies = [ 288 - "android-tzdata", 289 429 "iana-time-zone", 290 430 "js-sys", 291 431 "num-traits", 432 + "serde", 292 433 "wasm-bindgen", 293 434 "windows-link", 294 435 ] ··· 331 472 ] 332 473 333 474 [[package]] 475 + name = "cid" 476 + version = "0.11.1" 477 + source = "registry+https://github.com/rust-lang/crates.io-index" 478 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 479 + dependencies = [ 480 + "core2", 481 + "multibase", 482 + "multihash", 483 + "serde", 484 + "serde_bytes", 485 + "unsigned-varint", 486 + ] 487 + 488 + [[package]] 334 489 name = "cipher" 335 490 version = "0.4.4" 336 491 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 341 496 ] 342 497 343 498 [[package]] 499 + name = "cmake" 500 + version = "0.1.57" 501 + source = "registry+https://github.com/rust-lang/crates.io-index" 502 + checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" 503 + dependencies = [ 504 + "cc", 505 + ] 506 + 507 + [[package]] 508 + name = "cobs" 509 + version = "0.3.0" 510 + source = "registry+https://github.com/rust-lang/crates.io-index" 511 + checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" 512 + dependencies = [ 513 + "thiserror 2.0.17", 514 + ] 515 + 516 + [[package]] 517 + name = "compression-codecs" 518 + version = "0.4.35" 519 + source = "registry+https://github.com/rust-lang/crates.io-index" 520 + checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" 521 + dependencies = [ 522 + "compression-core", 523 + "flate2", 524 + "memchr", 525 + "zstd", 526 + "zstd-safe", 527 + ] 528 + 529 + [[package]] 530 + name = "compression-core" 531 + version = "0.4.31" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 534 + 535 + [[package]] 344 536 name = "concurrent-queue" 345 537 version = "2.5.0" 346 538 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 356 548 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 357 549 358 550 [[package]] 551 + name = "const-str" 552 + version = "0.4.3" 553 + source = "registry+https://github.com/rust-lang/crates.io-index" 554 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 555 + 556 + [[package]] 557 + name = "cordyceps" 558 + version = "0.3.4" 559 + source = "registry+https://github.com/rust-lang/crates.io-index" 560 + checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" 561 + dependencies = [ 562 + "loom", 563 + "tracing", 564 + ] 565 + 566 + [[package]] 359 567 name = "core-foundation" 360 568 version = "0.9.4" 361 569 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 372 580 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 373 581 374 582 [[package]] 583 + name = "core2" 584 + version = "0.4.0" 585 + source = "registry+https://github.com/rust-lang/crates.io-index" 586 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 587 + dependencies = [ 588 + "memchr", 589 + ] 590 + 591 + [[package]] 375 592 name = "cpufeatures" 376 593 version = "0.2.17" 377 594 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 382 599 383 600 [[package]] 384 601 name = "crc" 385 - version = "3.3.0" 602 + version = "3.4.0" 386 603 source = "registry+https://github.com/rust-lang/crates.io-index" 387 - checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 604 + checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" 388 605 dependencies = [ 389 606 "crc-catalog", 390 607 ] ··· 396 613 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 397 614 398 615 [[package]] 616 + name = "crc32fast" 617 + version = "1.5.0" 618 + source = "registry+https://github.com/rust-lang/crates.io-index" 619 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 620 + dependencies = [ 621 + "cfg-if", 622 + ] 623 + 624 + [[package]] 625 + name = "critical-section" 626 + version = "1.2.0" 627 + source = "registry+https://github.com/rust-lang/crates.io-index" 628 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 629 + 630 + [[package]] 399 631 name = "crossbeam-queue" 400 632 version = "0.3.12" 401 633 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 417 649 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 418 650 419 651 [[package]] 652 + name = "crypto-bigint" 653 + version = "0.5.5" 654 + source = "registry+https://github.com/rust-lang/crates.io-index" 655 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 656 + dependencies = [ 657 + "generic-array", 658 + "rand_core 0.6.4", 659 + "subtle", 660 + "zeroize", 661 + ] 662 + 663 + [[package]] 420 664 name = "crypto-common" 421 - version = "0.1.6" 665 + version = "0.1.7" 422 666 source = "registry+https://github.com/rust-lang/crates.io-index" 423 - checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 667 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 424 668 dependencies = [ 425 669 "generic-array", 426 670 "typenum", ··· 432 676 source = "registry+https://github.com/rust-lang/crates.io-index" 433 677 checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 434 678 dependencies = [ 435 - "darling_core", 436 - "darling_macro", 679 + "darling_core 0.20.11", 680 + "darling_macro 0.20.11", 681 + ] 682 + 683 + [[package]] 684 + name = "darling" 685 + version = "0.21.3" 686 + source = "registry+https://github.com/rust-lang/crates.io-index" 687 + checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" 688 + dependencies = [ 689 + "darling_core 0.21.3", 690 + "darling_macro 0.21.3", 437 691 ] 438 692 439 693 [[package]] ··· 447 701 "proc-macro2", 448 702 "quote", 449 703 "strsim", 450 - "syn", 704 + "syn 2.0.112", 705 + ] 706 + 707 + [[package]] 708 + name = "darling_core" 709 + version = "0.21.3" 710 + source = "registry+https://github.com/rust-lang/crates.io-index" 711 + checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" 712 + dependencies = [ 713 + "fnv", 714 + "ident_case", 715 + "proc-macro2", 716 + "quote", 717 + "strsim", 718 + "syn 2.0.112", 451 719 ] 452 720 453 721 [[package]] ··· 456 724 source = "registry+https://github.com/rust-lang/crates.io-index" 457 725 checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 458 726 dependencies = [ 459 - "darling_core", 727 + "darling_core 0.20.11", 460 728 "quote", 461 - "syn", 729 + "syn 2.0.112", 730 + ] 731 + 732 + [[package]] 733 + name = "darling_macro" 734 + version = "0.21.3" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" 737 + dependencies = [ 738 + "darling_core 0.21.3", 739 + "quote", 740 + "syn 2.0.112", 462 741 ] 463 742 464 743 [[package]] ··· 476 755 ] 477 756 478 757 [[package]] 758 + name = "data-encoding" 759 + version = "2.9.0" 760 + source = "registry+https://github.com/rust-lang/crates.io-index" 761 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 762 + 763 + [[package]] 764 + name = "data-encoding-macro" 765 + version = "0.1.18" 766 + source = "registry+https://github.com/rust-lang/crates.io-index" 767 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 768 + dependencies = [ 769 + "data-encoding", 770 + "data-encoding-macro-internal", 771 + ] 772 + 773 + [[package]] 774 + name = "data-encoding-macro-internal" 775 + version = "0.1.16" 776 + source = "registry+https://github.com/rust-lang/crates.io-index" 777 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 778 + dependencies = [ 779 + "data-encoding", 780 + "syn 2.0.112", 781 + ] 782 + 783 + [[package]] 479 784 name = "der" 480 785 version = "0.7.10" 481 786 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 487 792 ] 488 793 489 794 [[package]] 795 + name = "deranged" 796 + version = "0.5.5" 797 + source = "registry+https://github.com/rust-lang/crates.io-index" 798 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 799 + dependencies = [ 800 + "powerfmt", 801 + "serde_core", 802 + ] 803 + 804 + [[package]] 490 805 name = "derive_builder" 491 806 version = "0.20.2" 492 807 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 501 816 source = "registry+https://github.com/rust-lang/crates.io-index" 502 817 checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 503 818 dependencies = [ 504 - "darling", 819 + "darling 0.20.11", 505 820 "proc-macro2", 506 821 "quote", 507 - "syn", 822 + "syn 2.0.112", 508 823 ] 509 824 510 825 [[package]] ··· 514 829 checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 515 830 dependencies = [ 516 831 "derive_builder_core", 517 - "syn", 832 + "syn 2.0.112", 833 + ] 834 + 835 + [[package]] 836 + name = "derive_more" 837 + version = "1.0.0" 838 + source = "registry+https://github.com/rust-lang/crates.io-index" 839 + checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 840 + dependencies = [ 841 + "derive_more-impl", 518 842 ] 519 843 520 844 [[package]] 845 + name = "derive_more-impl" 846 + version = "1.0.0" 847 + source = "registry+https://github.com/rust-lang/crates.io-index" 848 + checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 849 + dependencies = [ 850 + "proc-macro2", 851 + "quote", 852 + "syn 2.0.112", 853 + "unicode-xid", 854 + ] 855 + 856 + [[package]] 857 + name = "diatomic-waker" 858 + version = "0.2.3" 859 + source = "registry+https://github.com/rust-lang/crates.io-index" 860 + checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" 861 + 862 + [[package]] 521 863 name = "digest" 522 864 version = "0.10.7" 523 865 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 537 879 dependencies = [ 538 880 "proc-macro2", 539 881 "quote", 540 - "syn", 882 + "syn 2.0.112", 541 883 ] 542 884 543 885 [[package]] ··· 547 889 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 548 890 549 891 [[package]] 892 + name = "dunce" 893 + version = "1.0.5" 894 + source = "registry+https://github.com/rust-lang/crates.io-index" 895 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 896 + 897 + [[package]] 898 + name = "dyn-clone" 899 + version = "1.0.20" 900 + source = "registry+https://github.com/rust-lang/crates.io-index" 901 + checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 902 + 903 + [[package]] 904 + name = "ecdsa" 905 + version = "0.16.9" 906 + source = "registry+https://github.com/rust-lang/crates.io-index" 907 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 908 + dependencies = [ 909 + "der", 910 + "digest", 911 + "elliptic-curve", 912 + "rfc6979", 913 + "signature", 914 + "spki", 915 + ] 916 + 917 + [[package]] 550 918 name = "either" 551 919 version = "1.15.0" 552 920 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 556 924 ] 557 925 558 926 [[package]] 927 + name = "elliptic-curve" 928 + version = "0.13.8" 929 + source = "registry+https://github.com/rust-lang/crates.io-index" 930 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 931 + dependencies = [ 932 + "base16ct", 933 + "crypto-bigint", 934 + "digest", 935 + "ff", 936 + "generic-array", 937 + "group", 938 + "pem-rfc7468", 939 + "pkcs8", 940 + "rand_core 0.6.4", 941 + "sec1", 942 + "subtle", 943 + "zeroize", 944 + ] 945 + 946 + [[package]] 559 947 name = "email-encoding" 560 948 version = "0.4.1" 561 949 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 572 960 checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 573 961 574 962 [[package]] 963 + name = "embedded-io" 964 + version = "0.4.0" 965 + source = "registry+https://github.com/rust-lang/crates.io-index" 966 + checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" 967 + 968 + [[package]] 969 + name = "embedded-io" 970 + version = "0.6.1" 971 + source = "registry+https://github.com/rust-lang/crates.io-index" 972 + checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" 973 + 974 + [[package]] 975 + name = "encoding_rs" 976 + version = "0.8.35" 977 + source = "registry+https://github.com/rust-lang/crates.io-index" 978 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 979 + dependencies = [ 980 + "cfg-if", 981 + ] 982 + 983 + [[package]] 575 984 name = "equivalent" 576 985 version = "1.0.2" 577 986 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 579 988 580 989 [[package]] 581 990 name = "errno" 582 - version = "0.3.13" 991 + version = "0.3.14" 583 992 source = "registry+https://github.com/rust-lang/crates.io-index" 584 - checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 993 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 585 994 dependencies = [ 586 995 "libc", 587 996 "windows-sys 0.59.0", ··· 616 1025 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 617 1026 618 1027 [[package]] 1028 + name = "ff" 1029 + version = "0.13.1" 1030 + source = "registry+https://github.com/rust-lang/crates.io-index" 1031 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1032 + dependencies = [ 1033 + "rand_core 0.6.4", 1034 + "subtle", 1035 + ] 1036 + 1037 + [[package]] 1038 + name = "find-msvc-tools" 1039 + version = "0.1.6" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" 1042 + 1043 + [[package]] 1044 + name = "flate2" 1045 + version = "1.1.5" 1046 + source = "registry+https://github.com/rust-lang/crates.io-index" 1047 + checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 1048 + dependencies = [ 1049 + "crc32fast", 1050 + "miniz_oxide", 1051 + ] 1052 + 1053 + [[package]] 619 1054 name = "flume" 620 1055 version = "0.11.1" 621 1056 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 623 1058 dependencies = [ 624 1059 "futures-core", 625 1060 "futures-sink", 626 - "spin", 1061 + "spin 0.9.8", 627 1062 ] 628 1063 629 1064 [[package]] ··· 639 1074 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 640 1075 641 1076 [[package]] 1077 + name = "foldhash" 1078 + version = "0.2.0" 1079 + source = "registry+https://github.com/rust-lang/crates.io-index" 1080 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 1081 + 1082 + [[package]] 642 1083 name = "foreign-types" 643 1084 version = "0.3.2" 644 1085 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 655 1096 656 1097 [[package]] 657 1098 name = "form_urlencoded" 658 - version = "1.2.1" 1099 + version = "1.2.2" 659 1100 source = "registry+https://github.com/rust-lang/crates.io-index" 660 - checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 1101 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 661 1102 dependencies = [ 662 1103 "percent-encoding", 663 1104 ] ··· 673 1114 ] 674 1115 675 1116 [[package]] 1117 + name = "fs_extra" 1118 + version = "1.3.0" 1119 + source = "registry+https://github.com/rust-lang/crates.io-index" 1120 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 1121 + 1122 + [[package]] 1123 + name = "futures-buffered" 1124 + version = "0.2.12" 1125 + source = "registry+https://github.com/rust-lang/crates.io-index" 1126 + checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" 1127 + dependencies = [ 1128 + "cordyceps", 1129 + "diatomic-waker", 1130 + "futures-core", 1131 + "pin-project-lite", 1132 + "spin 0.10.0", 1133 + ] 1134 + 1135 + [[package]] 676 1136 name = "futures-channel" 677 1137 version = "0.3.31" 678 1138 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 717 1177 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 718 1178 719 1179 [[package]] 1180 + name = "futures-lite" 1181 + version = "2.6.1" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" 1184 + dependencies = [ 1185 + "fastrand", 1186 + "futures-core", 1187 + "futures-io", 1188 + "parking", 1189 + "pin-project-lite", 1190 + ] 1191 + 1192 + [[package]] 1193 + name = "futures-macro" 1194 + version = "0.3.31" 1195 + source = "registry+https://github.com/rust-lang/crates.io-index" 1196 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 1197 + dependencies = [ 1198 + "proc-macro2", 1199 + "quote", 1200 + "syn 2.0.112", 1201 + ] 1202 + 1203 + [[package]] 720 1204 name = "futures-sink" 721 1205 version = "0.3.31" 722 1206 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 742 1226 dependencies = [ 743 1227 "futures-core", 744 1228 "futures-io", 1229 + "futures-macro", 745 1230 "futures-sink", 746 1231 "futures-task", 747 1232 "memchr", ··· 751 1236 ] 752 1237 753 1238 [[package]] 1239 + name = "generator" 1240 + version = "0.8.8" 1241 + source = "registry+https://github.com/rust-lang/crates.io-index" 1242 + checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" 1243 + dependencies = [ 1244 + "cc", 1245 + "cfg-if", 1246 + "libc", 1247 + "log", 1248 + "rustversion", 1249 + "windows-link", 1250 + "windows-result", 1251 + ] 1252 + 1253 + [[package]] 754 1254 name = "generic-array" 755 1255 version = "0.14.7" 756 1256 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 758 1258 dependencies = [ 759 1259 "typenum", 760 1260 "version_check", 1261 + "zeroize", 761 1262 ] 762 1263 763 1264 [[package]] ··· 767 1268 checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 768 1269 dependencies = [ 769 1270 "cfg-if", 1271 + "js-sys", 770 1272 "libc", 771 - "wasi 0.11.1+wasi-snapshot-preview1", 1273 + "wasi", 1274 + "wasm-bindgen", 772 1275 ] 773 1276 774 1277 [[package]] 775 1278 name = "getrandom" 776 - version = "0.3.3" 1279 + version = "0.3.4" 777 1280 source = "registry+https://github.com/rust-lang/crates.io-index" 778 - checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1281 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 779 1282 dependencies = [ 780 1283 "cfg-if", 781 1284 "js-sys", 782 1285 "libc", 783 1286 "r-efi", 784 - "wasi 0.14.2+wasi-0.2.4", 1287 + "wasip2", 785 1288 "wasm-bindgen", 786 1289 ] 787 1290 788 1291 [[package]] 789 - name = "gimli" 790 - version = "0.31.1" 791 - source = "registry+https://github.com/rust-lang/crates.io-index" 792 - checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 793 - 794 - [[package]] 795 1292 name = "globset" 796 - version = "0.4.16" 1293 + version = "0.4.18" 797 1294 source = "registry+https://github.com/rust-lang/crates.io-index" 798 - checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 1295 + checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" 799 1296 dependencies = [ 800 1297 "aho-corasick", 801 1298 "bstr", 802 1299 "log", 803 - "regex-automata 0.4.9", 804 - "regex-syntax 0.8.5", 1300 + "regex-automata", 1301 + "regex-syntax", 805 1302 ] 806 1303 807 1304 [[package]] 808 1305 name = "governor" 809 - version = "0.10.1" 1306 + version = "0.10.4" 810 1307 source = "registry+https://github.com/rust-lang/crates.io-index" 811 - checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" 1308 + checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" 812 1309 dependencies = [ 813 1310 "cfg-if", 814 1311 "dashmap", 815 1312 "futures-sink", 816 1313 "futures-timer", 817 1314 "futures-util", 818 - "getrandom 0.3.3", 819 - "hashbrown 0.15.5", 1315 + "getrandom 0.3.4", 1316 + "hashbrown 0.16.1", 820 1317 "nonzero_ext", 821 1318 "parking_lot", 822 1319 "portable-atomic", ··· 828 1325 ] 829 1326 830 1327 [[package]] 1328 + name = "group" 1329 + version = "0.13.0" 1330 + source = "registry+https://github.com/rust-lang/crates.io-index" 1331 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1332 + dependencies = [ 1333 + "ff", 1334 + "rand_core 0.6.4", 1335 + "subtle", 1336 + ] 1337 + 1338 + [[package]] 831 1339 name = "h2" 832 1340 version = "0.4.12" 833 1341 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 839 1347 "futures-core", 840 1348 "futures-sink", 841 1349 "http", 842 - "indexmap", 1350 + "indexmap 2.12.1", 843 1351 "slab", 844 1352 "tokio", 845 1353 "tokio-util", ··· 848 1356 849 1357 [[package]] 850 1358 name = "half" 851 - version = "2.6.0" 1359 + version = "2.7.1" 852 1360 source = "registry+https://github.com/rust-lang/crates.io-index" 853 - checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 1361 + checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" 854 1362 dependencies = [ 855 1363 "cfg-if", 856 1364 "crunchy", 1365 + "zerocopy", 857 1366 ] 858 1367 859 1368 [[package]] 860 1369 name = "handlebars" 861 - version = "6.3.2" 1370 + version = "6.4.0" 862 1371 source = "registry+https://github.com/rust-lang/crates.io-index" 863 - checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" 1372 + checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" 864 1373 dependencies = [ 865 1374 "derive_builder", 866 1375 "log", ··· 870 1379 "rust-embed", 871 1380 "serde", 872 1381 "serde_json", 873 - "thiserror 2.0.14", 1382 + "thiserror 2.0.17", 874 1383 ] 1384 + 1385 + [[package]] 1386 + name = "hash32" 1387 + version = "0.2.1" 1388 + source = "registry+https://github.com/rust-lang/crates.io-index" 1389 + checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 1390 + dependencies = [ 1391 + "byteorder", 1392 + ] 1393 + 1394 + [[package]] 1395 + name = "hashbrown" 1396 + version = "0.12.3" 1397 + source = "registry+https://github.com/rust-lang/crates.io-index" 1398 + checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 875 1399 876 1400 [[package]] 877 1401 name = "hashbrown" ··· 891 1415 dependencies = [ 892 1416 "allocator-api2", 893 1417 "equivalent", 894 - "foldhash", 1418 + "foldhash 0.1.5", 1419 + ] 1420 + 1421 + [[package]] 1422 + name = "hashbrown" 1423 + version = "0.16.1" 1424 + source = "registry+https://github.com/rust-lang/crates.io-index" 1425 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1426 + dependencies = [ 1427 + "allocator-api2", 1428 + "equivalent", 1429 + "foldhash 0.2.0", 895 1430 ] 896 1431 897 1432 [[package]] ··· 904 1439 ] 905 1440 906 1441 [[package]] 1442 + name = "heapless" 1443 + version = "0.7.17" 1444 + source = "registry+https://github.com/rust-lang/crates.io-index" 1445 + checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 1446 + dependencies = [ 1447 + "atomic-polyfill", 1448 + "hash32", 1449 + "rustc_version", 1450 + "serde", 1451 + "spin 0.9.8", 1452 + "stable_deref_trait", 1453 + ] 1454 + 1455 + [[package]] 1456 + name = "heck" 1457 + version = "0.4.1" 1458 + source = "registry+https://github.com/rust-lang/crates.io-index" 1459 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 1460 + 1461 + [[package]] 907 1462 name = "heck" 908 1463 version = "0.5.0" 909 1464 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 914 1469 version = "0.4.3" 915 1470 source = "registry+https://github.com/rust-lang/crates.io-index" 916 1471 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1472 + 1473 + [[package]] 1474 + name = "hex_fmt" 1475 + version = "0.3.0" 1476 + source = "registry+https://github.com/rust-lang/crates.io-index" 1477 + checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" 917 1478 918 1479 [[package]] 919 1480 name = "hkdf" ··· 935 1496 936 1497 [[package]] 937 1498 name = "home" 938 - version = "0.5.11" 1499 + version = "0.5.12" 939 1500 source = "registry+https://github.com/rust-lang/crates.io-index" 940 - checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 1501 + checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" 941 1502 dependencies = [ 942 - "windows-sys 0.59.0", 1503 + "windows-sys 0.61.2", 943 1504 ] 944 1505 945 1506 [[package]] 946 - name = "hostname" 947 - version = "0.4.1" 1507 + name = "html-escape" 1508 + version = "0.2.13" 948 1509 source = "registry+https://github.com/rust-lang/crates.io-index" 949 - checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" 1510 + checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" 950 1511 dependencies = [ 951 - "cfg-if", 952 - "libc", 953 - "windows-link", 1512 + "utf8-width", 954 1513 ] 955 1514 956 1515 [[package]] 957 1516 name = "http" 958 - version = "1.3.1" 1517 + version = "1.4.0" 959 1518 source = "registry+https://github.com/rust-lang/crates.io-index" 960 - checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1519 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 961 1520 dependencies = [ 962 1521 "bytes", 963 - "fnv", 964 1522 "itoa", 965 1523 ] 966 1524 ··· 1001 1559 1002 1560 [[package]] 1003 1561 name = "hyper" 1004 - version = "1.6.0" 1562 + version = "1.8.1" 1005 1563 source = "registry+https://github.com/rust-lang/crates.io-index" 1006 - checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 1564 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 1007 1565 dependencies = [ 1566 + "atomic-waker", 1008 1567 "bytes", 1009 1568 "futures-channel", 1010 - "futures-util", 1569 + "futures-core", 1011 1570 "h2", 1012 1571 "http", 1013 1572 "http-body", ··· 1015 1574 "httpdate", 1016 1575 "itoa", 1017 1576 "pin-project-lite", 1577 + "pin-utils", 1018 1578 "smallvec", 1019 1579 "tokio", 1020 1580 "want", 1021 1581 ] 1022 1582 1023 1583 [[package]] 1584 + name = "hyper-rustls" 1585 + version = "0.27.7" 1586 + source = "registry+https://github.com/rust-lang/crates.io-index" 1587 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1588 + dependencies = [ 1589 + "http", 1590 + "hyper", 1591 + "hyper-util", 1592 + "rustls", 1593 + "rustls-pki-types", 1594 + "tokio", 1595 + "tokio-rustls", 1596 + "tower-service", 1597 + "webpki-roots 1.0.5", 1598 + ] 1599 + 1600 + [[package]] 1024 1601 name = "hyper-timeout" 1025 1602 version = "0.5.2" 1026 1603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1035 1612 1036 1613 [[package]] 1037 1614 name = "hyper-util" 1038 - version = "0.1.16" 1615 + version = "0.1.19" 1039 1616 source = "registry+https://github.com/rust-lang/crates.io-index" 1040 - checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 1617 + checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" 1041 1618 dependencies = [ 1619 + "base64", 1042 1620 "bytes", 1043 1621 "futures-channel", 1044 1622 "futures-core", ··· 1046 1624 "http", 1047 1625 "http-body", 1048 1626 "hyper", 1627 + "ipnet", 1049 1628 "libc", 1629 + "percent-encoding", 1050 1630 "pin-project-lite", 1051 1631 "socket2", 1632 + "system-configuration", 1052 1633 "tokio", 1053 1634 "tower-service", 1054 1635 "tracing", 1636 + "windows-registry", 1055 1637 ] 1056 1638 1057 1639 [[package]] 1058 1640 name = "iana-time-zone" 1059 - version = "0.1.63" 1641 + version = "0.1.64" 1060 1642 source = "registry+https://github.com/rust-lang/crates.io-index" 1061 - checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1643 + checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 1062 1644 dependencies = [ 1063 1645 "android_system_properties", 1064 1646 "core-foundation-sys", ··· 1080 1662 1081 1663 [[package]] 1082 1664 name = "icu_collections" 1083 - version = "2.0.0" 1665 + version = "2.1.1" 1084 1666 source = "registry+https://github.com/rust-lang/crates.io-index" 1085 - checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1667 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 1086 1668 dependencies = [ 1087 1669 "displaydoc", 1088 1670 "potential_utf", ··· 1093 1675 1094 1676 [[package]] 1095 1677 name = "icu_locale_core" 1096 - version = "2.0.0" 1678 + version = "2.1.1" 1097 1679 source = "registry+https://github.com/rust-lang/crates.io-index" 1098 - checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1680 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 1099 1681 dependencies = [ 1100 1682 "displaydoc", 1101 1683 "litemap", ··· 1106 1688 1107 1689 [[package]] 1108 1690 name = "icu_normalizer" 1109 - version = "2.0.0" 1691 + version = "2.1.1" 1110 1692 source = "registry+https://github.com/rust-lang/crates.io-index" 1111 - checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1693 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 1112 1694 dependencies = [ 1113 - "displaydoc", 1114 1695 "icu_collections", 1115 1696 "icu_normalizer_data", 1116 1697 "icu_properties", ··· 1121 1702 1122 1703 [[package]] 1123 1704 name = "icu_normalizer_data" 1124 - version = "2.0.0" 1705 + version = "2.1.1" 1125 1706 source = "registry+https://github.com/rust-lang/crates.io-index" 1126 - checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1707 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 1127 1708 1128 1709 [[package]] 1129 1710 name = "icu_properties" 1130 - version = "2.0.1" 1711 + version = "2.1.2" 1131 1712 source = "registry+https://github.com/rust-lang/crates.io-index" 1132 - checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1713 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 1133 1714 dependencies = [ 1134 - "displaydoc", 1135 1715 "icu_collections", 1136 1716 "icu_locale_core", 1137 1717 "icu_properties_data", 1138 1718 "icu_provider", 1139 - "potential_utf", 1140 1719 "zerotrie", 1141 1720 "zerovec", 1142 1721 ] 1143 1722 1144 1723 [[package]] 1145 1724 name = "icu_properties_data" 1146 - version = "2.0.1" 1725 + version = "2.1.2" 1147 1726 source = "registry+https://github.com/rust-lang/crates.io-index" 1148 - checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1727 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 1149 1728 1150 1729 [[package]] 1151 1730 name = "icu_provider" 1152 - version = "2.0.0" 1731 + version = "2.1.1" 1153 1732 source = "registry+https://github.com/rust-lang/crates.io-index" 1154 - checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1733 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 1155 1734 dependencies = [ 1156 1735 "displaydoc", 1157 1736 "icu_locale_core", 1158 - "stable_deref_trait", 1159 - "tinystr", 1160 1737 "writeable", 1161 1738 "yoke", 1162 1739 "zerofrom", ··· 1172 1749 1173 1750 [[package]] 1174 1751 name = "idna" 1175 - version = "1.0.3" 1752 + version = "1.1.0" 1176 1753 source = "registry+https://github.com/rust-lang/crates.io-index" 1177 - checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1754 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1178 1755 dependencies = [ 1179 1756 "idna_adapter", 1180 1757 "smallvec", ··· 1193 1770 1194 1771 [[package]] 1195 1772 name = "indexmap" 1196 - version = "2.10.0" 1773 + version = "1.9.3" 1197 1774 source = "registry+https://github.com/rust-lang/crates.io-index" 1198 - checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 1775 + checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1776 + dependencies = [ 1777 + "autocfg", 1778 + "hashbrown 0.12.3", 1779 + "serde", 1780 + ] 1781 + 1782 + [[package]] 1783 + name = "indexmap" 1784 + version = "2.12.1" 1785 + source = "registry+https://github.com/rust-lang/crates.io-index" 1786 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1199 1787 dependencies = [ 1200 1788 "equivalent", 1201 - "hashbrown 0.15.5", 1789 + "hashbrown 0.16.1", 1790 + "serde", 1791 + "serde_core", 1792 + ] 1793 + 1794 + [[package]] 1795 + name = "indoc" 1796 + version = "2.0.7" 1797 + source = "registry+https://github.com/rust-lang/crates.io-index" 1798 + checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" 1799 + dependencies = [ 1800 + "rustversion", 1202 1801 ] 1203 1802 1204 1803 [[package]] ··· 1211 1810 ] 1212 1811 1213 1812 [[package]] 1214 - name = "io-uring" 1215 - version = "0.7.9" 1813 + name = "inventory" 1814 + version = "0.3.21" 1216 1815 source = "registry+https://github.com/rust-lang/crates.io-index" 1217 - checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" 1816 + checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" 1218 1817 dependencies = [ 1219 - "bitflags", 1220 - "cfg-if", 1221 - "libc", 1818 + "rustversion", 1819 + ] 1820 + 1821 + [[package]] 1822 + name = "ipld-core" 1823 + version = "0.4.2" 1824 + source = "registry+https://github.com/rust-lang/crates.io-index" 1825 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1826 + dependencies = [ 1827 + "cid", 1828 + "serde", 1829 + "serde_bytes", 1830 + ] 1831 + 1832 + [[package]] 1833 + name = "ipnet" 1834 + version = "2.11.0" 1835 + source = "registry+https://github.com/rust-lang/crates.io-index" 1836 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1837 + 1838 + [[package]] 1839 + name = "iri-string" 1840 + version = "0.7.10" 1841 + source = "registry+https://github.com/rust-lang/crates.io-index" 1842 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1843 + dependencies = [ 1844 + "memchr", 1845 + "serde", 1222 1846 ] 1223 1847 1224 1848 [[package]] 1225 1849 name = "itoa" 1226 - version = "1.0.15" 1850 + version = "1.0.17" 1227 1851 source = "registry+https://github.com/rust-lang/crates.io-index" 1228 - checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1852 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1853 + 1854 + [[package]] 1855 + name = "jacquard-api" 1856 + version = "0.9.5" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "4979fb1848c1dd7ac8fd12745bc71f56f6da61374407d5f9b06005467a954e5a" 1859 + dependencies = [ 1860 + "bon", 1861 + "bytes", 1862 + "jacquard-common", 1863 + "jacquard-derive", 1864 + "jacquard-lexicon", 1865 + "miette", 1866 + "rustversion", 1867 + "serde", 1868 + "serde_bytes", 1869 + "serde_ipld_dagcbor", 1870 + "thiserror 2.0.17", 1871 + "unicode-segmentation", 1872 + ] 1873 + 1874 + [[package]] 1875 + name = "jacquard-common" 1876 + version = "0.9.5" 1877 + source = "registry+https://github.com/rust-lang/crates.io-index" 1878 + checksum = "1751921e0bdae5e0077afade6161545e9ef7698306c868f800916e99ecbcaae9" 1879 + dependencies = [ 1880 + "base64", 1881 + "bon", 1882 + "bytes", 1883 + "chrono", 1884 + "cid", 1885 + "getrandom 0.2.16", 1886 + "getrandom 0.3.4", 1887 + "http", 1888 + "ipld-core", 1889 + "k256", 1890 + "langtag", 1891 + "miette", 1892 + "multibase", 1893 + "multihash", 1894 + "ouroboros", 1895 + "p256", 1896 + "postcard", 1897 + "rand 0.9.2", 1898 + "regex", 1899 + "regex-lite", 1900 + "reqwest", 1901 + "serde", 1902 + "serde_bytes", 1903 + "serde_html_form", 1904 + "serde_ipld_dagcbor", 1905 + "serde_json", 1906 + "signature", 1907 + "smol_str", 1908 + "thiserror 2.0.17", 1909 + "tokio", 1910 + "tokio-util", 1911 + "trait-variant", 1912 + "url", 1913 + ] 1914 + 1915 + [[package]] 1916 + name = "jacquard-derive" 1917 + version = "0.9.5" 1918 + source = "registry+https://github.com/rust-lang/crates.io-index" 1919 + checksum = "9c8d73dfee07943fdab93569ed1c28b06c6921ed891c08b415c4a323ff67e593" 1920 + dependencies = [ 1921 + "heck 0.5.0", 1922 + "jacquard-lexicon", 1923 + "proc-macro2", 1924 + "quote", 1925 + "syn 2.0.112", 1926 + ] 1927 + 1928 + [[package]] 1929 + name = "jacquard-identity" 1930 + version = "0.9.5" 1931 + source = "registry+https://github.com/rust-lang/crates.io-index" 1932 + checksum = "e7aaefa819fa4213cf59f180dba932f018a7cd0599582fd38474ee2a38c16cf2" 1933 + dependencies = [ 1934 + "bon", 1935 + "bytes", 1936 + "http", 1937 + "jacquard-api", 1938 + "jacquard-common", 1939 + "jacquard-lexicon", 1940 + "miette", 1941 + "n0-future", 1942 + "percent-encoding", 1943 + "reqwest", 1944 + "serde", 1945 + "serde_html_form", 1946 + "serde_json", 1947 + "thiserror 2.0.17", 1948 + "tokio", 1949 + "trait-variant", 1950 + "url", 1951 + "urlencoding", 1952 + ] 1953 + 1954 + [[package]] 1955 + name = "jacquard-lexicon" 1956 + version = "0.9.5" 1957 + source = "registry+https://github.com/rust-lang/crates.io-index" 1958 + checksum = "8411aff546569b0a1e0ef669bed2380cec1c00d48f02f3fcd57a71545321b3d8" 1959 + dependencies = [ 1960 + "cid", 1961 + "dashmap", 1962 + "heck 0.5.0", 1963 + "inventory", 1964 + "jacquard-common", 1965 + "miette", 1966 + "multihash", 1967 + "prettyplease", 1968 + "proc-macro2", 1969 + "quote", 1970 + "serde", 1971 + "serde_ipld_dagcbor", 1972 + "serde_json", 1973 + "serde_repr", 1974 + "serde_with", 1975 + "sha2", 1976 + "syn 2.0.112", 1977 + "thiserror 2.0.17", 1978 + "unicode-segmentation", 1979 + ] 1229 1980 1230 1981 [[package]] 1231 1982 name = "jobserver" 1232 - version = "0.1.33" 1983 + version = "0.1.34" 1233 1984 source = "registry+https://github.com/rust-lang/crates.io-index" 1234 - checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1985 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1235 1986 dependencies = [ 1236 - "getrandom 0.3.3", 1987 + "getrandom 0.3.4", 1237 1988 "libc", 1238 1989 ] 1239 1990 1240 1991 [[package]] 1992 + name = "josekit" 1993 + version = "0.10.3" 1994 + source = "registry+https://github.com/rust-lang/crates.io-index" 1995 + checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" 1996 + dependencies = [ 1997 + "anyhow", 1998 + "base64", 1999 + "flate2", 2000 + "openssl", 2001 + "regex", 2002 + "serde", 2003 + "serde_json", 2004 + "thiserror 2.0.17", 2005 + "time", 2006 + ] 2007 + 2008 + [[package]] 1241 2009 name = "js-sys" 1242 - version = "0.3.77" 2010 + version = "0.3.83" 1243 2011 source = "registry+https://github.com/rust-lang/crates.io-index" 1244 - checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 2012 + checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 1245 2013 dependencies = [ 1246 2014 "once_cell", 1247 2015 "wasm-bindgen", ··· 1270 2038 ] 1271 2039 1272 2040 [[package]] 2041 + name = "k256" 2042 + version = "0.13.4" 2043 + source = "registry+https://github.com/rust-lang/crates.io-index" 2044 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 2045 + dependencies = [ 2046 + "cfg-if", 2047 + "ecdsa", 2048 + "elliptic-curve", 2049 + "sha2", 2050 + ] 2051 + 2052 + [[package]] 2053 + name = "langtag" 2054 + version = "0.4.0" 2055 + source = "registry+https://github.com/rust-lang/crates.io-index" 2056 + checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" 2057 + dependencies = [ 2058 + "serde", 2059 + "static-regular-grammar", 2060 + "thiserror 1.0.69", 2061 + ] 2062 + 2063 + [[package]] 1273 2064 name = "lazy_static" 1274 2065 version = "1.5.0" 1275 2066 source = "registry+https://github.com/rust-lang/crates.io-index" 1276 2067 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1277 2068 dependencies = [ 1278 - "spin", 2069 + "spin 0.9.8", 1279 2070 ] 1280 2071 1281 2072 [[package]] 1282 2073 name = "lettre" 1283 - version = "0.11.18" 2074 + version = "0.11.19" 1284 2075 source = "registry+https://github.com/rust-lang/crates.io-index" 1285 - checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" 2076 + checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" 1286 2077 dependencies = [ 1287 2078 "async-trait", 1288 2079 "base64", ··· 1292 2083 "fastrand", 1293 2084 "futures-io", 1294 2085 "futures-util", 1295 - "hostname", 1296 2086 "httpdate", 1297 2087 "idna", 1298 2088 "mime", 1299 - "native-tls", 1300 - "nom", 2089 + "nom 8.0.0", 1301 2090 "percent-encoding", 1302 2091 "quoted_printable", 2092 + "rustls", 1303 2093 "socket2", 1304 2094 "tokio", 1305 - "tokio-native-tls", 2095 + "tokio-rustls", 1306 2096 "url", 2097 + "webpki-roots 1.0.5", 1307 2098 ] 1308 2099 1309 2100 [[package]] 1310 2101 name = "libc" 1311 - version = "0.2.175" 2102 + version = "0.2.178" 1312 2103 source = "registry+https://github.com/rust-lang/crates.io-index" 1313 - checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 2104 + checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" 1314 2105 1315 2106 [[package]] 1316 2107 name = "libm" ··· 1320 2111 1321 2112 [[package]] 1322 2113 name = "libredox" 1323 - version = "0.1.9" 2114 + version = "0.1.12" 1324 2115 source = "registry+https://github.com/rust-lang/crates.io-index" 1325 - checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 2116 + checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1326 2117 dependencies = [ 1327 2118 "bitflags", 1328 2119 "libc", 1329 - "redox_syscall", 2120 + "redox_syscall 0.7.0", 1330 2121 ] 1331 2122 1332 2123 [[package]] ··· 1341 2132 ] 1342 2133 1343 2134 [[package]] 1344 - name = "linux-raw-sys" 1345 - version = "0.9.4" 1346 - source = "registry+https://github.com/rust-lang/crates.io-index" 1347 - checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1348 - 1349 - [[package]] 1350 2135 name = "litemap" 1351 - version = "0.8.0" 2136 + version = "0.8.1" 1352 2137 source = "registry+https://github.com/rust-lang/crates.io-index" 1353 - checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 2138 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1354 2139 1355 2140 [[package]] 1356 2141 name = "lock_api" 1357 - version = "0.4.13" 2142 + version = "0.4.14" 1358 2143 source = "registry+https://github.com/rust-lang/crates.io-index" 1359 - checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 2144 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1360 2145 dependencies = [ 1361 - "autocfg", 1362 2146 "scopeguard", 1363 2147 ] 1364 2148 1365 2149 [[package]] 1366 2150 name = "log" 1367 - version = "0.4.27" 2151 + version = "0.4.29" 2152 + source = "registry+https://github.com/rust-lang/crates.io-index" 2153 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 2154 + 2155 + [[package]] 2156 + name = "loom" 2157 + version = "0.7.2" 2158 + source = "registry+https://github.com/rust-lang/crates.io-index" 2159 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 2160 + dependencies = [ 2161 + "cfg-if", 2162 + "generator", 2163 + "scoped-tls", 2164 + "tracing", 2165 + "tracing-subscriber", 2166 + ] 2167 + 2168 + [[package]] 2169 + name = "lru-slab" 2170 + version = "0.1.2" 1368 2171 source = "registry+https://github.com/rust-lang/crates.io-index" 1369 - checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 2172 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2173 + 2174 + [[package]] 2175 + name = "match-lookup" 2176 + version = "0.1.1" 2177 + source = "registry+https://github.com/rust-lang/crates.io-index" 2178 + checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 2179 + dependencies = [ 2180 + "proc-macro2", 2181 + "quote", 2182 + "syn 1.0.109", 2183 + ] 1370 2184 1371 2185 [[package]] 1372 2186 name = "matchers" 1373 - version = "0.1.0" 2187 + version = "0.2.0" 1374 2188 source = "registry+https://github.com/rust-lang/crates.io-index" 1375 - checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 2189 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 1376 2190 dependencies = [ 1377 - "regex-automata 0.1.10", 2191 + "regex-automata", 1378 2192 ] 1379 2193 1380 2194 [[package]] ··· 1395 2209 1396 2210 [[package]] 1397 2211 name = "memchr" 1398 - version = "2.7.5" 2212 + version = "2.7.6" 2213 + source = "registry+https://github.com/rust-lang/crates.io-index" 2214 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 2215 + 2216 + [[package]] 2217 + name = "miette" 2218 + version = "7.6.0" 2219 + source = "registry+https://github.com/rust-lang/crates.io-index" 2220 + checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 2221 + dependencies = [ 2222 + "cfg-if", 2223 + "miette-derive", 2224 + "unicode-width", 2225 + ] 2226 + 2227 + [[package]] 2228 + name = "miette-derive" 2229 + version = "7.6.0" 1399 2230 source = "registry+https://github.com/rust-lang/crates.io-index" 1400 - checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 2231 + checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 2232 + dependencies = [ 2233 + "proc-macro2", 2234 + "quote", 2235 + "syn 2.0.112", 2236 + ] 1401 2237 1402 2238 [[package]] 1403 2239 name = "mime" ··· 1406 2242 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1407 2243 1408 2244 [[package]] 2245 + name = "minimal-lexical" 2246 + version = "0.2.1" 2247 + source = "registry+https://github.com/rust-lang/crates.io-index" 2248 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2249 + 2250 + [[package]] 1409 2251 name = "miniz_oxide" 1410 2252 version = "0.8.9" 1411 2253 source = "registry+https://github.com/rust-lang/crates.io-index" 1412 2254 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1413 2255 dependencies = [ 1414 2256 "adler2", 2257 + "simd-adler32", 1415 2258 ] 1416 2259 1417 2260 [[package]] 1418 2261 name = "mio" 1419 - version = "1.0.4" 2262 + version = "1.1.1" 1420 2263 source = "registry+https://github.com/rust-lang/crates.io-index" 1421 - checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2264 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 1422 2265 dependencies = [ 1423 2266 "libc", 1424 - "wasi 0.11.1+wasi-snapshot-preview1", 1425 - "windows-sys 0.59.0", 2267 + "wasi", 2268 + "windows-sys 0.61.2", 1426 2269 ] 1427 2270 1428 2271 [[package]] 1429 - name = "native-tls" 1430 - version = "0.2.14" 2272 + name = "multibase" 2273 + version = "0.9.2" 1431 2274 source = "registry+https://github.com/rust-lang/crates.io-index" 1432 - checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 2275 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1433 2276 dependencies = [ 1434 - "libc", 1435 - "log", 1436 - "openssl", 1437 - "openssl-probe", 1438 - "openssl-sys", 1439 - "schannel", 1440 - "security-framework", 1441 - "security-framework-sys", 1442 - "tempfile", 2277 + "base-x", 2278 + "base256emoji", 2279 + "data-encoding", 2280 + "data-encoding-macro", 2281 + ] 2282 + 2283 + [[package]] 2284 + name = "multihash" 2285 + version = "0.19.3" 2286 + source = "registry+https://github.com/rust-lang/crates.io-index" 2287 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 2288 + dependencies = [ 2289 + "core2", 2290 + "serde", 2291 + "unsigned-varint", 2292 + ] 2293 + 2294 + [[package]] 2295 + name = "n0-future" 2296 + version = "0.1.3" 2297 + source = "registry+https://github.com/rust-lang/crates.io-index" 2298 + checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" 2299 + dependencies = [ 2300 + "cfg_aliases", 2301 + "derive_more", 2302 + "futures-buffered", 2303 + "futures-lite", 2304 + "futures-util", 2305 + "js-sys", 2306 + "pin-project", 2307 + "send_wrapper", 2308 + "tokio", 2309 + "tokio-util", 2310 + "wasm-bindgen", 2311 + "wasm-bindgen-futures", 2312 + "web-time", 2313 + ] 2314 + 2315 + [[package]] 2316 + name = "nom" 2317 + version = "7.1.3" 2318 + source = "registry+https://github.com/rust-lang/crates.io-index" 2319 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2320 + dependencies = [ 2321 + "memchr", 2322 + "minimal-lexical", 1443 2323 ] 1444 2324 1445 2325 [[package]] ··· 1465 2345 1466 2346 [[package]] 1467 2347 name = "nu-ansi-term" 1468 - version = "0.46.0" 2348 + version = "0.50.3" 1469 2349 source = "registry+https://github.com/rust-lang/crates.io-index" 1470 - checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 2350 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 1471 2351 dependencies = [ 1472 - "overload", 1473 - "winapi", 2352 + "windows-sys 0.59.0", 1474 2353 ] 1475 2354 1476 2355 [[package]] 1477 2356 name = "num-bigint-dig" 1478 - version = "0.8.4" 2357 + version = "0.8.6" 1479 2358 source = "registry+https://github.com/rust-lang/crates.io-index" 1480 - checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 2359 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 1481 2360 dependencies = [ 1482 - "byteorder", 1483 2361 "lazy_static", 1484 2362 "libm", 1485 2363 "num-integer", ··· 1489 2367 "smallvec", 1490 2368 "zeroize", 1491 2369 ] 2370 + 2371 + [[package]] 2372 + name = "num-conv" 2373 + version = "0.1.0" 2374 + source = "registry+https://github.com/rust-lang/crates.io-index" 2375 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1492 2376 1493 2377 [[package]] 1494 2378 name = "num-integer" ··· 1537 2421 1538 2422 [[package]] 1539 2423 name = "object" 1540 - version = "0.36.7" 2424 + version = "0.32.2" 1541 2425 source = "registry+https://github.com/rust-lang/crates.io-index" 1542 - checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 2426 + checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 1543 2427 dependencies = [ 1544 2428 "memchr", 1545 2429 ] ··· 1552 2436 1553 2437 [[package]] 1554 2438 name = "openssl" 1555 - version = "0.10.73" 2439 + version = "0.10.75" 1556 2440 source = "registry+https://github.com/rust-lang/crates.io-index" 1557 - checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 2441 + checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 1558 2442 dependencies = [ 1559 2443 "bitflags", 1560 2444 "cfg-if", ··· 1573 2457 dependencies = [ 1574 2458 "proc-macro2", 1575 2459 "quote", 1576 - "syn", 2460 + "syn 2.0.112", 1577 2461 ] 1578 2462 1579 2463 [[package]] 1580 - name = "openssl-probe" 1581 - version = "0.1.6" 1582 - source = "registry+https://github.com/rust-lang/crates.io-index" 1583 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1584 - 1585 - [[package]] 1586 2464 name = "openssl-sys" 1587 - version = "0.9.109" 2465 + version = "0.9.111" 1588 2466 source = "registry+https://github.com/rust-lang/crates.io-index" 1589 - checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 2467 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 1590 2468 dependencies = [ 1591 2469 "cc", 1592 2470 "libc", ··· 1595 2473 ] 1596 2474 1597 2475 [[package]] 1598 - name = "overload" 1599 - version = "0.1.1" 2476 + name = "ouroboros" 2477 + version = "0.18.5" 1600 2478 source = "registry+https://github.com/rust-lang/crates.io-index" 1601 - checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 2479 + checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" 2480 + dependencies = [ 2481 + "aliasable", 2482 + "ouroboros_macro", 2483 + "static_assertions", 2484 + ] 2485 + 2486 + [[package]] 2487 + name = "ouroboros_macro" 2488 + version = "0.18.5" 2489 + source = "registry+https://github.com/rust-lang/crates.io-index" 2490 + checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" 2491 + dependencies = [ 2492 + "heck 0.4.1", 2493 + "proc-macro2", 2494 + "proc-macro2-diagnostics", 2495 + "quote", 2496 + "syn 2.0.112", 2497 + ] 2498 + 2499 + [[package]] 2500 + name = "p256" 2501 + version = "0.13.2" 2502 + source = "registry+https://github.com/rust-lang/crates.io-index" 2503 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2504 + dependencies = [ 2505 + "ecdsa", 2506 + "elliptic-curve", 2507 + "primeorder", 2508 + "sha2", 2509 + ] 1602 2510 1603 2511 [[package]] 1604 2512 name = "parking" ··· 1608 2516 1609 2517 [[package]] 1610 2518 name = "parking_lot" 1611 - version = "0.12.4" 2519 + version = "0.12.5" 1612 2520 source = "registry+https://github.com/rust-lang/crates.io-index" 1613 - checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 2521 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1614 2522 dependencies = [ 1615 2523 "lock_api", 1616 2524 "parking_lot_core", ··· 1618 2526 1619 2527 [[package]] 1620 2528 name = "parking_lot_core" 1621 - version = "0.9.11" 2529 + version = "0.9.12" 1622 2530 source = "registry+https://github.com/rust-lang/crates.io-index" 1623 - checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 2531 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1624 2532 dependencies = [ 1625 2533 "cfg-if", 1626 2534 "libc", 1627 - "redox_syscall", 2535 + "redox_syscall 0.5.18", 1628 2536 "smallvec", 1629 - "windows-targets 0.52.6", 2537 + "windows-link", 1630 2538 ] 1631 2539 1632 2540 [[package]] ··· 1652 2560 1653 2561 [[package]] 1654 2562 name = "pds_gatekeeper" 1655 - version = "0.1.0" 2563 + version = "0.1.2" 1656 2564 dependencies = [ 1657 2565 "anyhow", 2566 + "aws-lc-rs", 1658 2567 "axum", 1659 2568 "axum-template", 1660 2569 "chrono", 2570 + "dashmap", 1661 2571 "dotenvy", 1662 2572 "handlebars", 1663 2573 "hex", 2574 + "html-escape", 1664 2575 "hyper-util", 2576 + "jacquard-common", 2577 + "jacquard-identity", 2578 + "josekit", 1665 2579 "jwt-compact", 1666 2580 "lettre", 2581 + "multibase", 1667 2582 "rand 0.9.2", 2583 + "reqwest", 1668 2584 "rust-embed", 2585 + "rustls", 1669 2586 "scrypt", 1670 2587 "serde", 1671 2588 "serde_json", 1672 2589 "sha2", 1673 2590 "sqlx", 1674 2591 "tokio", 2592 + "tower", 1675 2593 "tower-http", 1676 2594 "tower_governor", 1677 2595 "tracing", 1678 2596 "tracing-subscriber", 2597 + "urlencoding", 1679 2598 ] 1680 2599 1681 2600 [[package]] ··· 1689 2608 1690 2609 [[package]] 1691 2610 name = "percent-encoding" 1692 - version = "2.3.1" 2611 + version = "2.3.2" 1693 2612 source = "registry+https://github.com/rust-lang/crates.io-index" 1694 - checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 2613 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1695 2614 1696 2615 [[package]] 1697 2616 name = "pest" 1698 - version = "2.8.1" 2617 + version = "2.8.4" 1699 2618 source = "registry+https://github.com/rust-lang/crates.io-index" 1700 - checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 2619 + checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" 1701 2620 dependencies = [ 1702 2621 "memchr", 1703 - "thiserror 2.0.14", 1704 2622 "ucd-trie", 1705 2623 ] 1706 2624 1707 2625 [[package]] 1708 2626 name = "pest_derive" 1709 - version = "2.8.1" 2627 + version = "2.8.4" 1710 2628 source = "registry+https://github.com/rust-lang/crates.io-index" 1711 - checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 2629 + checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" 1712 2630 dependencies = [ 1713 2631 "pest", 1714 2632 "pest_generator", ··· 1716 2634 1717 2635 [[package]] 1718 2636 name = "pest_generator" 1719 - version = "2.8.1" 2637 + version = "2.8.4" 1720 2638 source = "registry+https://github.com/rust-lang/crates.io-index" 1721 - checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 2639 + checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" 1722 2640 dependencies = [ 1723 2641 "pest", 1724 2642 "pest_meta", 1725 2643 "proc-macro2", 1726 2644 "quote", 1727 - "syn", 2645 + "syn 2.0.112", 1728 2646 ] 1729 2647 1730 2648 [[package]] 1731 2649 name = "pest_meta" 1732 - version = "2.8.1" 2650 + version = "2.8.4" 1733 2651 source = "registry+https://github.com/rust-lang/crates.io-index" 1734 - checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 2652 + checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" 1735 2653 dependencies = [ 1736 2654 "pest", 1737 2655 "sha2", ··· 1754 2672 dependencies = [ 1755 2673 "proc-macro2", 1756 2674 "quote", 1757 - "syn", 2675 + "syn 2.0.112", 1758 2676 ] 1759 2677 1760 2678 [[package]] ··· 1798 2716 1799 2717 [[package]] 1800 2718 name = "portable-atomic" 1801 - version = "1.11.1" 2719 + version = "1.13.0" 2720 + source = "registry+https://github.com/rust-lang/crates.io-index" 2721 + checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" 2722 + 2723 + [[package]] 2724 + name = "postcard" 2725 + version = "1.1.3" 1802 2726 source = "registry+https://github.com/rust-lang/crates.io-index" 1803 - checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2727 + checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" 2728 + dependencies = [ 2729 + "cobs", 2730 + "embedded-io 0.4.0", 2731 + "embedded-io 0.6.1", 2732 + "heapless", 2733 + "serde", 2734 + ] 1804 2735 1805 2736 [[package]] 1806 2737 name = "potential_utf" 1807 - version = "0.1.2" 2738 + version = "0.1.4" 1808 2739 source = "registry+https://github.com/rust-lang/crates.io-index" 1809 - checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 2740 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1810 2741 dependencies = [ 1811 2742 "zerovec", 1812 2743 ] 1813 2744 1814 2745 [[package]] 2746 + name = "powerfmt" 2747 + version = "0.2.0" 2748 + source = "registry+https://github.com/rust-lang/crates.io-index" 2749 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 2750 + 2751 + [[package]] 1815 2752 name = "ppv-lite86" 1816 2753 version = "0.2.21" 1817 2754 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1821 2758 ] 1822 2759 1823 2760 [[package]] 2761 + name = "prettyplease" 2762 + version = "0.2.37" 2763 + source = "registry+https://github.com/rust-lang/crates.io-index" 2764 + checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 2765 + dependencies = [ 2766 + "proc-macro2", 2767 + "syn 2.0.112", 2768 + ] 2769 + 2770 + [[package]] 2771 + name = "primeorder" 2772 + version = "0.13.6" 2773 + source = "registry+https://github.com/rust-lang/crates.io-index" 2774 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2775 + dependencies = [ 2776 + "elliptic-curve", 2777 + ] 2778 + 2779 + [[package]] 2780 + name = "proc-macro-error" 2781 + version = "1.0.4" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 2784 + dependencies = [ 2785 + "proc-macro-error-attr", 2786 + "proc-macro2", 2787 + "quote", 2788 + "syn 1.0.109", 2789 + "version_check", 2790 + ] 2791 + 2792 + [[package]] 2793 + name = "proc-macro-error-attr" 2794 + version = "1.0.4" 2795 + source = "registry+https://github.com/rust-lang/crates.io-index" 2796 + checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 2797 + dependencies = [ 2798 + "proc-macro2", 2799 + "quote", 2800 + "version_check", 2801 + ] 2802 + 2803 + [[package]] 1824 2804 name = "proc-macro2" 1825 - version = "1.0.97" 2805 + version = "1.0.104" 1826 2806 source = "registry+https://github.com/rust-lang/crates.io-index" 1827 - checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" 2807 + checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" 1828 2808 dependencies = [ 1829 2809 "unicode-ident", 1830 2810 ] 1831 2811 1832 2812 [[package]] 2813 + name = "proc-macro2-diagnostics" 2814 + version = "0.10.1" 2815 + source = "registry+https://github.com/rust-lang/crates.io-index" 2816 + checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 2817 + dependencies = [ 2818 + "proc-macro2", 2819 + "quote", 2820 + "syn 2.0.112", 2821 + "version_check", 2822 + "yansi", 2823 + ] 2824 + 2825 + [[package]] 1833 2826 name = "psm" 1834 - version = "0.1.26" 2827 + version = "0.1.28" 1835 2828 source = "registry+https://github.com/rust-lang/crates.io-index" 1836 - checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" 2829 + checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" 1837 2830 dependencies = [ 2831 + "ar_archive_writer", 1838 2832 "cc", 1839 2833 ] 1840 2834 ··· 1848 2842 "libc", 1849 2843 "once_cell", 1850 2844 "raw-cpuid", 1851 - "wasi 0.11.1+wasi-snapshot-preview1", 2845 + "wasi", 1852 2846 "web-sys", 1853 2847 "winapi", 1854 2848 ] 1855 2849 1856 2850 [[package]] 2851 + name = "quinn" 2852 + version = "0.11.9" 2853 + source = "registry+https://github.com/rust-lang/crates.io-index" 2854 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 2855 + dependencies = [ 2856 + "bytes", 2857 + "cfg_aliases", 2858 + "pin-project-lite", 2859 + "quinn-proto", 2860 + "quinn-udp", 2861 + "rustc-hash", 2862 + "rustls", 2863 + "socket2", 2864 + "thiserror 2.0.17", 2865 + "tokio", 2866 + "tracing", 2867 + "web-time", 2868 + ] 2869 + 2870 + [[package]] 2871 + name = "quinn-proto" 2872 + version = "0.11.13" 2873 + source = "registry+https://github.com/rust-lang/crates.io-index" 2874 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 2875 + dependencies = [ 2876 + "bytes", 2877 + "getrandom 0.3.4", 2878 + "lru-slab", 2879 + "rand 0.9.2", 2880 + "ring", 2881 + "rustc-hash", 2882 + "rustls", 2883 + "rustls-pki-types", 2884 + "slab", 2885 + "thiserror 2.0.17", 2886 + "tinyvec", 2887 + "tracing", 2888 + "web-time", 2889 + ] 2890 + 2891 + [[package]] 2892 + name = "quinn-udp" 2893 + version = "0.5.14" 2894 + source = "registry+https://github.com/rust-lang/crates.io-index" 2895 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 2896 + dependencies = [ 2897 + "cfg_aliases", 2898 + "libc", 2899 + "once_cell", 2900 + "socket2", 2901 + "tracing", 2902 + "windows-sys 0.59.0", 2903 + ] 2904 + 2905 + [[package]] 1857 2906 name = "quote" 1858 - version = "1.0.40" 2907 + version = "1.0.42" 1859 2908 source = "registry+https://github.com/rust-lang/crates.io-index" 1860 - checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 2909 + checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 1861 2910 dependencies = [ 1862 2911 "proc-macro2", 1863 2912 ] ··· 1930 2979 source = "registry+https://github.com/rust-lang/crates.io-index" 1931 2980 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1932 2981 dependencies = [ 1933 - "getrandom 0.3.3", 2982 + "getrandom 0.3.4", 1934 2983 ] 1935 2984 1936 2985 [[package]] 2986 + name = "range-traits" 2987 + version = "0.3.2" 2988 + source = "registry+https://github.com/rust-lang/crates.io-index" 2989 + checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 2990 + 2991 + [[package]] 1937 2992 name = "raw-cpuid" 1938 - version = "11.5.0" 2993 + version = "11.6.0" 1939 2994 source = "registry+https://github.com/rust-lang/crates.io-index" 1940 - checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" 2995 + checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" 1941 2996 dependencies = [ 1942 2997 "bitflags", 1943 2998 ] 1944 2999 1945 3000 [[package]] 1946 3001 name = "redox_syscall" 1947 - version = "0.5.17" 3002 + version = "0.5.18" 1948 3003 source = "registry+https://github.com/rust-lang/crates.io-index" 1949 - checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 3004 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1950 3005 dependencies = [ 1951 3006 "bitflags", 1952 3007 ] 1953 3008 1954 3009 [[package]] 1955 - name = "regex" 1956 - version = "1.11.1" 3010 + name = "redox_syscall" 3011 + version = "0.7.0" 1957 3012 source = "registry+https://github.com/rust-lang/crates.io-index" 1958 - checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 3013 + checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" 1959 3014 dependencies = [ 1960 - "aho-corasick", 1961 - "memchr", 1962 - "regex-automata 0.4.9", 1963 - "regex-syntax 0.8.5", 3015 + "bitflags", 1964 3016 ] 1965 3017 1966 3018 [[package]] 1967 - name = "regex-automata" 1968 - version = "0.1.10" 3019 + name = "ref-cast" 3020 + version = "1.0.25" 1969 3021 source = "registry+https://github.com/rust-lang/crates.io-index" 1970 - checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 3022 + checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" 1971 3023 dependencies = [ 1972 - "regex-syntax 0.6.29", 3024 + "ref-cast-impl", 3025 + ] 3026 + 3027 + [[package]] 3028 + name = "ref-cast-impl" 3029 + version = "1.0.25" 3030 + source = "registry+https://github.com/rust-lang/crates.io-index" 3031 + checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" 3032 + dependencies = [ 3033 + "proc-macro2", 3034 + "quote", 3035 + "syn 2.0.112", 3036 + ] 3037 + 3038 + [[package]] 3039 + name = "regex" 3040 + version = "1.12.2" 3041 + source = "registry+https://github.com/rust-lang/crates.io-index" 3042 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 3043 + dependencies = [ 3044 + "aho-corasick", 3045 + "memchr", 3046 + "regex-automata", 3047 + "regex-syntax", 1973 3048 ] 1974 3049 1975 3050 [[package]] 1976 3051 name = "regex-automata" 1977 - version = "0.4.9" 3052 + version = "0.4.13" 1978 3053 source = "registry+https://github.com/rust-lang/crates.io-index" 1979 - checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 3054 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 1980 3055 dependencies = [ 1981 3056 "aho-corasick", 1982 3057 "memchr", 1983 - "regex-syntax 0.8.5", 3058 + "regex-syntax", 1984 3059 ] 1985 3060 1986 3061 [[package]] 1987 - name = "regex-syntax" 1988 - version = "0.6.29" 3062 + name = "regex-lite" 3063 + version = "0.1.8" 1989 3064 source = "registry+https://github.com/rust-lang/crates.io-index" 1990 - checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 3065 + checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" 1991 3066 1992 3067 [[package]] 1993 3068 name = "regex-syntax" 1994 - version = "0.8.5" 3069 + version = "0.8.8" 1995 3070 source = "registry+https://github.com/rust-lang/crates.io-index" 1996 - checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 3071 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 3072 + 3073 + [[package]] 3074 + name = "reqwest" 3075 + version = "0.12.28" 3076 + source = "registry+https://github.com/rust-lang/crates.io-index" 3077 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 3078 + dependencies = [ 3079 + "base64", 3080 + "bytes", 3081 + "encoding_rs", 3082 + "futures-core", 3083 + "h2", 3084 + "http", 3085 + "http-body", 3086 + "http-body-util", 3087 + "hyper", 3088 + "hyper-rustls", 3089 + "hyper-util", 3090 + "js-sys", 3091 + "log", 3092 + "mime", 3093 + "percent-encoding", 3094 + "pin-project-lite", 3095 + "quinn", 3096 + "rustls", 3097 + "rustls-pki-types", 3098 + "serde", 3099 + "serde_json", 3100 + "serde_urlencoded", 3101 + "sync_wrapper", 3102 + "tokio", 3103 + "tokio-rustls", 3104 + "tower", 3105 + "tower-http", 3106 + "tower-service", 3107 + "url", 3108 + "wasm-bindgen", 3109 + "wasm-bindgen-futures", 3110 + "web-sys", 3111 + "webpki-roots 1.0.5", 3112 + ] 3113 + 3114 + [[package]] 3115 + name = "rfc6979" 3116 + version = "0.4.0" 3117 + source = "registry+https://github.com/rust-lang/crates.io-index" 3118 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 3119 + dependencies = [ 3120 + "hmac", 3121 + "subtle", 3122 + ] 1997 3123 1998 3124 [[package]] 1999 3125 name = "ring" ··· 2005 3131 "cfg-if", 2006 3132 "getrandom 0.2.16", 2007 3133 "libc", 2008 - "untrusted", 3134 + "untrusted 0.9.0", 2009 3135 "windows-sys 0.52.0", 2010 3136 ] 2011 3137 2012 3138 [[package]] 2013 3139 name = "rsa" 2014 - version = "0.9.8" 3140 + version = "0.9.9" 2015 3141 source = "registry+https://github.com/rust-lang/crates.io-index" 2016 - checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 3142 + checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" 2017 3143 dependencies = [ 2018 3144 "const-oid", 2019 3145 "digest", ··· 2031 3157 2032 3158 [[package]] 2033 3159 name = "rust-embed" 2034 - version = "8.7.2" 3160 + version = "8.9.0" 2035 3161 source = "registry+https://github.com/rust-lang/crates.io-index" 2036 - checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" 3162 + checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" 2037 3163 dependencies = [ 2038 3164 "rust-embed-impl", 2039 3165 "rust-embed-utils", ··· 2042 3168 2043 3169 [[package]] 2044 3170 name = "rust-embed-impl" 2045 - version = "8.7.2" 3171 + version = "8.9.0" 2046 3172 source = "registry+https://github.com/rust-lang/crates.io-index" 2047 - checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" 3173 + checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" 2048 3174 dependencies = [ 2049 3175 "proc-macro2", 2050 3176 "quote", 2051 3177 "rust-embed-utils", 2052 - "syn", 3178 + "syn 2.0.112", 2053 3179 "walkdir", 2054 3180 ] 2055 3181 2056 3182 [[package]] 2057 3183 name = "rust-embed-utils" 2058 - version = "8.7.2" 3184 + version = "8.9.0" 2059 3185 source = "registry+https://github.com/rust-lang/crates.io-index" 2060 - checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" 3186 + checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" 2061 3187 dependencies = [ 2062 3188 "globset", 2063 3189 "sha2", ··· 2065 3191 ] 2066 3192 2067 3193 [[package]] 2068 - name = "rustc-demangle" 2069 - version = "0.1.26" 3194 + name = "rustc-hash" 3195 + version = "2.1.1" 2070 3196 source = "registry+https://github.com/rust-lang/crates.io-index" 2071 - checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 3197 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2072 3198 2073 3199 [[package]] 2074 - name = "rustix" 2075 - version = "1.0.8" 3200 + name = "rustc_version" 3201 + version = "0.4.1" 2076 3202 source = "registry+https://github.com/rust-lang/crates.io-index" 2077 - checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 3203 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 2078 3204 dependencies = [ 2079 - "bitflags", 2080 - "errno", 2081 - "libc", 2082 - "linux-raw-sys", 2083 - "windows-sys 0.59.0", 3205 + "semver", 2084 3206 ] 2085 3207 2086 3208 [[package]] 2087 3209 name = "rustls" 2088 - version = "0.23.31" 3210 + version = "0.23.35" 2089 3211 source = "registry+https://github.com/rust-lang/crates.io-index" 2090 - checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 3212 + checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 2091 3213 dependencies = [ 3214 + "aws-lc-rs", 3215 + "log", 2092 3216 "once_cell", 2093 3217 "ring", 2094 3218 "rustls-pki-types", ··· 2099 3223 2100 3224 [[package]] 2101 3225 name = "rustls-pki-types" 2102 - version = "1.12.0" 3226 + version = "1.13.2" 2103 3227 source = "registry+https://github.com/rust-lang/crates.io-index" 2104 - checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 3228 + checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" 2105 3229 dependencies = [ 3230 + "web-time", 2106 3231 "zeroize", 2107 3232 ] 2108 3233 2109 3234 [[package]] 2110 3235 name = "rustls-webpki" 2111 - version = "0.103.4" 3236 + version = "0.103.8" 2112 3237 source = "registry+https://github.com/rust-lang/crates.io-index" 2113 - checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 3238 + checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 2114 3239 dependencies = [ 3240 + "aws-lc-rs", 2115 3241 "ring", 2116 3242 "rustls-pki-types", 2117 - "untrusted", 3243 + "untrusted 0.9.0", 2118 3244 ] 2119 3245 2120 3246 [[package]] ··· 2125 3251 2126 3252 [[package]] 2127 3253 name = "ryu" 2128 - version = "1.0.20" 3254 + version = "1.0.22" 2129 3255 source = "registry+https://github.com/rust-lang/crates.io-index" 2130 - checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 3256 + checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" 2131 3257 2132 3258 [[package]] 2133 3259 name = "salsa20" ··· 2148 3274 ] 2149 3275 2150 3276 [[package]] 2151 - name = "schannel" 2152 - version = "0.1.27" 3277 + name = "schemars" 3278 + version = "0.9.0" 2153 3279 source = "registry+https://github.com/rust-lang/crates.io-index" 2154 - checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 3280 + checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" 2155 3281 dependencies = [ 2156 - "windows-sys 0.59.0", 3282 + "dyn-clone", 3283 + "ref-cast", 3284 + "serde", 3285 + "serde_json", 2157 3286 ] 3287 + 3288 + [[package]] 3289 + name = "schemars" 3290 + version = "1.2.0" 3291 + source = "registry+https://github.com/rust-lang/crates.io-index" 3292 + checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" 3293 + dependencies = [ 3294 + "dyn-clone", 3295 + "ref-cast", 3296 + "serde", 3297 + "serde_json", 3298 + ] 3299 + 3300 + [[package]] 3301 + name = "scoped-tls" 3302 + version = "1.0.1" 3303 + source = "registry+https://github.com/rust-lang/crates.io-index" 3304 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 2158 3305 2159 3306 [[package]] 2160 3307 name = "scopeguard" ··· 2175 3322 ] 2176 3323 2177 3324 [[package]] 3325 + name = "sec1" 3326 + version = "0.7.3" 3327 + source = "registry+https://github.com/rust-lang/crates.io-index" 3328 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 3329 + dependencies = [ 3330 + "base16ct", 3331 + "der", 3332 + "generic-array", 3333 + "pkcs8", 3334 + "subtle", 3335 + "zeroize", 3336 + ] 3337 + 3338 + [[package]] 2178 3339 name = "secp256k1" 2179 3340 version = "0.28.2" 2180 3341 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2193 3354 ] 2194 3355 2195 3356 [[package]] 2196 - name = "security-framework" 2197 - version = "2.11.1" 3357 + name = "semver" 3358 + version = "1.0.27" 2198 3359 source = "registry+https://github.com/rust-lang/crates.io-index" 2199 - checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3360 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 3361 + 3362 + [[package]] 3363 + name = "send_wrapper" 3364 + version = "0.6.0" 3365 + source = "registry+https://github.com/rust-lang/crates.io-index" 3366 + checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" 3367 + 3368 + [[package]] 3369 + name = "serde" 3370 + version = "1.0.228" 3371 + source = "registry+https://github.com/rust-lang/crates.io-index" 3372 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2200 3373 dependencies = [ 2201 - "bitflags", 2202 - "core-foundation", 2203 - "core-foundation-sys", 2204 - "libc", 2205 - "security-framework-sys", 3374 + "serde_core", 3375 + "serde_derive", 2206 3376 ] 2207 3377 2208 3378 [[package]] 2209 - name = "security-framework-sys" 2210 - version = "2.14.0" 3379 + name = "serde_bytes" 3380 + version = "0.11.19" 2211 3381 source = "registry+https://github.com/rust-lang/crates.io-index" 2212 - checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 3382 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2213 3383 dependencies = [ 2214 - "core-foundation-sys", 2215 - "libc", 3384 + "serde", 3385 + "serde_core", 2216 3386 ] 2217 3387 2218 3388 [[package]] 2219 - name = "serde" 2220 - version = "1.0.219" 3389 + name = "serde_core" 3390 + version = "1.0.228" 2221 3391 source = "registry+https://github.com/rust-lang/crates.io-index" 2222 - checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 3392 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2223 3393 dependencies = [ 2224 3394 "serde_derive", 2225 3395 ] 2226 3396 2227 3397 [[package]] 2228 3398 name = "serde_derive" 2229 - version = "1.0.219" 3399 + version = "1.0.228" 2230 3400 source = "registry+https://github.com/rust-lang/crates.io-index" 2231 - checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 3401 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2232 3402 dependencies = [ 2233 3403 "proc-macro2", 2234 3404 "quote", 2235 - "syn", 3405 + "syn 2.0.112", 3406 + ] 3407 + 3408 + [[package]] 3409 + name = "serde_html_form" 3410 + version = "0.2.8" 3411 + source = "registry+https://github.com/rust-lang/crates.io-index" 3412 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 3413 + dependencies = [ 3414 + "form_urlencoded", 3415 + "indexmap 2.12.1", 3416 + "itoa", 3417 + "ryu", 3418 + "serde_core", 3419 + ] 3420 + 3421 + [[package]] 3422 + name = "serde_ipld_dagcbor" 3423 + version = "0.6.4" 3424 + source = "registry+https://github.com/rust-lang/crates.io-index" 3425 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 3426 + dependencies = [ 3427 + "cbor4ii", 3428 + "ipld-core", 3429 + "scopeguard", 3430 + "serde", 2236 3431 ] 2237 3432 2238 3433 [[package]] 2239 3434 name = "serde_json" 2240 - version = "1.0.142" 3435 + version = "1.0.148" 2241 3436 source = "registry+https://github.com/rust-lang/crates.io-index" 2242 - checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 3437 + checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" 2243 3438 dependencies = [ 3439 + "indexmap 2.12.1", 2244 3440 "itoa", 2245 3441 "memchr", 2246 - "ryu", 2247 3442 "serde", 3443 + "serde_core", 3444 + "zmij", 2248 3445 ] 2249 3446 2250 3447 [[package]] 2251 3448 name = "serde_path_to_error" 2252 - version = "0.1.17" 3449 + version = "0.1.20" 2253 3450 source = "registry+https://github.com/rust-lang/crates.io-index" 2254 - checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 3451 + checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" 2255 3452 dependencies = [ 2256 3453 "itoa", 2257 3454 "serde", 3455 + "serde_core", 3456 + ] 3457 + 3458 + [[package]] 3459 + name = "serde_repr" 3460 + version = "0.1.20" 3461 + source = "registry+https://github.com/rust-lang/crates.io-index" 3462 + checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 3463 + dependencies = [ 3464 + "proc-macro2", 3465 + "quote", 3466 + "syn 2.0.112", 2258 3467 ] 2259 3468 2260 3469 [[package]] ··· 2270 3479 ] 2271 3480 2272 3481 [[package]] 3482 + name = "serde_with" 3483 + version = "3.16.1" 3484 + source = "registry+https://github.com/rust-lang/crates.io-index" 3485 + checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" 3486 + dependencies = [ 3487 + "base64", 3488 + "chrono", 3489 + "hex", 3490 + "indexmap 1.9.3", 3491 + "indexmap 2.12.1", 3492 + "schemars 0.9.0", 3493 + "schemars 1.2.0", 3494 + "serde_core", 3495 + "serde_json", 3496 + "serde_with_macros", 3497 + "time", 3498 + ] 3499 + 3500 + [[package]] 3501 + name = "serde_with_macros" 3502 + version = "3.16.1" 3503 + source = "registry+https://github.com/rust-lang/crates.io-index" 3504 + checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" 3505 + dependencies = [ 3506 + "darling 0.21.3", 3507 + "proc-macro2", 3508 + "quote", 3509 + "syn 2.0.112", 3510 + ] 3511 + 3512 + [[package]] 2273 3513 name = "sha1" 2274 3514 version = "0.10.6" 2275 3515 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2308 3548 2309 3549 [[package]] 2310 3550 name = "signal-hook-registry" 2311 - version = "1.4.6" 3551 + version = "1.4.8" 2312 3552 source = "registry+https://github.com/rust-lang/crates.io-index" 2313 - checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 3553 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 2314 3554 dependencies = [ 3555 + "errno", 2315 3556 "libc", 2316 3557 ] 2317 3558 ··· 2326 3567 ] 2327 3568 2328 3569 [[package]] 3570 + name = "simd-adler32" 3571 + version = "0.3.8" 3572 + source = "registry+https://github.com/rust-lang/crates.io-index" 3573 + checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 3574 + 3575 + [[package]] 2329 3576 name = "slab" 2330 3577 version = "0.4.11" 2331 3578 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2341 3588 ] 2342 3589 2343 3590 [[package]] 3591 + name = "smol_str" 3592 + version = "0.3.4" 3593 + source = "registry+https://github.com/rust-lang/crates.io-index" 3594 + checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" 3595 + dependencies = [ 3596 + "borsh", 3597 + "serde_core", 3598 + ] 3599 + 3600 + [[package]] 2344 3601 name = "socket2" 2345 - version = "0.6.0" 3602 + version = "0.6.1" 2346 3603 source = "registry+https://github.com/rust-lang/crates.io-index" 2347 - checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 3604 + checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 2348 3605 dependencies = [ 2349 3606 "libc", 2350 - "windows-sys 0.59.0", 3607 + "windows-sys 0.60.2", 2351 3608 ] 2352 3609 2353 3610 [[package]] ··· 2358 3615 dependencies = [ 2359 3616 "lock_api", 2360 3617 ] 3618 + 3619 + [[package]] 3620 + name = "spin" 3621 + version = "0.10.0" 3622 + source = "registry+https://github.com/rust-lang/crates.io-index" 3623 + checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" 2361 3624 2362 3625 [[package]] 2363 3626 name = "spinning_top" ··· 2410 3673 "futures-util", 2411 3674 "hashbrown 0.15.5", 2412 3675 "hashlink", 2413 - "indexmap", 3676 + "indexmap 2.12.1", 2414 3677 "log", 2415 3678 "memchr", 2416 3679 "once_cell", ··· 2420 3683 "serde_json", 2421 3684 "sha2", 2422 3685 "smallvec", 2423 - "thiserror 2.0.14", 3686 + "thiserror 2.0.17", 2424 3687 "tokio", 2425 3688 "tokio-stream", 2426 3689 "tracing", ··· 2438 3701 "quote", 2439 3702 "sqlx-core", 2440 3703 "sqlx-macros-core", 2441 - "syn", 3704 + "syn 2.0.112", 2442 3705 ] 2443 3706 2444 3707 [[package]] ··· 2449 3712 dependencies = [ 2450 3713 "dotenvy", 2451 3714 "either", 2452 - "heck", 3715 + "heck 0.5.0", 2453 3716 "hex", 2454 3717 "once_cell", 2455 3718 "proc-macro2", ··· 2461 3724 "sqlx-mysql", 2462 3725 "sqlx-postgres", 2463 3726 "sqlx-sqlite", 2464 - "syn", 3727 + "syn 2.0.112", 2465 3728 "tokio", 2466 3729 "url", 2467 3730 ] ··· 2504 3767 "smallvec", 2505 3768 "sqlx-core", 2506 3769 "stringprep", 2507 - "thiserror 2.0.14", 3770 + "thiserror 2.0.17", 2508 3771 "tracing", 2509 3772 "whoami", 2510 3773 ] ··· 2542 3805 "smallvec", 2543 3806 "sqlx-core", 2544 3807 "stringprep", 2545 - "thiserror 2.0.14", 3808 + "thiserror 2.0.17", 2546 3809 "tracing", 2547 3810 "whoami", 2548 3811 ] ··· 2567 3830 "serde", 2568 3831 "serde_urlencoded", 2569 3832 "sqlx-core", 2570 - "thiserror 2.0.14", 3833 + "thiserror 2.0.17", 2571 3834 "tracing", 2572 3835 "url", 2573 3836 ] 2574 3837 2575 3838 [[package]] 2576 3839 name = "stable_deref_trait" 2577 - version = "1.2.0" 3840 + version = "1.2.1" 2578 3841 source = "registry+https://github.com/rust-lang/crates.io-index" 2579 - checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3842 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 2580 3843 2581 3844 [[package]] 2582 3845 name = "stacker" 2583 - version = "0.1.21" 3846 + version = "0.1.22" 2584 3847 source = "registry+https://github.com/rust-lang/crates.io-index" 2585 - checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" 3848 + checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" 2586 3849 dependencies = [ 2587 3850 "cc", 2588 3851 "cfg-if", ··· 2592 3855 ] 2593 3856 2594 3857 [[package]] 3858 + name = "static-regular-grammar" 3859 + version = "2.0.2" 3860 + source = "registry+https://github.com/rust-lang/crates.io-index" 3861 + checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" 3862 + dependencies = [ 3863 + "abnf", 3864 + "btree-range-map", 3865 + "ciborium", 3866 + "hex_fmt", 3867 + "indoc", 3868 + "proc-macro-error", 3869 + "proc-macro2", 3870 + "quote", 3871 + "serde", 3872 + "sha2", 3873 + "syn 2.0.112", 3874 + "thiserror 1.0.69", 3875 + ] 3876 + 3877 + [[package]] 3878 + name = "static_assertions" 3879 + version = "1.1.0" 3880 + source = "registry+https://github.com/rust-lang/crates.io-index" 3881 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 3882 + 3883 + [[package]] 2595 3884 name = "stringprep" 2596 3885 version = "0.1.5" 2597 3886 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2616 3905 2617 3906 [[package]] 2618 3907 name = "syn" 2619 - version = "2.0.105" 3908 + version = "1.0.109" 3909 + source = "registry+https://github.com/rust-lang/crates.io-index" 3910 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 3911 + dependencies = [ 3912 + "proc-macro2", 3913 + "quote", 3914 + "unicode-ident", 3915 + ] 3916 + 3917 + [[package]] 3918 + name = "syn" 3919 + version = "2.0.112" 2620 3920 source = "registry+https://github.com/rust-lang/crates.io-index" 2621 - checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" 3921 + checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" 2622 3922 dependencies = [ 2623 3923 "proc-macro2", 2624 3924 "quote", ··· 2630 3930 version = "1.0.2" 2631 3931 source = "registry+https://github.com/rust-lang/crates.io-index" 2632 3932 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3933 + dependencies = [ 3934 + "futures-core", 3935 + ] 2633 3936 2634 3937 [[package]] 2635 3938 name = "synstructure" ··· 2639 3942 dependencies = [ 2640 3943 "proc-macro2", 2641 3944 "quote", 2642 - "syn", 3945 + "syn 2.0.112", 2643 3946 ] 2644 3947 2645 3948 [[package]] 2646 - name = "tempfile" 2647 - version = "3.21.0" 3949 + name = "system-configuration" 3950 + version = "0.6.1" 2648 3951 source = "registry+https://github.com/rust-lang/crates.io-index" 2649 - checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" 3952 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2650 3953 dependencies = [ 2651 - "fastrand", 2652 - "getrandom 0.3.3", 2653 - "once_cell", 2654 - "rustix", 2655 - "windows-sys 0.59.0", 3954 + "bitflags", 3955 + "core-foundation", 3956 + "system-configuration-sys", 3957 + ] 3958 + 3959 + [[package]] 3960 + name = "system-configuration-sys" 3961 + version = "0.6.0" 3962 + source = "registry+https://github.com/rust-lang/crates.io-index" 3963 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 3964 + dependencies = [ 3965 + "core-foundation-sys", 3966 + "libc", 2656 3967 ] 2657 3968 2658 3969 [[package]] ··· 2666 3977 2667 3978 [[package]] 2668 3979 name = "thiserror" 2669 - version = "2.0.14" 3980 + version = "2.0.17" 2670 3981 source = "registry+https://github.com/rust-lang/crates.io-index" 2671 - checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" 3982 + checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 2672 3983 dependencies = [ 2673 - "thiserror-impl 2.0.14", 3984 + "thiserror-impl 2.0.17", 2674 3985 ] 2675 3986 2676 3987 [[package]] ··· 2681 3992 dependencies = [ 2682 3993 "proc-macro2", 2683 3994 "quote", 2684 - "syn", 3995 + "syn 2.0.112", 2685 3996 ] 2686 3997 2687 3998 [[package]] 2688 3999 name = "thiserror-impl" 2689 - version = "2.0.14" 4000 + version = "2.0.17" 2690 4001 source = "registry+https://github.com/rust-lang/crates.io-index" 2691 - checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" 4002 + checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 2692 4003 dependencies = [ 2693 4004 "proc-macro2", 2694 4005 "quote", 2695 - "syn", 4006 + "syn 2.0.112", 2696 4007 ] 2697 4008 2698 4009 [[package]] ··· 2705 4016 ] 2706 4017 2707 4018 [[package]] 4019 + name = "time" 4020 + version = "0.3.44" 4021 + source = "registry+https://github.com/rust-lang/crates.io-index" 4022 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 4023 + dependencies = [ 4024 + "deranged", 4025 + "itoa", 4026 + "num-conv", 4027 + "powerfmt", 4028 + "serde", 4029 + "time-core", 4030 + "time-macros", 4031 + ] 4032 + 4033 + [[package]] 4034 + name = "time-core" 4035 + version = "0.1.6" 4036 + source = "registry+https://github.com/rust-lang/crates.io-index" 4037 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 4038 + 4039 + [[package]] 4040 + name = "time-macros" 4041 + version = "0.2.24" 4042 + source = "registry+https://github.com/rust-lang/crates.io-index" 4043 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 4044 + dependencies = [ 4045 + "num-conv", 4046 + "time-core", 4047 + ] 4048 + 4049 + [[package]] 2708 4050 name = "tinystr" 2709 - version = "0.8.1" 4051 + version = "0.8.2" 2710 4052 source = "registry+https://github.com/rust-lang/crates.io-index" 2711 - checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 4053 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 2712 4054 dependencies = [ 2713 4055 "displaydoc", 2714 4056 "zerovec", ··· 2716 4058 2717 4059 [[package]] 2718 4060 name = "tinyvec" 2719 - version = "1.9.0" 4061 + version = "1.10.0" 2720 4062 source = "registry+https://github.com/rust-lang/crates.io-index" 2721 - checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 4063 + checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" 2722 4064 dependencies = [ 2723 4065 "tinyvec_macros", 2724 4066 ] ··· 2731 4073 2732 4074 [[package]] 2733 4075 name = "tokio" 2734 - version = "1.47.1" 4076 + version = "1.48.0" 2735 4077 source = "registry+https://github.com/rust-lang/crates.io-index" 2736 - checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 4078 + checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 2737 4079 dependencies = [ 2738 - "backtrace", 2739 4080 "bytes", 2740 - "io-uring", 2741 4081 "libc", 2742 4082 "mio", 2743 4083 "pin-project-lite", 2744 4084 "signal-hook-registry", 2745 - "slab", 2746 4085 "socket2", 2747 4086 "tokio-macros", 2748 - "windows-sys 0.59.0", 4087 + "windows-sys 0.61.2", 2749 4088 ] 2750 4089 2751 4090 [[package]] 2752 4091 name = "tokio-macros" 2753 - version = "2.5.0" 4092 + version = "2.6.0" 2754 4093 source = "registry+https://github.com/rust-lang/crates.io-index" 2755 - checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 4094 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 2756 4095 dependencies = [ 2757 4096 "proc-macro2", 2758 4097 "quote", 2759 - "syn", 4098 + "syn 2.0.112", 2760 4099 ] 2761 4100 2762 4101 [[package]] 2763 - name = "tokio-native-tls" 2764 - version = "0.3.1" 4102 + name = "tokio-rustls" 4103 + version = "0.26.4" 2765 4104 source = "registry+https://github.com/rust-lang/crates.io-index" 2766 - checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 4105 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 2767 4106 dependencies = [ 2768 - "native-tls", 4107 + "rustls", 2769 4108 "tokio", 2770 4109 ] 2771 4110 ··· 2782 4121 2783 4122 [[package]] 2784 4123 name = "tokio-util" 2785 - version = "0.7.15" 4124 + version = "0.7.17" 2786 4125 source = "registry+https://github.com/rust-lang/crates.io-index" 2787 - checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 4126 + checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 2788 4127 dependencies = [ 2789 4128 "bytes", 2790 4129 "futures-core", 2791 4130 "futures-sink", 4131 + "futures-util", 2792 4132 "pin-project-lite", 2793 4133 "tokio", 2794 4134 ] 2795 4135 2796 4136 [[package]] 2797 4137 name = "tonic" 2798 - version = "0.14.1" 4138 + version = "0.14.2" 2799 4139 source = "registry+https://github.com/rust-lang/crates.io-index" 2800 - checksum = "67ac5a8627ada0968acec063a4746bf79588aa03ccb66db2f75d7dce26722a40" 4140 + checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" 2801 4141 dependencies = [ 2802 4142 "async-trait", 2803 4143 "axum", ··· 2830 4170 dependencies = [ 2831 4171 "futures-core", 2832 4172 "futures-util", 2833 - "indexmap", 4173 + "indexmap 2.12.1", 2834 4174 "pin-project-lite", 2835 4175 "slab", 2836 4176 "sync_wrapper", ··· 2843 4183 2844 4184 [[package]] 2845 4185 name = "tower-http" 2846 - version = "0.6.6" 4186 + version = "0.6.8" 2847 4187 source = "registry+https://github.com/rust-lang/crates.io-index" 2848 - checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 4188 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 2849 4189 dependencies = [ 2850 4190 "async-compression", 2851 4191 "bitflags", 2852 4192 "bytes", 2853 4193 "futures-core", 4194 + "futures-util", 2854 4195 "http", 2855 4196 "http-body", 4197 + "http-body-util", 4198 + "iri-string", 2856 4199 "pin-project-lite", 2857 4200 "tokio", 2858 4201 "tokio-util", 4202 + "tower", 2859 4203 "tower-layer", 2860 4204 "tower-service", 2861 4205 ] ··· 2883 4227 "governor", 2884 4228 "http", 2885 4229 "pin-project", 2886 - "thiserror 2.0.14", 4230 + "thiserror 2.0.17", 2887 4231 "tonic", 2888 4232 "tower", 2889 4233 "tracing", ··· 2891 4235 2892 4236 [[package]] 2893 4237 name = "tracing" 2894 - version = "0.1.41" 4238 + version = "0.1.44" 2895 4239 source = "registry+https://github.com/rust-lang/crates.io-index" 2896 - checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 4240 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 2897 4241 dependencies = [ 2898 4242 "log", 2899 4243 "pin-project-lite", ··· 2903 4247 2904 4248 [[package]] 2905 4249 name = "tracing-attributes" 2906 - version = "0.1.30" 4250 + version = "0.1.31" 2907 4251 source = "registry+https://github.com/rust-lang/crates.io-index" 2908 - checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 4252 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 2909 4253 dependencies = [ 2910 4254 "proc-macro2", 2911 4255 "quote", 2912 - "syn", 4256 + "syn 2.0.112", 2913 4257 ] 2914 4258 2915 4259 [[package]] 2916 4260 name = "tracing-core" 2917 - version = "0.1.34" 4261 + version = "0.1.36" 2918 4262 source = "registry+https://github.com/rust-lang/crates.io-index" 2919 - checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 4263 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 2920 4264 dependencies = [ 2921 4265 "once_cell", 2922 4266 "valuable", ··· 2935 4279 2936 4280 [[package]] 2937 4281 name = "tracing-subscriber" 2938 - version = "0.3.19" 4282 + version = "0.3.22" 2939 4283 source = "registry+https://github.com/rust-lang/crates.io-index" 2940 - checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 4284 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 2941 4285 dependencies = [ 2942 4286 "matchers", 2943 4287 "nu-ansi-term", 2944 4288 "once_cell", 2945 - "regex", 4289 + "regex-automata", 2946 4290 "sharded-slab", 2947 4291 "smallvec", 2948 4292 "thread_local", ··· 2952 4296 ] 2953 4297 2954 4298 [[package]] 4299 + name = "trait-variant" 4300 + version = "0.1.2" 4301 + source = "registry+https://github.com/rust-lang/crates.io-index" 4302 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 4303 + dependencies = [ 4304 + "proc-macro2", 4305 + "quote", 4306 + "syn 2.0.112", 4307 + ] 4308 + 4309 + [[package]] 2955 4310 name = "try-lock" 2956 4311 version = "0.2.5" 2957 4312 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2959 4314 2960 4315 [[package]] 2961 4316 name = "typenum" 2962 - version = "1.18.0" 4317 + version = "1.19.0" 2963 4318 source = "registry+https://github.com/rust-lang/crates.io-index" 2964 - checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 4319 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 2965 4320 2966 4321 [[package]] 2967 4322 name = "ucd-trie" ··· 2977 4332 2978 4333 [[package]] 2979 4334 name = "unicode-ident" 2980 - version = "1.0.18" 4335 + version = "1.0.22" 2981 4336 source = "registry+https://github.com/rust-lang/crates.io-index" 2982 - checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 4337 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 2983 4338 2984 4339 [[package]] 2985 4340 name = "unicode-normalization" 2986 - version = "0.1.24" 4341 + version = "0.1.25" 2987 4342 source = "registry+https://github.com/rust-lang/crates.io-index" 2988 - checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 4343 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 2989 4344 dependencies = [ 2990 4345 "tinyvec", 2991 4346 ] 2992 4347 2993 4348 [[package]] 2994 4349 name = "unicode-properties" 2995 - version = "0.1.3" 4350 + version = "0.1.4" 2996 4351 source = "registry+https://github.com/rust-lang/crates.io-index" 2997 - checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 4352 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 4353 + 4354 + [[package]] 4355 + name = "unicode-segmentation" 4356 + version = "1.12.0" 4357 + source = "registry+https://github.com/rust-lang/crates.io-index" 4358 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 4359 + 4360 + [[package]] 4361 + name = "unicode-width" 4362 + version = "0.1.14" 4363 + source = "registry+https://github.com/rust-lang/crates.io-index" 4364 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 4365 + 4366 + [[package]] 4367 + name = "unicode-xid" 4368 + version = "0.2.6" 4369 + source = "registry+https://github.com/rust-lang/crates.io-index" 4370 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 4371 + 4372 + [[package]] 4373 + name = "unsigned-varint" 4374 + version = "0.8.0" 4375 + source = "registry+https://github.com/rust-lang/crates.io-index" 4376 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 4377 + 4378 + [[package]] 4379 + name = "untrusted" 4380 + version = "0.7.1" 4381 + source = "registry+https://github.com/rust-lang/crates.io-index" 4382 + checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 2998 4383 2999 4384 [[package]] 3000 4385 name = "untrusted" ··· 3004 4389 3005 4390 [[package]] 3006 4391 name = "url" 3007 - version = "2.5.4" 4392 + version = "2.5.7" 3008 4393 source = "registry+https://github.com/rust-lang/crates.io-index" 3009 - checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 4394 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 3010 4395 dependencies = [ 3011 4396 "form_urlencoded", 3012 4397 "idna", 3013 4398 "percent-encoding", 4399 + "serde", 3014 4400 ] 3015 4401 3016 4402 [[package]] 4403 + name = "urlencoding" 4404 + version = "2.1.3" 4405 + source = "registry+https://github.com/rust-lang/crates.io-index" 4406 + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 4407 + 4408 + [[package]] 4409 + name = "utf8-width" 4410 + version = "0.1.8" 4411 + source = "registry+https://github.com/rust-lang/crates.io-index" 4412 + checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" 4413 + 4414 + [[package]] 3017 4415 name = "utf8_iter" 3018 4416 version = "1.0.4" 3019 4417 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3063 4461 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3064 4462 3065 4463 [[package]] 3066 - name = "wasi" 3067 - version = "0.14.2+wasi-0.2.4" 4464 + name = "wasip2" 4465 + version = "1.0.1+wasi-0.2.4" 3068 4466 source = "registry+https://github.com/rust-lang/crates.io-index" 3069 - checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 4467 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 3070 4468 dependencies = [ 3071 - "wit-bindgen-rt", 4469 + "wit-bindgen", 3072 4470 ] 3073 4471 3074 4472 [[package]] ··· 3079 4477 3080 4478 [[package]] 3081 4479 name = "wasm-bindgen" 3082 - version = "0.2.100" 4480 + version = "0.2.106" 3083 4481 source = "registry+https://github.com/rust-lang/crates.io-index" 3084 - checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 4482 + checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 3085 4483 dependencies = [ 3086 4484 "cfg-if", 3087 4485 "once_cell", 3088 4486 "rustversion", 3089 4487 "wasm-bindgen-macro", 4488 + "wasm-bindgen-shared", 3090 4489 ] 3091 4490 3092 4491 [[package]] 3093 - name = "wasm-bindgen-backend" 3094 - version = "0.2.100" 4492 + name = "wasm-bindgen-futures" 4493 + version = "0.4.56" 3095 4494 source = "registry+https://github.com/rust-lang/crates.io-index" 3096 - checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 4495 + checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" 3097 4496 dependencies = [ 3098 - "bumpalo", 3099 - "log", 3100 - "proc-macro2", 3101 - "quote", 3102 - "syn", 3103 - "wasm-bindgen-shared", 4497 + "cfg-if", 4498 + "js-sys", 4499 + "once_cell", 4500 + "wasm-bindgen", 4501 + "web-sys", 3104 4502 ] 3105 4503 3106 4504 [[package]] 3107 4505 name = "wasm-bindgen-macro" 3108 - version = "0.2.100" 4506 + version = "0.2.106" 3109 4507 source = "registry+https://github.com/rust-lang/crates.io-index" 3110 - checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 4508 + checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 3111 4509 dependencies = [ 3112 4510 "quote", 3113 4511 "wasm-bindgen-macro-support", ··· 3115 4513 3116 4514 [[package]] 3117 4515 name = "wasm-bindgen-macro-support" 3118 - version = "0.2.100" 4516 + version = "0.2.106" 3119 4517 source = "registry+https://github.com/rust-lang/crates.io-index" 3120 - checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 4518 + checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 3121 4519 dependencies = [ 4520 + "bumpalo", 3122 4521 "proc-macro2", 3123 4522 "quote", 3124 - "syn", 3125 - "wasm-bindgen-backend", 4523 + "syn 2.0.112", 3126 4524 "wasm-bindgen-shared", 3127 4525 ] 3128 4526 3129 4527 [[package]] 3130 4528 name = "wasm-bindgen-shared" 3131 - version = "0.2.100" 4529 + version = "0.2.106" 3132 4530 source = "registry+https://github.com/rust-lang/crates.io-index" 3133 - checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 4531 + checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 3134 4532 dependencies = [ 3135 4533 "unicode-ident", 3136 4534 ] 3137 4535 3138 4536 [[package]] 3139 4537 name = "web-sys" 3140 - version = "0.3.77" 4538 + version = "0.3.83" 3141 4539 source = "registry+https://github.com/rust-lang/crates.io-index" 3142 - checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 4540 + checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" 3143 4541 dependencies = [ 3144 4542 "js-sys", 3145 4543 "wasm-bindgen", ··· 3161 4559 source = "registry+https://github.com/rust-lang/crates.io-index" 3162 4560 checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 3163 4561 dependencies = [ 3164 - "webpki-roots 1.0.2", 4562 + "webpki-roots 1.0.5", 3165 4563 ] 3166 4564 3167 4565 [[package]] 3168 4566 name = "webpki-roots" 3169 - version = "1.0.2" 4567 + version = "1.0.5" 3170 4568 source = "registry+https://github.com/rust-lang/crates.io-index" 3171 - checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 4569 + checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" 3172 4570 dependencies = [ 3173 4571 "rustls-pki-types", 3174 4572 ] ··· 3201 4599 3202 4600 [[package]] 3203 4601 name = "winapi-util" 3204 - version = "0.1.9" 4602 + version = "0.1.11" 3205 4603 source = "registry+https://github.com/rust-lang/crates.io-index" 3206 - checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 4604 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 3207 4605 dependencies = [ 3208 - "windows-sys 0.59.0", 4606 + "windows-sys 0.48.0", 3209 4607 ] 3210 4608 3211 4609 [[package]] ··· 3216 4614 3217 4615 [[package]] 3218 4616 name = "windows-core" 3219 - version = "0.61.2" 4617 + version = "0.62.2" 3220 4618 source = "registry+https://github.com/rust-lang/crates.io-index" 3221 - checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 4619 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 3222 4620 dependencies = [ 3223 4621 "windows-implement", 3224 4622 "windows-interface", ··· 3229 4627 3230 4628 [[package]] 3231 4629 name = "windows-implement" 3232 - version = "0.60.0" 4630 + version = "0.60.2" 3233 4631 source = "registry+https://github.com/rust-lang/crates.io-index" 3234 - checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 4632 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 3235 4633 dependencies = [ 3236 4634 "proc-macro2", 3237 4635 "quote", 3238 - "syn", 4636 + "syn 2.0.112", 3239 4637 ] 3240 4638 3241 4639 [[package]] 3242 4640 name = "windows-interface" 3243 - version = "0.59.1" 4641 + version = "0.59.3" 3244 4642 source = "registry+https://github.com/rust-lang/crates.io-index" 3245 - checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 4643 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 3246 4644 dependencies = [ 3247 4645 "proc-macro2", 3248 4646 "quote", 3249 - "syn", 4647 + "syn 2.0.112", 3250 4648 ] 3251 4649 3252 4650 [[package]] 3253 4651 name = "windows-link" 3254 - version = "0.1.3" 4652 + version = "0.2.1" 3255 4653 source = "registry+https://github.com/rust-lang/crates.io-index" 3256 - checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 4654 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4655 + 4656 + [[package]] 4657 + name = "windows-registry" 4658 + version = "0.6.1" 4659 + source = "registry+https://github.com/rust-lang/crates.io-index" 4660 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 4661 + dependencies = [ 4662 + "windows-link", 4663 + "windows-result", 4664 + "windows-strings", 4665 + ] 3257 4666 3258 4667 [[package]] 3259 4668 name = "windows-result" 3260 - version = "0.3.4" 4669 + version = "0.4.1" 3261 4670 source = "registry+https://github.com/rust-lang/crates.io-index" 3262 - checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 4671 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 3263 4672 dependencies = [ 3264 4673 "windows-link", 3265 4674 ] 3266 4675 3267 4676 [[package]] 3268 4677 name = "windows-strings" 3269 - version = "0.4.2" 4678 + version = "0.5.1" 3270 4679 source = "registry+https://github.com/rust-lang/crates.io-index" 3271 - checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 4680 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 3272 4681 dependencies = [ 3273 4682 "windows-link", 3274 4683 ] ··· 3301 4710 ] 3302 4711 3303 4712 [[package]] 4713 + name = "windows-sys" 4714 + version = "0.60.2" 4715 + source = "registry+https://github.com/rust-lang/crates.io-index" 4716 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 4717 + dependencies = [ 4718 + "windows-targets 0.53.5", 4719 + ] 4720 + 4721 + [[package]] 4722 + name = "windows-sys" 4723 + version = "0.61.2" 4724 + source = "registry+https://github.com/rust-lang/crates.io-index" 4725 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 4726 + dependencies = [ 4727 + "windows-link", 4728 + ] 4729 + 4730 + [[package]] 3304 4731 name = "windows-targets" 3305 4732 version = "0.48.5" 3306 4733 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3324 4751 "windows_aarch64_gnullvm 0.52.6", 3325 4752 "windows_aarch64_msvc 0.52.6", 3326 4753 "windows_i686_gnu 0.52.6", 3327 - "windows_i686_gnullvm", 4754 + "windows_i686_gnullvm 0.52.6", 3328 4755 "windows_i686_msvc 0.52.6", 3329 4756 "windows_x86_64_gnu 0.52.6", 3330 4757 "windows_x86_64_gnullvm 0.52.6", ··· 3332 4759 ] 3333 4760 3334 4761 [[package]] 4762 + name = "windows-targets" 4763 + version = "0.53.5" 4764 + source = "registry+https://github.com/rust-lang/crates.io-index" 4765 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 4766 + dependencies = [ 4767 + "windows-link", 4768 + "windows_aarch64_gnullvm 0.53.1", 4769 + "windows_aarch64_msvc 0.53.1", 4770 + "windows_i686_gnu 0.53.1", 4771 + "windows_i686_gnullvm 0.53.1", 4772 + "windows_i686_msvc 0.53.1", 4773 + "windows_x86_64_gnu 0.53.1", 4774 + "windows_x86_64_gnullvm 0.53.1", 4775 + "windows_x86_64_msvc 0.53.1", 4776 + ] 4777 + 4778 + [[package]] 3335 4779 name = "windows_aarch64_gnullvm" 3336 4780 version = "0.48.5" 3337 4781 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3344 4788 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3345 4789 3346 4790 [[package]] 4791 + name = "windows_aarch64_gnullvm" 4792 + version = "0.53.1" 4793 + source = "registry+https://github.com/rust-lang/crates.io-index" 4794 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 4795 + 4796 + [[package]] 3347 4797 name = "windows_aarch64_msvc" 3348 4798 version = "0.48.5" 3349 4799 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3356 4806 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3357 4807 3358 4808 [[package]] 4809 + name = "windows_aarch64_msvc" 4810 + version = "0.53.1" 4811 + source = "registry+https://github.com/rust-lang/crates.io-index" 4812 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 4813 + 4814 + [[package]] 3359 4815 name = "windows_i686_gnu" 3360 4816 version = "0.48.5" 3361 4817 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3368 4824 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 3369 4825 3370 4826 [[package]] 4827 + name = "windows_i686_gnu" 4828 + version = "0.53.1" 4829 + source = "registry+https://github.com/rust-lang/crates.io-index" 4830 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 4831 + 4832 + [[package]] 3371 4833 name = "windows_i686_gnullvm" 3372 4834 version = "0.52.6" 3373 4835 source = "registry+https://github.com/rust-lang/crates.io-index" 3374 4836 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 4837 + 4838 + [[package]] 4839 + name = "windows_i686_gnullvm" 4840 + version = "0.53.1" 4841 + source = "registry+https://github.com/rust-lang/crates.io-index" 4842 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 3375 4843 3376 4844 [[package]] 3377 4845 name = "windows_i686_msvc" ··· 3386 4854 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3387 4855 3388 4856 [[package]] 4857 + name = "windows_i686_msvc" 4858 + version = "0.53.1" 4859 + source = "registry+https://github.com/rust-lang/crates.io-index" 4860 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 4861 + 4862 + [[package]] 3389 4863 name = "windows_x86_64_gnu" 3390 4864 version = "0.48.5" 3391 4865 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3398 4872 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3399 4873 3400 4874 [[package]] 4875 + name = "windows_x86_64_gnu" 4876 + version = "0.53.1" 4877 + source = "registry+https://github.com/rust-lang/crates.io-index" 4878 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 4879 + 4880 + [[package]] 3401 4881 name = "windows_x86_64_gnullvm" 3402 4882 version = "0.48.5" 3403 4883 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3410 4890 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3411 4891 3412 4892 [[package]] 4893 + name = "windows_x86_64_gnullvm" 4894 + version = "0.53.1" 4895 + source = "registry+https://github.com/rust-lang/crates.io-index" 4896 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 4897 + 4898 + [[package]] 3413 4899 name = "windows_x86_64_msvc" 3414 4900 version = "0.48.5" 3415 4901 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3422 4908 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3423 4909 3424 4910 [[package]] 3425 - name = "wit-bindgen-rt" 3426 - version = "0.39.0" 4911 + name = "windows_x86_64_msvc" 4912 + version = "0.53.1" 3427 4913 source = "registry+https://github.com/rust-lang/crates.io-index" 3428 - checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 3429 - dependencies = [ 3430 - "bitflags", 3431 - ] 4914 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 4915 + 4916 + [[package]] 4917 + name = "wit-bindgen" 4918 + version = "0.46.0" 4919 + source = "registry+https://github.com/rust-lang/crates.io-index" 4920 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 3432 4921 3433 4922 [[package]] 3434 4923 name = "writeable" 3435 - version = "0.6.1" 4924 + version = "0.6.2" 3436 4925 source = "registry+https://github.com/rust-lang/crates.io-index" 3437 - checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 4926 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 4927 + 4928 + [[package]] 4929 + name = "yansi" 4930 + version = "1.0.1" 4931 + source = "registry+https://github.com/rust-lang/crates.io-index" 4932 + checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 3438 4933 3439 4934 [[package]] 3440 4935 name = "yoke" 3441 - version = "0.8.0" 4936 + version = "0.8.1" 3442 4937 source = "registry+https://github.com/rust-lang/crates.io-index" 3443 - checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 4938 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 3444 4939 dependencies = [ 3445 - "serde", 3446 4940 "stable_deref_trait", 3447 4941 "yoke-derive", 3448 4942 "zerofrom", ··· 3450 4944 3451 4945 [[package]] 3452 4946 name = "yoke-derive" 3453 - version = "0.8.0" 4947 + version = "0.8.1" 3454 4948 source = "registry+https://github.com/rust-lang/crates.io-index" 3455 - checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 4949 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 3456 4950 dependencies = [ 3457 4951 "proc-macro2", 3458 4952 "quote", 3459 - "syn", 4953 + "syn 2.0.112", 3460 4954 "synstructure", 3461 4955 ] 3462 4956 3463 4957 [[package]] 3464 4958 name = "zerocopy" 3465 - version = "0.8.26" 4959 + version = "0.8.31" 3466 4960 source = "registry+https://github.com/rust-lang/crates.io-index" 3467 - checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 4961 + checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" 3468 4962 dependencies = [ 3469 4963 "zerocopy-derive", 3470 4964 ] 3471 4965 3472 4966 [[package]] 3473 4967 name = "zerocopy-derive" 3474 - version = "0.8.26" 4968 + version = "0.8.31" 3475 4969 source = "registry+https://github.com/rust-lang/crates.io-index" 3476 - checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 4970 + checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" 3477 4971 dependencies = [ 3478 4972 "proc-macro2", 3479 4973 "quote", 3480 - "syn", 4974 + "syn 2.0.112", 3481 4975 ] 3482 4976 3483 4977 [[package]] ··· 3497 4991 dependencies = [ 3498 4992 "proc-macro2", 3499 4993 "quote", 3500 - "syn", 4994 + "syn 2.0.112", 3501 4995 "synstructure", 3502 4996 ] 3503 4997 3504 4998 [[package]] 3505 4999 name = "zeroize" 3506 - version = "1.8.1" 5000 + version = "1.8.2" 3507 5001 source = "registry+https://github.com/rust-lang/crates.io-index" 3508 - checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 5002 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 3509 5003 dependencies = [ 3510 5004 "zeroize_derive", 3511 5005 ] 3512 5006 3513 5007 [[package]] 3514 5008 name = "zeroize_derive" 3515 - version = "1.4.2" 5009 + version = "1.4.3" 3516 5010 source = "registry+https://github.com/rust-lang/crates.io-index" 3517 - checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 5011 + checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" 3518 5012 dependencies = [ 3519 5013 "proc-macro2", 3520 5014 "quote", 3521 - "syn", 5015 + "syn 2.0.112", 3522 5016 ] 3523 5017 3524 5018 [[package]] 3525 5019 name = "zerotrie" 3526 - version = "0.2.2" 5020 + version = "0.2.3" 3527 5021 source = "registry+https://github.com/rust-lang/crates.io-index" 3528 - checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 5022 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 3529 5023 dependencies = [ 3530 5024 "displaydoc", 3531 5025 "yoke", ··· 3534 5028 3535 5029 [[package]] 3536 5030 name = "zerovec" 3537 - version = "0.11.4" 5031 + version = "0.11.5" 3538 5032 source = "registry+https://github.com/rust-lang/crates.io-index" 3539 - checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 5033 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 3540 5034 dependencies = [ 3541 5035 "yoke", 3542 5036 "zerofrom", ··· 3545 5039 3546 5040 [[package]] 3547 5041 name = "zerovec-derive" 3548 - version = "0.11.1" 5042 + version = "0.11.2" 3549 5043 source = "registry+https://github.com/rust-lang/crates.io-index" 3550 - checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 5044 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 3551 5045 dependencies = [ 3552 5046 "proc-macro2", 3553 5047 "quote", 3554 - "syn", 5048 + "syn 2.0.112", 3555 5049 ] 3556 5050 3557 5051 [[package]] 5052 + name = "zmij" 5053 + version = "1.0.8" 5054 + source = "registry+https://github.com/rust-lang/crates.io-index" 5055 + checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" 5056 + 5057 + [[package]] 3558 5058 name = "zstd" 3559 5059 version = "0.13.3" 3560 5060 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3574 5074 3575 5075 [[package]] 3576 5076 name = "zstd-sys" 3577 - version = "2.0.15+zstd.1.5.7" 5077 + version = "2.0.16+zstd.1.5.7" 3578 5078 source = "registry+https://github.com/rust-lang/crates.io-index" 3579 - checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 5079 + checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 3580 5080 dependencies = [ 3581 5081 "cc", 3582 5082 "pkg-config",
+23 -10
Cargo.toml
··· 1 1 [package] 2 2 name = "pds_gatekeeper" 3 - version = "0.1.0" 3 + version = "0.1.2" 4 4 edition = "2024" 5 + license = "MIT" 5 6 6 7 [dependencies] 7 - axum = { version = "0.8.4", features = ["macros", "json"] } 8 - tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] } 8 + axum = { version = "0.8.8", features = ["macros", "json"] } 9 + tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "signal"] } 9 10 sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "sqlite", "migrate", "chrono"] } 10 11 dotenvy = "0.15.7" 11 12 serde = { version = "1.0", features = ["derive"] } 12 13 serde_json = "1.0" 13 14 tracing = "0.1" 14 15 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 15 - hyper-util = { version = "0.1.16", features = ["client", "client-legacy"] } 16 + hyper-util = { version = "0.1.19", features = ["client", "client-legacy"] } 16 17 tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 17 - tower_governor = "0.8.0" 18 + tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 18 19 hex = "0.4" 19 20 jwt-compact = { version = "0.8.0", features = ["es256k"] } 20 21 scrypt = "0.11" 21 - lettre = { version = "0.11.18", features = ["tokio1", "pool", "tokio1-native-tls"] } 22 - handlebars = { version = "6.3.2", features = ["rust-embed"] } 23 - rust-embed = "8.7.2" 22 + #Leaveing these two cause I think it is needed by the email crate for ssl 23 + aws-lc-rs = "1.15.2" 24 + rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 + lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 26 + handlebars = { version = "6.4.0", features = ["rust-embed"] } 27 + rust-embed = "8.9.0" 24 28 axum-template = { version = "3.0.0", features = ["handlebars"] } 25 29 rand = "0.9.2" 26 - anyhow = "1.0.99" 27 - chrono = "0.4.41" 30 + anyhow = "1.0.100" 31 + chrono = { version = "0.4.42", features = ["default", "serde"] } 28 32 sha2 = "0.10" 33 + jacquard-common = "0.9.5" 34 + jacquard-identity = "0.9.5" 35 + multibase = "0.9.2" 36 + reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 37 + urlencoding = "2.1" 38 + html-escape = "0.2.13" 39 + josekit = "0.10.3" 40 + dashmap = "6.1" 41 + tower = "0.5"
+10
Dockerfile
··· 1 + FROM rust:1.89.0-bookworm AS builder 2 + WORKDIR /app 3 + COPY ../ /app 4 + RUN cargo build --release 5 + # 6 + FROM rust:1.89-slim-bookworm AS api 7 + RUN apt-get update 8 + RUN apt-get install -y ca-certificates 9 + COPY --from=builder /app/target/release/pds_gatekeeper /usr/local/bin/pds_gatekeeper 10 + CMD ["pds_gatekeeper"]
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Bailey Townsend 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+171 -12
README.md
··· 15 15 - Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins 16 16 - Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA 17 17 18 - ## Captcha on Create Account 18 + ## Captcha on account creation 19 19 20 - Future feature? 20 + Require a `verificationCode` set on the `createAccount` request. This is gotten from completing a captcha challenge 21 + hosted on the 22 + PDS mimicking what the Bluesky Entryway does. Migration tools will need to support this, but social-apps will support 23 + and redirect to `GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT`. This is how the clients know to get the code to prove a captcha 24 + was successful. 25 + 26 + - Requires `GATEKEEPER_CREATE_ACCOUNT_CAPTCHA` to be set to true. 27 + - Requires `PDS_HCAPTCHA_SITE_KEY` and `PDS_HCAPTCHA_SECRET_KEY` to be set. Can sign up at https://www.hcaptcha.com/ 28 + - Requires proxying `/xrpc/com.atproto.server.describeServer`, `/xrpc/com.atproto.server.createAccount` and `/gate/*` to 29 + PDS 30 + Gatekeeper 31 + - Optional `GATEKEEPER_JWE_KEY` key to encrypt the captcha verification code. Defaults to a random 32 byte key. Not 32 + strictly needed unless you're scaling 33 + - Optional`GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT` default redirect on captcha success. Defaults to `https://bsky.app`. 34 + - Optional `GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS` allowed redirect urls for captcha success. You want these to match the 35 + url showing the captcha. Defaults are: 36 + - https://bsky.app 37 + - https://pdsmoover.com 38 + - https://blacksky.community 39 + - https://tektite.cc 40 + 41 + ## Block account creation unless it's a migration 42 + 43 + You can set `GATEKEEPER_ALLOW_ONLY_MIGRATIONS` to block createAccount unless it's via a migration. This does not require 44 + a change for migration tools, but social-apps create a new account will no longer work and to create a brand new account 45 + users will need to do this via the Oauth account create screen on the PDS. We recommend setting `PDS_HCAPTCHA_SITE_KEY` 46 + and `PDS_HCAPTCHA_SECRET_KEY` so the OAuth screen is protected by a captcha if you use this with invite codes turned 47 + off. 21 48 22 49 # Setup 23 50 24 - We are getting close! Testing now 51 + PDS Gatekeeper has 2 parts to its setup, docker compose file and a reverse proxy (Caddy in this case). I will be 52 + assuming you setup the PDS following the directions 53 + found [here](https://atproto.com/guides/self-hosting), but if yours is different, or you have questions, feel free to 54 + let 55 + me know, and we can figure it out. 56 + 57 + ## Docker compose 58 + 59 + The pds gatekeeper container can be found on docker hub under the name `fatfingers23/pds_gatekeeper`. The container does 60 + need access to the `/pds` root folder to access the same db's as your PDS. The part you need to add would look a bit 61 + like below. You can find a full example of what I use for my pds at [./examples/compose.yml](./examples/compose.yml). 62 + This is usually found at `/pds/compose.yaml`on your PDS> 63 + 64 + ```yml 65 + gatekeeper: 66 + container_name: gatekeeper 67 + image: fatfingers23/pds_gatekeeper:latest 68 + network_mode: host 69 + restart: unless-stopped 70 + #This gives the container to the access to the PDS folder. Source is the location on your server of that directory 71 + volumes: 72 + - type: bind 73 + source: /pds 74 + target: /pds 75 + depends_on: 76 + - pds 77 + ``` 78 + 79 + For Coolify, if you're using Traefik as your proxy you'll need to make sure the labels for the container are set up 80 + correctly. A full example can be found at [./examples/coolify-compose.yml](./examples/coolify-compose.yml). 81 + 82 + ```yml 83 + gatekeeper: 84 + container_name: gatekeeper 85 + image: 'fatfingers23/pds_gatekeeper:latest' 86 + restart: unless-stopped 87 + volumes: 88 + - '/pds:/pds' 89 + environment: 90 + - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' 91 + - 'PDS_BASE_URL=http://pds:3000' 92 + - GATEKEEPER_HOST=0.0.0.0 93 + depends_on: 94 + - pds 95 + healthcheck: 96 + test: 97 + - CMD 98 + - timeout 99 + - '1' 100 + - bash 101 + - '-c' 102 + - 'cat < /dev/null > /dev/tcp/0.0.0.0/8080' 103 + interval: 10s 104 + timeout: 5s 105 + retries: 3 106 + start_period: 10s 107 + labels: 108 + - traefik.enable=true 109 + - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`))' 110 + - traefik.http.routers.pds-gatekeeper.entrypoints=https 111 + - traefik.http.routers.pds-gatekeeper.tls=true 112 + - traefik.http.routers.pds-gatekeeper.priority=100 113 + - traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors 114 + - traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080 115 + - traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http 116 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH' 117 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*' 118 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*' 119 + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100 120 + - traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true 121 + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true 122 + ``` 25 123 26 - Nothing here yet! If you are brave enough to try before full release, let me know and I'll help you set it up. 27 - But I want to run it locally on my own PDS first to test run it a bit. 124 + ## Caddy setup 28 125 29 - Example Caddyfile (mostly so I don't lose it for now. Will have a better one in the future) 126 + For the reverse proxy I use caddy. This part is what overwrites the endpoints and proxies them to PDS gatekeeper to add 127 + in extra functionality. The main part is below, for a full example see [./examples/Caddyfile](./examples/Caddyfile). 128 + This is usually found at `/pds/caddy/etc/caddy/Caddyfile` on your PDS. 30 129 31 - ```caddyfile 32 - http://localhost { 130 + ``` 131 + @gatekeeper { 132 + path /xrpc/com.atproto.server.getSession 133 + path /xrpc/com.atproto.server.describeServer 134 + path /xrpc/com.atproto.server.updateEmail 135 + path /xrpc/com.atproto.server.createSession 136 + path /xrpc/com.atproto.server.createAccount 137 + path /@atproto/oauth-provider/~api/sign-in 138 + path /gate/* 139 + } 140 + 141 + handle @gatekeeper { 142 + reverse_proxy http://localhost:8080 143 + } 33 144 145 + reverse_proxy http://localhost:3000 146 + ``` 147 + 148 + If you use a cloudflare tunnel then your caddyfile would look a bit more like below with your tunnel proxying to 149 + `localhost:8081` (or w/e port you want). 150 + 151 + ``` 152 + http://*.localhost:8082, http://localhost:8082 { 34 153 @gatekeeper { 35 154 path /xrpc/com.atproto.server.getSession 155 + path /xrpc/com.atproto.server.describeServer 36 156 path /xrpc/com.atproto.server.updateEmail 37 157 path /xrpc/com.atproto.server.createSession 158 + path /xrpc/com.atproto.server.createAccount 38 159 path /@atproto/oauth-provider/~api/sign-in 160 + path /gate/* 39 161 } 40 162 41 163 handle @gatekeeper { 42 - reverse_proxy http://localhost:8080 164 + #This is the address for PDS gatekeeper, default is 8080 165 + reverse_proxy http://localhost:8080 166 + #Makes sure the cloudflare ip is proxied and able to be picked up by pds gatekeeper 167 + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} 43 168 } 169 + reverse_proxy http://localhost:3000 170 + } 171 + 172 + ``` 173 + 174 + # Environment variables and bonuses 175 + 176 + Every environment variable can be set in the `pds.env` and shared between PDS and gatekeeper and the PDS, with the 177 + exception of `PDS_ENV_LOCATION`. This can be set to load the pds.env, by default it checks `/pds/pds.env` and is 178 + recommended to mount the `/pds` folder on the server to `/pds` in the pds gatekeeper container. 179 + 180 + `PDS_DATA_DIRECTORY` - Root directory of the PDS. Same as the one found in `pds.env` this is how pds gatekeeper knows 181 + knows the rest of the environment variables. 182 + 183 + `GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY` - The folder for templates of the emails PDS gatekeeper sends. You can find them 184 + in [./email_templates](./email_templates). You are free to edit them as you please and set this variable to a location 185 + in the pds gateekeper container and it will use them in place of the default ones. Just make sure ot keep the names the 186 + same. 44 187 45 - reverse_proxy /* http://localhost:3000 46 - } 188 + `GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT` - Subject of the email sent to the user when they turn on 2FA. Defaults to 189 + `Sign in to Bluesky` 190 + 191 + `PDS_BASE_URL` - Base url of the PDS. You most likely want `https://localhost:3000` which is also the default 192 + 193 + `GATEKEEPER_HOST` - Host for pds gatekeeper. Defaults to `127.0.0.1` 194 + 195 + `GATEKEEPER_PORT` - Port for pds gatekeeper. Defaults to `8080` 47 196 48 - ``` 197 + `GATEKEEPER_CREATE_ACCOUNT_PER_SECOND` - Sets how often it takes a count off the limiter. example if you hit the rate 198 + limit of 5 and set to 60, then in 60 seconds you will be able to make one more. Or in 5 minutes be able to make 5 more. 199 + 200 + `GATEKEEPER_CREATE_ACCOUNT_BURST` - Sets how many requests can be made in a burst. In the prior example this is where 201 + the 5 comes from. Example can set this to 10 to allow for 10 requests in a burst, and after 60 seconds it will drop one 202 + off. 203 + 204 + `GATEKEEPER_ALLOW_ONLY_MIGRATIONS` - Defaults false. If set to true, will only allow the 205 + `/xrpc/com.atproto.server.createAccount` endpoint to be used for migrations. Meaning it will check for the serviceAuth 206 + token and verify it is valid. 207 +
+30
examples/Caddyfile
··· 1 + { 2 + email youremail@myemail.com 3 + on_demand_tls { 4 + ask http://localhost:3000/tls-check 5 + } 6 + } 7 + 8 + *.yourpds.com, yourpds.com { 9 + tls { 10 + on_demand 11 + } 12 + # You'll most likely just want from here to.... 13 + @gatekeeper { 14 + path /xrpc/com.atproto.server.getSession 15 + path /xrpc/com.atproto.server.describeServer 16 + path /xrpc/com.atproto.server.updateEmail 17 + path /xrpc/com.atproto.server.createSession 18 + path /xrpc/com.atproto.server.createAccount 19 + path /@atproto/oauth-provider/~api/sign-in 20 + path /gate/* 21 + } 22 + 23 + handle @gatekeeper { 24 + #This is the address for PDS gatekeeper, default is 8080 25 + reverse_proxy http://localhost:8080 26 + } 27 + 28 + reverse_proxy http://localhost:3000 29 + #..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line 30 + }
+51
examples/compose.yml
··· 1 + version: '3.9' 2 + services: 3 + caddy: 4 + container_name: caddy 5 + image: caddy:2 6 + network_mode: host 7 + depends_on: 8 + - pds 9 + restart: unless-stopped 10 + volumes: 11 + - type: bind 12 + source: /pds/caddy/data 13 + target: /data 14 + - type: bind 15 + source: /pds/caddy/etc/caddy 16 + target: /etc/caddy 17 + pds: 18 + container_name: pds 19 + image: ghcr.io/bluesky-social/pds:0.4 20 + network_mode: host 21 + restart: unless-stopped 22 + volumes: 23 + - type: bind 24 + source: /pds 25 + target: /pds 26 + env_file: 27 + - /pds/pds.env 28 + watchtower: 29 + container_name: watchtower 30 + image: containrrr/watchtower:latest 31 + network_mode: host 32 + volumes: 33 + - type: bind 34 + source: /var/run/docker.sock 35 + target: /var/run/docker.sock 36 + restart: unless-stopped 37 + environment: 38 + WATCHTOWER_CLEANUP: true 39 + WATCHTOWER_SCHEDULE: "@midnight" 40 + gatekeeper: 41 + container_name: gatekeeper 42 + image: fatfingers23/pds_gatekeeper:latest 43 + network_mode: host 44 + restart: unless-stopped 45 + #This gives the container to the access to the PDS folder. Source is the location on your server of that directory 46 + volumes: 47 + - type: bind 48 + source: /pds 49 + target: /pds 50 + depends_on: 51 + - pds
+73
examples/coolify-compose.yml
··· 1 + services: 2 + pds: 3 + image: 'ghcr.io/bluesky-social/pds:0.4.182' 4 + volumes: 5 + - '/pds:/pds' 6 + environment: 7 + - SERVICE_URL_PDS_3000 8 + - 'PDS_HOSTNAME=${SERVICE_FQDN_PDS_3000}' 9 + - 'PDS_JWT_SECRET=${SERVICE_HEX_32_JWTSECRET}' 10 + - 'PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}' 11 + - 'PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL}' 12 + - 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}' 13 + - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' 14 + - 'PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks' 15 + - 'PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-104857600}' 16 + - 'PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}' 17 + - 'PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS}' 18 + - 'PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL}' 19 + - 'PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}' 20 + - 'PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}' 21 + - 'PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport}' 22 + - 'PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}' 23 + - 'PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}' 24 + - 'LOG_ENABLED=${LOG_ENABLED:-true}' 25 + command: "sh -c '\n set -euo pipefail\n echo \"Installing required packages and pdsadmin...\"\n apk add --no-cache openssl curl bash jq coreutils gnupg util-linux-misc >/dev/null\n curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh\n chmod 700 /usr/local/bin/pdsadmin.sh\n ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin\n echo \"Creating an empty pds.env file so pdsadmin works...\"\n touch ${PDS_DATA_DIRECTORY}/pds.env\n echo \"Launching PDS, enjoy!...\"\n exec node --enable-source-maps index.js\n'\n" 26 + healthcheck: 27 + test: 28 + - CMD 29 + - wget 30 + - '--spider' 31 + - 'http://127.0.0.1:3000/xrpc/_health' 32 + interval: 5s 33 + timeout: 10s 34 + retries: 10 35 + gatekeeper: 36 + container_name: gatekeeper 37 + image: 'fatfingers23/pds_gatekeeper:latest' 38 + restart: unless-stopped 39 + volumes: 40 + - '/pds:/pds' 41 + environment: 42 + - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' 43 + - 'PDS_BASE_URL=http://pds:3000' 44 + - GATEKEEPER_HOST=0.0.0.0 45 + depends_on: 46 + - pds 47 + healthcheck: 48 + test: 49 + - CMD 50 + - timeout 51 + - '1' 52 + - bash 53 + - '-c' 54 + - 'cat < /dev/null > /dev/tcp/0.0.0.0/8080' 55 + interval: 10s 56 + timeout: 5s 57 + retries: 3 58 + start_period: 10s 59 + labels: 60 + - traefik.enable=true 61 + - 'traefik.http.routers.pds-gatekeeper.rule=Host(`yourpds.com`) && (Path(`/xrpc/com.atproto.server.getSession`) || Path(`/xrpc/com.atproto.server.describeServer`) || Path(`/xrpc/com.atproto.server.updateEmail`) || Path(`/xrpc/com.atproto.server.createSession`) || Path(`/xrpc/com.atproto.server.createAccount`) || Path(`/@atproto/oauth-provider/~api/sign-in`) || Path(`/gate`))' 62 + - traefik.http.routers.pds-gatekeeper.entrypoints=https 63 + - traefik.http.routers.pds-gatekeeper.tls=true 64 + - traefik.http.routers.pds-gatekeeper.priority=100 65 + - traefik.http.routers.pds-gatekeeper.middlewares=gatekeeper-cors 66 + - traefik.http.services.pds-gatekeeper.loadbalancer.server.port=8080 67 + - traefik.http.services.pds-gatekeeper.loadbalancer.server.scheme=http 68 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS,PATCH' 69 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowheaders=*' 70 + - 'traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolalloworiginlist=*' 71 + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolmaxage=100 72 + - traefik.http.middlewares.gatekeeper-cors.headers.addvaryheader=true 73 + - traefik.http.middlewares.gatekeeper-cors.headers.accesscontrolallowcredentials=true
+166
html_templates/captcha.hbs
··· 1 + <html lang="en" class=" "> 2 + <head> 3 + <meta charset="utf-8"/> 4 + <meta 5 + name="viewport" 6 + content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover" 7 + /> 8 + <meta name="referrer" content="origin-when-cross-origin"/> 9 + 10 + <title> 11 + {{pds}} - Captcha 12 + </title> 13 + <style> 14 + :root, 15 + :root.light-mode { 16 + --brand-color: rgb(16, 131, 254); 17 + --primary-color: rgb(7, 10, 13); 18 + --secondary-color: rgb(66, 86, 108); 19 + --bg-primary-color: rgb(255, 255, 255); 20 + --bg-secondary-color: rgb(240, 242, 245); 21 + } 22 + 23 + @media (prefers-color-scheme: dark) { 24 + :root { 25 + --brand-color: rgb(16, 131, 254); 26 + --primary-color: rgb(255, 255, 255); 27 + --secondary-color: rgb(133, 152, 173); 28 + --bg-primary-color: rgb(7, 10, 13); 29 + --bg-secondary-color: rgb(13, 18, 23); 30 + } 31 + } 32 + 33 + :root.dark-mode { 34 + --brand-color: rgb(16, 131, 254); 35 + --primary-color: rgb(255, 255, 255); 36 + --secondary-color: rgb(133, 152, 173); 37 + --bg-primary-color: rgb(7, 10, 13); 38 + --bg-secondary-color: rgb(13, 18, 23); 39 + } 40 + 41 + body { 42 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, 43 + Arial, sans-serif; 44 + background: var(--bg-primary-color); 45 + color: var(--primary-color); 46 + text-rendering: optimizeLegibility; 47 + -webkit-font-smoothing: antialiased; 48 + } 49 + 50 + .info { 51 + border-radius: 8px; 52 + padding: 12px 14px; 53 + letter-spacing: 0.25px; 54 + font-weight: 400; 55 + background-color: var(--bg-secondary-color); 56 + color: var(--secondary-color); 57 + } 58 + 59 + .gate-page, 60 + .error-page { 61 + margin: 0; 62 + margin-top: 10px; 63 + display: flex; 64 + justify-content: center; 65 + } 66 + 67 + .gate-page .main, 68 + .error-page .main { 69 + max-width: 600px; 70 + width: 100%; 71 + display: flex; 72 + flex-direction: column; 73 + } 74 + 75 + .gate-page .main { 76 + padding: 0; 77 + } 78 + 79 + .error-page .main { 80 + padding: 20px; 81 + } 82 + 83 + .gate-page .main > :not(:first-child), 84 + .error-page .main > :not(:first-child) { 85 + margin-top: 20px; 86 + } 87 + 88 + .gate-page #gate-form { 89 + margin: 0; 90 + } 91 + 92 + .gate-page #hcaptcha { 93 + display: flex; 94 + justify-content: center; 95 + } 96 + 97 + .pds-title { 98 + font-size: 2.25rem; 99 + font-weight: 700; 100 + line-height: 1.2; 101 + text-align: center; 102 + color: var(--primary-color); 103 + margin: 10px 0 16px; 104 + } 105 + 106 + </style> 107 + 108 + <script type="text/javascript"> 109 + function onCaptchaReady() { 110 + const theme = document.documentElement.classList.contains('dark-mode') ? 111 + 'dark' : 112 + document.documentElement.classList.contains('light-mode') ? 113 + 'light' : 114 + window.matchMedia('(prefers-color-scheme: dark)').matches ? 115 + 'dark' : 116 + 'light' 117 + hcaptcha.render('hcaptcha', {theme}); 118 + } 119 + 120 + function onCaptchaComplete() { 121 + setTimeout(function () { 122 + document.getElementById('gate-form').submit(); 123 + }, 1000); 124 + } 125 + 126 + function onCaptchaError() { 127 + const url = new URL(location.href); 128 + url.searchParams.set('error', 'true'); 129 + location.assign(url.search); 130 + } 131 + 132 + function onCaptchaExpired() { 133 + const url = new URL(location.href); 134 + url.searchParams.set('error', 'expired'); 135 + location.assign(url.search); 136 + } 137 + </script> 138 + <script 139 + src="https://js.hcaptcha.com/1/api.js?render=explicit&onload=onCaptchaReady" 140 + async 141 + defer 142 + ></script> 143 + <link rel="stylesheet" href="/gate/signup/assets/common.css"/> 144 + </head> 145 + 146 + <body class="gate-page"> 147 + <div class="main"> 148 + <div class="pds-title">{{pds}}</div> 149 + <form id="gate-form" action="" method="POST"> 150 + <div 151 + id="hcaptcha" 152 + data-sitekey="{{captcha_site_key}}" 153 + data-callback="onCaptchaComplete" 154 + data-error-callback="onCaptchaError" 155 + data-expired-callback="onCaptchaExpired" 156 + data-chalexpired-callback="onCaptchaExpired" 157 + ></div> 158 + <input type="hidden" name="redirect_url" value="{{redirect_url}}"/> 159 + </form> 160 + {{#if error_message }} 161 + <div class="info">{{error_message}}</div> 162 + {{/if}} 163 + 164 + </div> 165 + </body> 166 + </html>
+6
justfile
··· 1 + release: 2 + docker buildx build \ 3 + --platform linux/arm64,linux/amd64 \ 4 + --tag fatfingers23/pds_gatekeeper:latest \ 5 + --tag fatfingers23/pds_gatekeeper:0.1.0.5 \ 6 + --push .
+10
migrations/20251126000000_gate_codes.sql
··· 1 + -- Add migration script here 2 + CREATE TABLE IF NOT EXISTS gate_codes 3 + ( 4 + code VARCHAR PRIMARY KEY, 5 + handle VARCHAR NOT NULL, 6 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 7 + ); 8 + 9 + -- Index on created_at for efficient cleanup of expired codes 10 + CREATE INDEX IF NOT EXISTS idx_gate_codes_created_at ON gate_codes(created_at);
+101
src/auth/cache.rs
··· 1 + use dashmap::DashMap; 2 + use std::sync::Arc; 3 + use std::time::{Duration, Instant}; 4 + 5 + #[derive(Clone, Debug)] 6 + struct CachedHandle { 7 + handle: String, 8 + cached_at: Instant, 9 + } 10 + 11 + /// A thread-safe cache for DID-to-handle resolutions with TTL expiration. 12 + #[derive(Clone)] 13 + pub struct HandleCache { 14 + cache: Arc<DashMap<String, CachedHandle>>, 15 + ttl: Duration, 16 + } 17 + 18 + impl Default for HandleCache { 19 + fn default() -> Self { 20 + Self::new() 21 + } 22 + } 23 + 24 + impl HandleCache { 25 + /// Creates a new HandleCache with a default TTL of 1 hour. 26 + pub fn new() -> Self { 27 + Self::with_ttl(Duration::from_secs(3600)) 28 + } 29 + 30 + /// Creates a new HandleCache with a custom TTL. 31 + pub fn with_ttl(ttl: Duration) -> Self { 32 + Self { 33 + cache: Arc::new(DashMap::new()), 34 + ttl, 35 + } 36 + } 37 + 38 + /// Gets a cached handle for the given DID, if it exists and hasn't expired. 39 + pub fn get(&self, did: &str) -> Option<String> { 40 + let entry = self.cache.get(did)?; 41 + if entry.cached_at.elapsed() > self.ttl { 42 + drop(entry); 43 + self.cache.remove(did); 44 + return None; 45 + } 46 + Some(entry.handle.clone()) 47 + } 48 + 49 + /// Inserts a DID-to-handle mapping into the cache. 50 + pub fn insert(&self, did: String, handle: String) { 51 + self.cache.insert( 52 + did, 53 + CachedHandle { 54 + handle, 55 + cached_at: Instant::now(), 56 + }, 57 + ); 58 + } 59 + 60 + /// Removes expired entries from the cache. 61 + /// Call this periodically to prevent memory growth. 62 + pub fn cleanup(&self) { 63 + self.cache.retain(|_, v| v.cached_at.elapsed() <= self.ttl); 64 + } 65 + 66 + /// Returns the number of entries in the cache. 67 + pub fn len(&self) -> usize { 68 + self.cache.len() 69 + } 70 + 71 + /// Returns true if the cache is empty. 72 + pub fn is_empty(&self) -> bool { 73 + self.cache.is_empty() 74 + } 75 + } 76 + 77 + #[cfg(test)] 78 + mod tests { 79 + use super::*; 80 + 81 + #[test] 82 + fn test_cache_insert_and_get() { 83 + let cache = HandleCache::new(); 84 + cache.insert("did:plc:test".into(), "test.handle.com".into()); 85 + assert_eq!(cache.get("did:plc:test"), Some("test.handle.com".into())); 86 + } 87 + 88 + #[test] 89 + fn test_cache_miss() { 90 + let cache = HandleCache::new(); 91 + assert_eq!(cache.get("did:plc:nonexistent"), None); 92 + } 93 + 94 + #[test] 95 + fn test_cache_expiration() { 96 + let cache = HandleCache::with_ttl(Duration::from_millis(1)); 97 + cache.insert("did:plc:test".into(), "test.handle.com".into()); 98 + std::thread::sleep(Duration::from_millis(10)); 99 + assert_eq!(cache.get("did:plc:test"), None); 100 + } 101 + }
+456
src/auth/middleware.rs
··· 1 + use super::{AuthRules, HandleCache, SessionData}; 2 + use crate::helpers::json_error_response; 3 + use crate::AppState; 4 + use axum::extract::{Request, State}; 5 + use axum::http::{HeaderMap, StatusCode}; 6 + use axum::middleware::Next; 7 + use axum::response::{IntoResponse, Response}; 8 + use jacquard_identity::resolver::IdentityResolver; 9 + use jacquard_identity::PublicResolver; 10 + use jwt_compact::alg::{Hs256, Hs256Key}; 11 + use jwt_compact::{AlgorithmExt, Claims, Token, UntrustedToken, ValidationError}; 12 + use serde::{Deserialize, Serialize}; 13 + use std::env; 14 + use std::sync::Arc; 15 + use tracing::log; 16 + 17 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 18 + pub enum AuthScheme { 19 + Bearer, 20 + DPoP, 21 + } 22 + 23 + #[derive(Serialize, Deserialize)] 24 + pub struct TokenClaims { 25 + pub sub: String, 26 + /// OAuth scopes as space-separated string (per OAuth 2.0 spec) 27 + #[serde(default)] 28 + pub scope: Option<String>, 29 + } 30 + 31 + /// State passed to the auth middleware containing both AppState and auth rules. 32 + #[derive(Clone)] 33 + pub struct AuthMiddlewareState { 34 + pub app_state: AppState, 35 + pub rules: AuthRules, 36 + } 37 + 38 + /// Core middleware function that validates authentication and applies auth rules. 39 + /// 40 + /// Use this with `axum::middleware::from_fn_with_state`: 41 + /// ```ignore 42 + /// use axum::middleware::from_fn_with_state; 43 + /// 44 + /// let mw_state = AuthMiddlewareState { 45 + /// app_state: state.clone(), 46 + /// rules: AuthRules::HandleEndsWith(".blacksky.team".into()), 47 + /// }; 48 + /// 49 + /// .route("/protected", get(handler).layer(from_fn_with_state(mw_state, auth_middleware))) 50 + /// ``` 51 + pub async fn auth_middleware( 52 + State(mw_state): State<AuthMiddlewareState>, 53 + req: Request, 54 + next: Next, 55 + ) -> Response { 56 + let AuthMiddlewareState { app_state, rules } = mw_state; 57 + 58 + // 1. Extract DID and scopes from JWT (Bearer token) 59 + let extracted = match extract_auth_from_request(req.headers()) { 60 + Ok(Some(auth)) => auth, 61 + Ok(None) => { 62 + return json_error_response(StatusCode::UNAUTHORIZED, "AuthRequired", "Authentication required") 63 + .unwrap_or_else(|_| StatusCode::UNAUTHORIZED.into_response()); 64 + } 65 + Err(e) => { 66 + log::error!("Token extraction error: {}", e); 67 + return json_error_response(StatusCode::UNAUTHORIZED, "InvalidToken", &e) 68 + .unwrap_or_else(|_| StatusCode::UNAUTHORIZED.into_response()); 69 + } 70 + }; 71 + 72 + // 2. Resolve DID to handle (check cache first) 73 + let handle = match resolve_did_to_handle(&app_state.resolver, &app_state.handle_cache, &extracted.did).await { 74 + Ok(handle) => handle, 75 + Err(e) => { 76 + log::error!("Failed to resolve DID {} to handle: {}", extracted.did, e); 77 + return json_error_response( 78 + StatusCode::INTERNAL_SERVER_ERROR, 79 + "ResolutionError", 80 + "Failed to resolve identity", 81 + ) 82 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 83 + } 84 + }; 85 + 86 + // 3. Build session data and validate rules 87 + let session = SessionData { 88 + did: extracted.did, 89 + handle, 90 + scopes: extracted.scopes, 91 + }; 92 + 93 + if !rules.validate(&session) { 94 + return json_error_response(StatusCode::FORBIDDEN, "AccessDenied", "Access denied by authorization rules") 95 + .unwrap_or_else(|_| StatusCode::FORBIDDEN.into_response()); 96 + } 97 + 98 + // 4. Pass through on success 99 + next.run(req).await 100 + } 101 + 102 + /// Extracted authentication data from JWT 103 + struct ExtractedAuth { 104 + did: String, 105 + scopes: Vec<String>, 106 + } 107 + 108 + /// Extracts the DID and scopes from the Authorization header (Bearer JWT). 109 + fn extract_auth_from_request(headers: &HeaderMap) -> Result<Option<ExtractedAuth>, String> { 110 + let auth = extract_auth(headers)?; 111 + 112 + match auth { 113 + None => Ok(None), 114 + Some((scheme, token_str)) => { 115 + match scheme { 116 + AuthScheme::Bearer => { 117 + let token = UntrustedToken::new(&token_str) 118 + .map_err(|_| "Invalid token format".to_string())?; 119 + 120 + let _claims: Claims<TokenClaims> = token 121 + .deserialize_claims_unchecked() 122 + .map_err(|_| "Failed to parse token claims".to_string())?; 123 + 124 + let key = Hs256Key::new( 125 + env::var("PDS_JWT_SECRET") 126 + .map_err(|_| "PDS_JWT_SECRET not configured".to_string())?, 127 + ); 128 + 129 + let validated: Token<TokenClaims> = Hs256 130 + .validator(&key) 131 + .validate(&token) 132 + .map_err(|e: ValidationError| format!("Token validation failed: {:?}", e))?; 133 + 134 + let custom = &validated.claims().custom; 135 + 136 + // Parse scopes from space-separated string (OAuth 2.0 spec) 137 + let scopes: Vec<String> = custom.scope 138 + .as_ref() 139 + .map(|s| s.split_whitespace().map(|s| s.to_string()).collect()) 140 + .unwrap_or_default(); 141 + 142 + Ok(Some(ExtractedAuth { 143 + did: custom.sub.clone(), 144 + scopes, 145 + })) 146 + } 147 + AuthScheme::DPoP => { 148 + // DPoP tokens are not validated here; pass through without auth data 149 + Ok(None) 150 + } 151 + } 152 + } 153 + } 154 + } 155 + 156 + /// Extracts the authentication scheme and token from the Authorization header. 157 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 158 + match headers.get(axum::http::header::AUTHORIZATION) { 159 + None => Ok(None), 160 + Some(hv) => { 161 + let s = hv 162 + .to_str() 163 + .map_err(|_| "Authorization header is not valid UTF-8".to_string())?; 164 + 165 + let mut parts = s.splitn(2, ' '); 166 + match (parts.next(), parts.next()) { 167 + (Some("Bearer"), Some(tok)) if !tok.is_empty() => { 168 + Ok(Some((AuthScheme::Bearer, tok.to_string()))) 169 + } 170 + (Some("DPoP"), Some(tok)) if !tok.is_empty() => { 171 + Ok(Some((AuthScheme::DPoP, tok.to_string()))) 172 + } 173 + _ => Err( 174 + "Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'" 175 + .to_string(), 176 + ), 177 + } 178 + } 179 + } 180 + } 181 + 182 + /// Resolves a DID to its handle using the PublicResolver, with caching. 183 + async fn resolve_did_to_handle( 184 + resolver: &Arc<PublicResolver>, 185 + cache: &HandleCache, 186 + did: &str, 187 + ) -> Result<String, String> { 188 + // Check cache first 189 + if let Some(handle) = cache.get(did) { 190 + return Ok(handle); 191 + } 192 + 193 + // Parse the DID 194 + let did_parsed = jacquard_common::types::did::Did::new(did) 195 + .map_err(|e| format!("Invalid DID: {:?}", e))?; 196 + 197 + // Resolve the DID document 198 + let did_doc_response = resolver 199 + .resolve_did_doc(&did_parsed) 200 + .await 201 + .map_err(|e| format!("DID resolution failed: {:?}", e))?; 202 + 203 + let doc = did_doc_response 204 + .parse() 205 + .map_err(|e| format!("Failed to parse DID document: {:?}", e))?; 206 + 207 + // Extract handle from alsoKnownAs field 208 + // Format is typically: ["at://handle.example.com"] 209 + let handle: String = doc 210 + .also_known_as 211 + .as_ref() 212 + .and_then(|aka| { 213 + aka.iter() 214 + .find(|uri| uri.starts_with("at://")) 215 + .map(|uri| uri.strip_prefix("at://").unwrap_or(uri.as_ref()).to_string()) 216 + }) 217 + .ok_or_else(|| "No ATProto handle found in DID document".to_string())?; 218 + 219 + // Cache the result 220 + cache.insert(did.to_string(), handle.clone()); 221 + 222 + Ok(handle) 223 + } 224 + 225 + // ============================================================================ 226 + // Helper Functions for Creating Middleware State 227 + // ============================================================================ 228 + 229 + /// Creates an `AuthMiddlewareState` for requiring the handle to end with a specific suffix. 230 + /// 231 + /// # Example 232 + /// ```ignore 233 + /// use axum::middleware::from_fn_with_state; 234 + /// use crate::auth::{auth_middleware, handle_ends_with}; 235 + /// 236 + /// .route("/protected", get(handler).layer( 237 + /// from_fn_with_state(handle_ends_with(".blacksky.team", &state), auth_middleware) 238 + /// )) 239 + /// ``` 240 + pub fn handle_ends_with(suffix: impl Into<String>, state: &AppState) -> AuthMiddlewareState { 241 + AuthMiddlewareState { 242 + app_state: state.clone(), 243 + rules: AuthRules::HandleEndsWith(suffix.into()), 244 + } 245 + } 246 + 247 + /// Creates an `AuthMiddlewareState` for requiring the handle to end with any of the specified suffixes. 248 + pub fn handle_ends_with_any<I, T>(suffixes: I, state: &AppState) -> AuthMiddlewareState 249 + where 250 + I: IntoIterator<Item = T>, 251 + T: Into<String>, 252 + { 253 + AuthMiddlewareState { 254 + app_state: state.clone(), 255 + rules: AuthRules::HandleEndsWithAny(suffixes.into_iter().map(|s| s.into()).collect()), 256 + } 257 + } 258 + 259 + /// Creates an `AuthMiddlewareState` for requiring the DID to equal a specific value. 260 + pub fn did_equals(did: impl Into<String>, state: &AppState) -> AuthMiddlewareState { 261 + AuthMiddlewareState { 262 + app_state: state.clone(), 263 + rules: AuthRules::DidEquals(did.into()), 264 + } 265 + } 266 + 267 + /// Creates an `AuthMiddlewareState` for requiring the DID to be one of the specified values. 268 + pub fn did_equals_any<I, T>(dids: I, state: &AppState) -> AuthMiddlewareState 269 + where 270 + I: IntoIterator<Item = T>, 271 + T: Into<String>, 272 + { 273 + AuthMiddlewareState { 274 + app_state: state.clone(), 275 + rules: AuthRules::DidEqualsAny(dids.into_iter().map(|d| d.into()).collect()), 276 + } 277 + } 278 + 279 + /// Creates an `AuthMiddlewareState` with custom auth rules. 280 + pub fn with_rules(rules: AuthRules, state: &AppState) -> AuthMiddlewareState { 281 + AuthMiddlewareState { 282 + app_state: state.clone(), 283 + rules, 284 + } 285 + } 286 + 287 + // ============================================================================ 288 + // Scope Helper Functions 289 + // ============================================================================ 290 + 291 + /// Creates an `AuthMiddlewareState` requiring a specific OAuth scope. 292 + /// 293 + /// # Example 294 + /// ```ignore 295 + /// .route("/xrpc/com.atproto.repo.createRecord", 296 + /// post(handler).layer(from_fn_with_state( 297 + /// scope_equals("repo:app.bsky.feed.post", &state), 298 + /// auth_middleware 299 + /// ))) 300 + /// ``` 301 + pub fn scope_equals(scope: impl Into<String>, state: &AppState) -> AuthMiddlewareState { 302 + AuthMiddlewareState { 303 + app_state: state.clone(), 304 + rules: AuthRules::ScopeEquals(scope.into()), 305 + } 306 + } 307 + 308 + /// Creates an `AuthMiddlewareState` requiring ANY of the specified scopes (OR logic). 309 + /// 310 + /// # Example 311 + /// ```ignore 312 + /// .route("/xrpc/com.atproto.repo.putRecord", 313 + /// post(handler).layer(from_fn_with_state( 314 + /// scope_any(["repo:app.bsky.feed.post", "transition:generic"], &state), 315 + /// auth_middleware 316 + /// ))) 317 + /// ``` 318 + pub fn scope_any<I, T>(scopes: I, state: &AppState) -> AuthMiddlewareState 319 + where 320 + I: IntoIterator<Item = T>, 321 + T: Into<String>, 322 + { 323 + AuthMiddlewareState { 324 + app_state: state.clone(), 325 + rules: AuthRules::ScopeEqualsAny(scopes.into_iter().map(|s| s.into()).collect()), 326 + } 327 + } 328 + 329 + /// Creates an `AuthMiddlewareState` requiring ALL of the specified scopes (AND logic). 330 + /// 331 + /// # Example 332 + /// ```ignore 333 + /// .route("/xrpc/com.atproto.admin.updateAccount", 334 + /// post(handler).layer(from_fn_with_state( 335 + /// scope_all(["account:email", "account:repo?action=manage"], &state), 336 + /// auth_middleware 337 + /// ))) 338 + /// ``` 339 + pub fn scope_all<I, T>(scopes: I, state: &AppState) -> AuthMiddlewareState 340 + where 341 + I: IntoIterator<Item = T>, 342 + T: Into<String>, 343 + { 344 + AuthMiddlewareState { 345 + app_state: state.clone(), 346 + rules: AuthRules::ScopeEqualsAll(scopes.into_iter().map(|s| s.into()).collect()), 347 + } 348 + } 349 + 350 + // ============================================================================ 351 + // Combined Rule Helpers (Identity + Scope) 352 + // ============================================================================ 353 + 354 + /// Creates an `AuthMiddlewareState` requiring handle to end with suffix AND have a specific scope. 355 + /// 356 + /// # Example 357 + /// ```ignore 358 + /// .route("/xrpc/community.blacksky.feed.generator", 359 + /// post(handler).layer(from_fn_with_state( 360 + /// handle_ends_with_and_scope(".blacksky.team", "transition:generic", &state), 361 + /// auth_middleware 362 + /// ))) 363 + /// ``` 364 + pub fn handle_ends_with_and_scope( 365 + suffix: impl Into<String>, 366 + scope: impl Into<String>, 367 + state: &AppState, 368 + ) -> AuthMiddlewareState { 369 + AuthMiddlewareState { 370 + app_state: state.clone(), 371 + rules: AuthRules::All(vec![ 372 + AuthRules::HandleEndsWith(suffix.into()), 373 + AuthRules::ScopeEquals(scope.into()), 374 + ]), 375 + } 376 + } 377 + 378 + /// Creates an `AuthMiddlewareState` requiring handle to end with suffix AND have ALL specified scopes. 379 + /// 380 + /// # Example 381 + /// ```ignore 382 + /// .route("/xrpc/community.blacksky.admin.manage", 383 + /// post(handler).layer(from_fn_with_state( 384 + /// handle_ends_with_and_scopes(".blacksky.team", ["transition:generic", "identity:*"], &state), 385 + /// auth_middleware 386 + /// ))) 387 + /// ``` 388 + pub fn handle_ends_with_and_scopes<I, T>( 389 + suffix: impl Into<String>, 390 + scopes: I, 391 + state: &AppState, 392 + ) -> AuthMiddlewareState 393 + where 394 + I: IntoIterator<Item = T>, 395 + T: Into<String>, 396 + { 397 + AuthMiddlewareState { 398 + app_state: state.clone(), 399 + rules: AuthRules::All(vec![ 400 + AuthRules::HandleEndsWith(suffix.into()), 401 + AuthRules::ScopeEqualsAll(scopes.into_iter().map(|s| s.into()).collect()), 402 + ]), 403 + } 404 + } 405 + 406 + /// Creates an `AuthMiddlewareState` requiring DID to equal value AND have a specific scope. 407 + /// 408 + /// # Example 409 + /// ```ignore 410 + /// .route("/xrpc/com.atproto.admin.deleteAccount", 411 + /// post(handler).layer(from_fn_with_state( 412 + /// did_with_scope("did:plc:rnpkyqnmsw4ipey6eotbdnnf", "transition:generic", &state), 413 + /// auth_middleware 414 + /// ))) 415 + /// ``` 416 + pub fn did_with_scope( 417 + did: impl Into<String>, 418 + scope: impl Into<String>, 419 + state: &AppState, 420 + ) -> AuthMiddlewareState { 421 + AuthMiddlewareState { 422 + app_state: state.clone(), 423 + rules: AuthRules::All(vec![ 424 + AuthRules::DidEquals(did.into()), 425 + AuthRules::ScopeEquals(scope.into()), 426 + ]), 427 + } 428 + } 429 + 430 + /// Creates an `AuthMiddlewareState` requiring DID to equal value AND have ALL specified scopes. 431 + /// 432 + /// # Example 433 + /// ```ignore 434 + /// .route("/xrpc/com.atproto.admin.fullAccess", 435 + /// post(handler).layer(from_fn_with_state( 436 + /// did_with_scopes("did:plc:rnpkyqnmsw4ipey6eotbdnnf", ["transition:generic", "identity:*"], &state), 437 + /// auth_middleware 438 + /// ))) 439 + /// ``` 440 + pub fn did_with_scopes<I, T>( 441 + did: impl Into<String>, 442 + scopes: I, 443 + state: &AppState, 444 + ) -> AuthMiddlewareState 445 + where 446 + I: IntoIterator<Item = T>, 447 + T: Into<String>, 448 + { 449 + AuthMiddlewareState { 450 + app_state: state.clone(), 451 + rules: AuthRules::All(vec![ 452 + AuthRules::DidEquals(did.into()), 453 + AuthRules::ScopeEqualsAll(scopes.into_iter().map(|s| s.into()).collect()), 454 + ]), 455 + } 456 + }
+16
src/auth/mod.rs
··· 1 + mod cache; 2 + mod middleware; 3 + mod rules; 4 + 5 + pub use cache::HandleCache; 6 + pub use middleware::{ 7 + // Core middleware 8 + auth_middleware, with_rules, AuthMiddlewareState, 9 + // Identity helpers 10 + did_equals, did_equals_any, handle_ends_with, handle_ends_with_any, 11 + // Scope helpers 12 + scope_all, scope_any, scope_equals, 13 + // Combined helpers (identity + scope) 14 + did_with_scope, did_with_scopes, handle_ends_with_and_scope, handle_ends_with_and_scopes, 15 + }; 16 + pub use rules::{AuthRules, SessionData};
+694
src/auth/rules.rs
··· 1 + /// Authentication rules that can be validated against session data 2 + #[derive(Debug, Clone, PartialEq)] 3 + pub enum AuthRules { 4 + /// Handle must end with the specified suffix 5 + HandleEndsWith(String), 6 + /// Handle must end with any of the specified suffixes (OR logic) 7 + HandleEndsWithAny(Vec<String>), 8 + /// DID must exactly match the specified value 9 + DidEquals(String), 10 + /// DID must match any of the specified values (OR logic) 11 + DidEqualsAny(Vec<String>), 12 + /// Session must have the specified OAuth scope 13 + ScopeEquals(String), 14 + /// Session must have ANY of the specified scopes (OR logic) 15 + ScopeEqualsAny(Vec<String>), 16 + /// Session must have ALL of the specified scopes (AND logic) 17 + ScopeEqualsAll(Vec<String>), 18 + /// All nested rules must be satisfied (AND logic) 19 + All(Vec<AuthRules>), 20 + /// At least one nested rule must be satisfied (OR logic) 21 + Any(Vec<AuthRules>), 22 + } 23 + 24 + /// Session data used for authentication validation 25 + #[derive(Debug, Clone)] 26 + pub struct SessionData { 27 + /// The user's DID 28 + pub did: String, 29 + /// The user's handle 30 + pub handle: String, 31 + /// OAuth 2.0 scopes granted to this session 32 + pub scopes: Vec<String>, 33 + } 34 + 35 + impl AuthRules { 36 + /// Validates if the given session data meets the authentication requirements 37 + pub fn validate(&self, session_data: &SessionData) -> bool { 38 + match self { 39 + AuthRules::HandleEndsWith(suffix) => session_data.handle.ends_with(suffix), 40 + AuthRules::HandleEndsWithAny(suffixes) => { 41 + suffixes.iter().any(|s| session_data.handle.ends_with(s)) 42 + } 43 + AuthRules::DidEquals(did) => session_data.did == *did, 44 + AuthRules::DidEqualsAny(dids) => dids.iter().any(|d| session_data.did == *d), 45 + AuthRules::ScopeEquals(scope) => has_scope(&session_data.scopes, scope), 46 + AuthRules::ScopeEqualsAny(scopes) => has_any_scope(&session_data.scopes, scopes), 47 + AuthRules::ScopeEqualsAll(scopes) => has_all_scopes(&session_data.scopes, scopes), 48 + AuthRules::All(rules) => rules.iter().all(|r| r.validate(session_data)), 49 + AuthRules::Any(rules) => rules.iter().any(|r| r.validate(session_data)), 50 + } 51 + } 52 + } 53 + 54 + /// Checks if the session has a specific scope 55 + pub fn has_scope(scopes: &[String], required_scope: &str) -> bool { 56 + scopes.iter().any(|s| s == required_scope) 57 + } 58 + 59 + /// Checks if the session has ANY of the required scopes (OR logic) 60 + pub fn has_any_scope(scopes: &[String], required_scopes: &[String]) -> bool { 61 + required_scopes.iter().any(|req| has_scope(scopes, req)) 62 + } 63 + 64 + /// Checks if the session has ALL of the required scopes (AND logic) 65 + pub fn has_all_scopes(scopes: &[String], required_scopes: &[String]) -> bool { 66 + required_scopes.iter().all(|req| has_scope(scopes, req)) 67 + } 68 + 69 + #[cfg(test)] 70 + mod tests { 71 + use super::*; 72 + 73 + fn test_session(did: &str, handle: &str, scopes: Vec<&str>) -> SessionData { 74 + SessionData { 75 + did: did.to_string(), 76 + handle: handle.to_string(), 77 + scopes: scopes.into_iter().map(|s| s.to_string()).collect(), 78 + } 79 + } 80 + 81 + #[test] 82 + fn test_handle_ends_with() { 83 + let rules = AuthRules::HandleEndsWith(".blacksky.team".into()); 84 + 85 + let valid = test_session("did:plc:123", "alice.blacksky.team", vec!["atproto"]); 86 + assert!(rules.validate(&valid)); 87 + 88 + let invalid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto"]); 89 + assert!(!rules.validate(&invalid)); 90 + } 91 + 92 + #[test] 93 + fn test_handle_ends_with_any() { 94 + let rules = AuthRules::HandleEndsWithAny(vec![".blacksky.team".into(), ".bsky.team".into()]); 95 + 96 + let valid1 = test_session("did:plc:123", "alice.blacksky.team", vec!["atproto"]); 97 + assert!(rules.validate(&valid1)); 98 + 99 + let valid2 = test_session("did:plc:123", "bob.bsky.team", vec!["atproto"]); 100 + assert!(rules.validate(&valid2)); 101 + 102 + let invalid = test_session("did:plc:123", "charlie.bsky.social", vec!["atproto"]); 103 + assert!(!rules.validate(&invalid)); 104 + } 105 + 106 + #[test] 107 + fn test_did_equals() { 108 + let rules = AuthRules::DidEquals("did:plc:alice".into()); 109 + 110 + let valid = test_session("did:plc:alice", "alice.bsky.social", vec!["atproto"]); 111 + assert!(rules.validate(&valid)); 112 + 113 + let invalid = test_session("did:plc:bob", "bob.bsky.social", vec!["atproto"]); 114 + assert!(!rules.validate(&invalid)); 115 + } 116 + 117 + #[test] 118 + fn test_any_combinator() { 119 + let rules = AuthRules::Any(vec![ 120 + AuthRules::DidEquals("did:plc:admin".into()), 121 + AuthRules::HandleEndsWith(".blacksky.team".into()), 122 + ]); 123 + 124 + // First condition met 125 + let valid1 = test_session("did:plc:admin", "admin.bsky.social", vec!["atproto"]); 126 + assert!(rules.validate(&valid1)); 127 + 128 + // Second condition met 129 + let valid2 = test_session("did:plc:user", "user.blacksky.team", vec!["atproto"]); 130 + assert!(rules.validate(&valid2)); 131 + 132 + // Neither condition met 133 + let invalid = test_session("did:plc:user", "user.bsky.social", vec!["atproto"]); 134 + assert!(!rules.validate(&invalid)); 135 + } 136 + 137 + #[test] 138 + fn test_all_combinator() { 139 + let rules = AuthRules::All(vec![ 140 + AuthRules::HandleEndsWith(".blacksky.team".into()), 141 + AuthRules::DidEqualsAny(vec!["did:plc:alice".into(), "did:plc:bob".into()]), 142 + ]); 143 + 144 + // Both conditions met 145 + let valid = test_session("did:plc:alice", "alice.blacksky.team", vec!["atproto"]); 146 + assert!(rules.validate(&valid)); 147 + 148 + // Handle wrong 149 + let invalid1 = test_session("did:plc:alice", "alice.bsky.social", vec!["atproto"]); 150 + assert!(!rules.validate(&invalid1)); 151 + 152 + // DID wrong 153 + let invalid2 = test_session("did:plc:charlie", "charlie.blacksky.team", vec!["atproto"]); 154 + assert!(!rules.validate(&invalid2)); 155 + } 156 + 157 + // ======================================================================== 158 + // Scope Tests 159 + // ======================================================================== 160 + 161 + #[test] 162 + fn test_scope_equals() { 163 + let rules = AuthRules::ScopeEquals("transition:generic".into()); 164 + 165 + let valid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto", "transition:generic"]); 166 + assert!(rules.validate(&valid)); 167 + 168 + let invalid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto"]); 169 + assert!(!rules.validate(&invalid)); 170 + } 171 + 172 + #[test] 173 + fn test_scope_any() { 174 + let rules = AuthRules::ScopeEqualsAny(vec![ 175 + "transition:generic".into(), 176 + "repo:app.bsky.feed.post".into(), 177 + ]); 178 + 179 + // Has first scope 180 + let valid1 = test_session("did:plc:123", "alice.bsky.social", vec!["atproto", "transition:generic"]); 181 + assert!(rules.validate(&valid1)); 182 + 183 + // Has second scope 184 + let valid2 = test_session("did:plc:123", "alice.bsky.social", vec!["atproto", "repo:app.bsky.feed.post"]); 185 + assert!(rules.validate(&valid2)); 186 + 187 + // Has neither 188 + let invalid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto"]); 189 + assert!(!rules.validate(&invalid)); 190 + } 191 + 192 + #[test] 193 + fn test_scope_all() { 194 + let rules = AuthRules::ScopeEqualsAll(vec![ 195 + "atproto".into(), 196 + "transition:generic".into(), 197 + ]); 198 + 199 + // Has both scopes 200 + let valid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto", "transition:generic"]); 201 + assert!(rules.validate(&valid)); 202 + 203 + // Missing one scope 204 + let invalid = test_session("did:plc:123", "alice.bsky.social", vec!["atproto"]); 205 + assert!(!rules.validate(&invalid)); 206 + } 207 + 208 + // ======================================================================== 209 + // Combined Rules Tests (Identity + Scope) 210 + // ======================================================================== 211 + 212 + #[test] 213 + fn test_handle_ends_with_and_scope() { 214 + let rules = AuthRules::All(vec![ 215 + AuthRules::HandleEndsWith(".blacksky.team".into()), 216 + AuthRules::ScopeEquals("transition:generic".into()), 217 + ]); 218 + 219 + // Both conditions met 220 + let valid = test_session( 221 + "did:plc:123", 222 + "alice.blacksky.team", 223 + vec!["atproto", "transition:generic"], 224 + ); 225 + assert!(rules.validate(&valid)); 226 + 227 + // Handle correct, scope wrong 228 + let invalid1 = test_session("did:plc:123", "alice.blacksky.team", vec!["atproto"]); 229 + assert!(!rules.validate(&invalid1)); 230 + 231 + // Scope correct, handle wrong 232 + let invalid2 = test_session( 233 + "did:plc:123", 234 + "alice.bsky.social", 235 + vec!["atproto", "transition:generic"], 236 + ); 237 + assert!(!rules.validate(&invalid2)); 238 + } 239 + 240 + #[test] 241 + fn test_did_with_scope() { 242 + let rules = AuthRules::All(vec![ 243 + AuthRules::DidEquals("did:plc:rnpkyqnmsw4ipey6eotbdnnf".into()), 244 + AuthRules::ScopeEquals("transition:generic".into()), 245 + ]); 246 + 247 + // Both conditions met 248 + let valid = test_session( 249 + "did:plc:rnpkyqnmsw4ipey6eotbdnnf", 250 + "admin.bsky.social", 251 + vec!["atproto", "transition:generic"], 252 + ); 253 + assert!(rules.validate(&valid)); 254 + 255 + // DID correct, scope wrong 256 + let invalid1 = test_session( 257 + "did:plc:rnpkyqnmsw4ipey6eotbdnnf", 258 + "admin.bsky.social", 259 + vec!["atproto"], 260 + ); 261 + assert!(!rules.validate(&invalid1)); 262 + 263 + // Scope correct, DID wrong 264 + let invalid2 = test_session( 265 + "did:plc:wrongdid", 266 + "admin.bsky.social", 267 + vec!["atproto", "transition:generic"], 268 + ); 269 + assert!(!rules.validate(&invalid2)); 270 + } 271 + 272 + #[test] 273 + fn test_complex_admin_or_moderator_rule() { 274 + // Admin DID OR (moderator handle + scope) 275 + let rules = AuthRules::Any(vec![ 276 + AuthRules::DidEquals("did:plc:rnpkyqnmsw4ipey6eotbdnnf".into()), 277 + AuthRules::All(vec![ 278 + AuthRules::HandleEndsWith(".mod.team".into()), 279 + AuthRules::ScopeEquals("account:email".into()), 280 + ]), 281 + ]); 282 + 283 + // Admin DID (doesn't need scope) 284 + let admin = test_session("did:plc:rnpkyqnmsw4ipey6eotbdnnf", "admin.bsky.social", vec!["atproto"]); 285 + assert!(rules.validate(&admin)); 286 + 287 + // Moderator with correct handle and scope 288 + let mod_valid = test_session( 289 + "did:plc:somemod", 290 + "alice.mod.team", 291 + vec!["atproto", "account:email"], 292 + ); 293 + assert!(rules.validate(&mod_valid)); 294 + 295 + // Moderator handle but missing scope 296 + let mod_no_scope = test_session("did:plc:somemod", "alice.mod.team", vec!["atproto"]); 297 + assert!(!rules.validate(&mod_no_scope)); 298 + 299 + // Has scope but not moderator handle 300 + let not_mod = test_session( 301 + "did:plc:someuser", 302 + "alice.bsky.social", 303 + vec!["atproto", "account:email"], 304 + ); 305 + assert!(!rules.validate(&not_mod)); 306 + } 307 + 308 + // ======================================================================== 309 + // Additional Coverage Tests 310 + // ======================================================================== 311 + 312 + #[test] 313 + fn test_did_equals_any() { 314 + let rules = AuthRules::DidEqualsAny(vec![ 315 + "did:plc:alice123456789012345678".into(), 316 + "did:plc:bob12345678901234567890".into(), 317 + "did:plc:charlie12345678901234567".into(), 318 + ]); 319 + 320 + // First DID matches 321 + let valid1 = test_session("did:plc:alice123456789012345678", "alice.bsky.social", vec!["atproto"]); 322 + assert!(rules.validate(&valid1)); 323 + 324 + // Second DID matches 325 + let valid2 = test_session("did:plc:bob12345678901234567890", "bob.bsky.social", vec!["atproto"]); 326 + assert!(rules.validate(&valid2)); 327 + 328 + // Third DID matches 329 + let valid3 = test_session("did:plc:charlie12345678901234567", "charlie.bsky.social", vec!["atproto"]); 330 + assert!(rules.validate(&valid3)); 331 + 332 + // DID not in list 333 + let invalid = test_session("did:plc:unknown1234567890123456", "unknown.bsky.social", vec!["atproto"]); 334 + assert!(!rules.validate(&invalid)); 335 + 336 + // Partial match should fail (prefix) 337 + let partial = test_session("did:plc:alice", "alice.bsky.social", vec!["atproto"]); 338 + assert!(!rules.validate(&partial)); 339 + } 340 + 341 + // ======================================================================== 342 + // Empty Combinator Edge Cases 343 + // ======================================================================== 344 + 345 + #[test] 346 + fn test_empty_any_returns_false() { 347 + // Any with empty rules should return false (no rule can be satisfied) 348 + let rules = AuthRules::Any(vec![]); 349 + let session = test_session("did:plc:test12345678901234567", "test.bsky.social", vec!["atproto"]); 350 + assert!(!rules.validate(&session)); 351 + } 352 + 353 + #[test] 354 + fn test_empty_all_returns_true() { 355 + // All with empty rules should return true (vacuous truth - all zero rules are satisfied) 356 + let rules = AuthRules::All(vec![]); 357 + let session = test_session("did:plc:test12345678901234567", "test.bsky.social", vec!["atproto"]); 358 + assert!(rules.validate(&session)); 359 + } 360 + 361 + // ======================================================================== 362 + // Handle Edge Cases 363 + // ======================================================================== 364 + 365 + #[test] 366 + fn test_handle_exact_suffix_match() { 367 + // Handle that IS exactly the suffix should still match 368 + let rules = AuthRules::HandleEndsWith(".blacksky.team".into()); 369 + 370 + // Handle is exactly the suffix 371 + let exact = test_session("did:plc:test12345678901234567", ".blacksky.team", vec!["atproto"]); 372 + assert!(rules.validate(&exact)); 373 + 374 + // Normal case - handle with prefix 375 + let normal = test_session("did:plc:test12345678901234567", "alice.blacksky.team", vec!["atproto"]); 376 + assert!(rules.validate(&normal)); 377 + 378 + // Empty handle should not match 379 + let empty = test_session("did:plc:test12345678901234567", "", vec!["atproto"]); 380 + assert!(!rules.validate(&empty)); 381 + } 382 + 383 + // ======================================================================== 384 + // Deeply Nested Combinators 385 + // ======================================================================== 386 + 387 + #[test] 388 + fn test_deeply_nested_rules() { 389 + // Complex nested rule: Any(All(Any(...), ...), All(...)) 390 + // Scenario: (Admin OR VIP) AND (has scope OR team member) 391 + // OR 392 + // Specific moderator DID 393 + let rules = AuthRules::Any(vec![ 394 + // Branch 1: Complex nested condition 395 + AuthRules::All(vec![ 396 + // Must be admin or VIP 397 + AuthRules::Any(vec![ 398 + AuthRules::DidEquals("did:plc:admin123456789012345".into()), 399 + AuthRules::HandleEndsWith(".vip.social".into()), 400 + ]), 401 + // AND must have scope or be team member 402 + AuthRules::Any(vec![ 403 + AuthRules::ScopeEquals("transition:generic".into()), 404 + AuthRules::HandleEndsWith(".team.internal".into()), 405 + ]), 406 + ]), 407 + // Branch 2: Specific moderator bypass 408 + AuthRules::DidEquals("did:plc:moderator12345678901".into()), 409 + ]); 410 + 411 + // Admin with required scope - should pass via Branch 1 412 + let admin_with_scope = test_session( 413 + "did:plc:admin123456789012345", 414 + "admin.bsky.social", 415 + vec!["atproto", "transition:generic"], 416 + ); 417 + assert!(rules.validate(&admin_with_scope)); 418 + 419 + // VIP with required scope - should pass via Branch 1 420 + let vip_with_scope = test_session( 421 + "did:plc:somevip1234567890123", 422 + "alice.vip.social", 423 + vec!["atproto", "transition:generic"], 424 + ); 425 + assert!(rules.validate(&vip_with_scope)); 426 + 427 + // Moderator bypass - should pass via Branch 2 428 + let moderator = test_session( 429 + "did:plc:moderator12345678901", 430 + "mod.bsky.social", 431 + vec!["atproto"], 432 + ); 433 + assert!(rules.validate(&moderator)); 434 + 435 + // Admin without scope and not team member - should fail 436 + let admin_no_scope = test_session( 437 + "did:plc:admin123456789012345", 438 + "admin.bsky.social", 439 + vec!["atproto"], 440 + ); 441 + assert!(!rules.validate(&admin_no_scope)); 442 + 443 + // Random user - should fail 444 + let random = test_session( 445 + "did:plc:random12345678901234", 446 + "random.bsky.social", 447 + vec!["atproto", "transition:generic"], 448 + ); 449 + assert!(!rules.validate(&random)); 450 + } 451 + 452 + // ======================================================================== 453 + // ATProto Scope Specification Tests 454 + // ======================================================================== 455 + 456 + #[test] 457 + fn test_scope_with_query_params() { 458 + // ATProto scopes can have query parameters 459 + // These are treated as literal strings (exact matching) 460 + 461 + // blob scope with accept parameter 462 + let blob_rules = AuthRules::ScopeEquals("blob?accept=image/*".into()); 463 + let has_blob = test_session( 464 + "did:plc:test12345678901234567", 465 + "test.bsky.social", 466 + vec!["atproto", "blob?accept=image/*"], 467 + ); 468 + assert!(blob_rules.validate(&has_blob)); 469 + 470 + // Different query param should not match 471 + let wrong_blob = test_session( 472 + "did:plc:test12345678901234567", 473 + "test.bsky.social", 474 + vec!["atproto", "blob?accept=video/*"], 475 + ); 476 + assert!(!blob_rules.validate(&wrong_blob)); 477 + 478 + // account:repo with action parameter 479 + let account_rules = AuthRules::ScopeEquals("account:repo?action=manage".into()); 480 + let has_account = test_session( 481 + "did:plc:test12345678901234567", 482 + "test.bsky.social", 483 + vec!["atproto", "account:repo?action=manage"], 484 + ); 485 + assert!(account_rules.validate(&has_account)); 486 + 487 + // Multiple query params 488 + let multi_param_rules = AuthRules::ScopeEquals("blob?accept=image/*&accept=video/*".into()); 489 + let has_multi = test_session( 490 + "did:plc:test12345678901234567", 491 + "test.bsky.social", 492 + vec!["atproto", "blob?accept=image/*&accept=video/*"], 493 + ); 494 + assert!(multi_param_rules.validate(&has_multi)); 495 + } 496 + 497 + #[test] 498 + fn test_scope_exact_matching_no_wildcards() { 499 + // ATProto spec: Wildcards do NOT work for collections 500 + // repo:app.bsky.feed.post should NOT match repo:app.bsky.feed.* 501 + // This test verifies our exact matching is correct 502 + 503 + let rules = AuthRules::ScopeEquals("repo:app.bsky.feed.post".into()); 504 + 505 + // Exact match works 506 + let exact = test_session( 507 + "did:plc:test12345678901234567", 508 + "test.bsky.social", 509 + vec!["atproto", "repo:app.bsky.feed.post"], 510 + ); 511 + assert!(rules.validate(&exact)); 512 + 513 + // Wildcard scope in JWT should NOT satisfy specific requirement 514 + // (user has repo:app.bsky.feed.* but endpoint requires repo:app.bsky.feed.post) 515 + let wildcard = test_session( 516 + "did:plc:test12345678901234567", 517 + "test.bsky.social", 518 + vec!["atproto", "repo:app.bsky.feed.*"], 519 + ); 520 + assert!(!rules.validate(&wildcard)); 521 + 522 + // Different collection should not match 523 + let different = test_session( 524 + "did:plc:test12345678901234567", 525 + "test.bsky.social", 526 + vec!["atproto", "repo:app.bsky.feed.like"], 527 + ); 528 + assert!(!rules.validate(&different)); 529 + 530 + // Prefix should not match 531 + let prefix = test_session( 532 + "did:plc:test12345678901234567", 533 + "test.bsky.social", 534 + vec!["atproto", "repo:app.bsky.feed"], 535 + ); 536 + assert!(!rules.validate(&prefix)); 537 + 538 + // repo:* should not match specific collection (exact matching) 539 + let full_wildcard = test_session( 540 + "did:plc:test12345678901234567", 541 + "test.bsky.social", 542 + vec!["atproto", "repo:*"], 543 + ); 544 + assert!(!rules.validate(&full_wildcard)); 545 + } 546 + 547 + #[test] 548 + fn test_scope_case_sensitivity() { 549 + // OAuth scopes are case-sensitive per RFC 6749 550 + 551 + let rules = AuthRules::ScopeEquals("atproto".into()); 552 + 553 + // Exact case matches 554 + let exact = test_session("did:plc:test12345678901234567", "test.bsky.social", vec!["atproto"]); 555 + assert!(rules.validate(&exact)); 556 + 557 + // UPPERCASE should NOT match 558 + let upper = test_session("did:plc:test12345678901234567", "test.bsky.social", vec!["ATPROTO"]); 559 + assert!(!rules.validate(&upper)); 560 + 561 + // Mixed case should NOT match 562 + let mixed = test_session("did:plc:test12345678901234567", "test.bsky.social", vec!["AtProto"]); 563 + assert!(!rules.validate(&mixed)); 564 + 565 + // Test with namespaced scope 566 + let ns_rules = AuthRules::ScopeEquals("transition:generic".into()); 567 + let ns_upper = test_session( 568 + "did:plc:test12345678901234567", 569 + "test.bsky.social", 570 + vec!["TRANSITION:GENERIC"], 571 + ); 572 + assert!(!ns_rules.validate(&ns_upper)); 573 + } 574 + 575 + #[test] 576 + fn test_scope_with_wildcards_exact_match() { 577 + // Wildcard scopes like blob:*/* and identity:* are stored as literal strings 578 + // The middleware does exact matching, so JWT must contain the exact string 579 + 580 + // blob:*/* - full blob wildcard 581 + let blob_rules = AuthRules::ScopeEquals("blob:*/*".into()); 582 + let has_blob_wildcard = test_session( 583 + "did:plc:test12345678901234567", 584 + "test.bsky.social", 585 + vec!["atproto", "blob:*/*"], 586 + ); 587 + assert!(blob_rules.validate(&has_blob_wildcard)); 588 + 589 + // Specific blob type should NOT match blob:*/* requirement 590 + let has_specific_blob = test_session( 591 + "did:plc:test12345678901234567", 592 + "test.bsky.social", 593 + vec!["atproto", "blob:image/png"], 594 + ); 595 + assert!(!blob_rules.validate(&has_specific_blob)); 596 + 597 + // identity:* - full identity wildcard 598 + let identity_rules = AuthRules::ScopeEquals("identity:*".into()); 599 + let has_identity_wildcard = test_session( 600 + "did:plc:test12345678901234567", 601 + "test.bsky.social", 602 + vec!["atproto", "identity:*"], 603 + ); 604 + assert!(identity_rules.validate(&has_identity_wildcard)); 605 + 606 + // Specific identity scope should NOT match identity:* requirement 607 + let has_specific_identity = test_session( 608 + "did:plc:test12345678901234567", 609 + "test.bsky.social", 610 + vec!["atproto", "identity:handle"], 611 + ); 612 + assert!(!identity_rules.validate(&has_specific_identity)); 613 + } 614 + 615 + // ======================================================================== 616 + // Scope Helper Function Tests 617 + // ======================================================================== 618 + 619 + #[test] 620 + fn test_has_scope_helper() { 621 + let scopes: Vec<String> = vec![ 622 + "atproto".to_string(), 623 + "transition:generic".to_string(), 624 + "repo:app.bsky.feed.post".to_string(), 625 + ]; 626 + 627 + // Present scopes 628 + assert!(has_scope(&scopes, "atproto")); 629 + assert!(has_scope(&scopes, "transition:generic")); 630 + assert!(has_scope(&scopes, "repo:app.bsky.feed.post")); 631 + 632 + // Absent scopes 633 + assert!(!has_scope(&scopes, "identity:*")); 634 + assert!(!has_scope(&scopes, "")); 635 + assert!(!has_scope(&scopes, "ATPROTO")); // Case sensitive 636 + 637 + // Empty scopes list 638 + let empty: Vec<String> = vec![]; 639 + assert!(!has_scope(&empty, "atproto")); 640 + } 641 + 642 + #[test] 643 + fn test_has_any_scope_helper() { 644 + let scopes: Vec<String> = vec![ 645 + "atproto".to_string(), 646 + "repo:app.bsky.feed.post".to_string(), 647 + ]; 648 + 649 + // Has one of the required scopes 650 + let required1 = vec!["transition:generic".to_string(), "atproto".to_string()]; 651 + assert!(has_any_scope(&scopes, &required1)); 652 + 653 + // Has none of the required scopes 654 + let required2 = vec!["transition:generic".to_string(), "identity:*".to_string()]; 655 + assert!(!has_any_scope(&scopes, &required2)); 656 + 657 + // Empty required list - should return false (no scope to match) 658 + let empty_required: Vec<String> = vec![]; 659 + assert!(!has_any_scope(&scopes, &empty_required)); 660 + 661 + // Empty scopes list 662 + let empty_scopes: Vec<String> = vec![]; 663 + assert!(!has_any_scope(&empty_scopes, &required1)); 664 + } 665 + 666 + #[test] 667 + fn test_has_all_scopes_helper() { 668 + let scopes: Vec<String> = vec![ 669 + "atproto".to_string(), 670 + "transition:generic".to_string(), 671 + "repo:app.bsky.feed.post".to_string(), 672 + ]; 673 + 674 + // Has all required scopes 675 + let required1 = vec!["atproto".to_string(), "transition:generic".to_string()]; 676 + assert!(has_all_scopes(&scopes, &required1)); 677 + 678 + // Missing one required scope 679 + let required2 = vec!["atproto".to_string(), "identity:*".to_string()]; 680 + assert!(!has_all_scopes(&scopes, &required2)); 681 + 682 + // Empty required list - should return true (vacuously all zero required are present) 683 + let empty_required: Vec<String> = vec![]; 684 + assert!(has_all_scopes(&scopes, &empty_required)); 685 + 686 + // Empty scopes list with requirements 687 + let empty_scopes: Vec<String> = vec![]; 688 + assert!(!has_all_scopes(&empty_scopes, &required1)); 689 + 690 + // Single scope requirement 691 + let single = vec!["atproto".to_string()]; 692 + assert!(has_all_scopes(&scopes, &single)); 693 + } 694 + }
+247
src/gate.rs
··· 1 + use crate::AppState; 2 + use crate::helpers::{generate_gate_token, json_error_response}; 3 + use axum::Form; 4 + use axum::extract::{Query, State}; 5 + use axum::http::StatusCode; 6 + use axum::response::{IntoResponse, Redirect, Response}; 7 + use axum_template::RenderHtml; 8 + use chrono::{DateTime, Utc}; 9 + use serde::{Deserialize, Serialize}; 10 + use std::env; 11 + use tracing::log; 12 + 13 + #[derive(Deserialize)] 14 + pub struct GateQuery { 15 + handle: String, 16 + state: String, 17 + #[serde(default)] 18 + error: Option<String>, 19 + #[serde(default)] 20 + redirect_url: Option<String>, 21 + } 22 + 23 + #[derive(Deserialize, Serialize)] 24 + pub struct CaptchaPage { 25 + handle: String, 26 + state: String, 27 + captcha_site_key: String, 28 + error_message: Option<String>, 29 + pds: String, 30 + redirect_url: Option<String>, 31 + } 32 + 33 + #[derive(Deserialize)] 34 + pub struct CaptchaForm { 35 + #[serde(rename = "h-captcha-response")] 36 + h_captcha_response: String, 37 + #[serde(default)] 38 + redirect_url: Option<String>, 39 + } 40 + 41 + /// GET /gate - Display the captcha page 42 + pub async fn get_gate( 43 + Query(params): Query<GateQuery>, 44 + State(state): State<AppState>, 45 + ) -> impl IntoResponse { 46 + let hcaptcha_site_key = match env::var("PDS_HCAPTCHA_SITE_KEY") { 47 + Ok(key) => key, 48 + Err(_) => { 49 + return json_error_response( 50 + StatusCode::INTERNAL_SERVER_ERROR, 51 + "ServerError", 52 + "hCaptcha is not configured", 53 + ) 54 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 55 + } 56 + }; 57 + 58 + let error_message = match params.error { 59 + None => None, 60 + Some(error) => Some(html_escape::encode_safe(&error).to_string()), 61 + }; 62 + 63 + RenderHtml( 64 + "captcha.hbs", 65 + state.template_engine, 66 + CaptchaPage { 67 + handle: params.handle, 68 + state: params.state, 69 + captcha_site_key: hcaptcha_site_key, 70 + error_message, 71 + pds: state.app_config.pds_service_did.replace("did:web:", ""), 72 + redirect_url: params.redirect_url, 73 + }, 74 + ) 75 + .into_response() 76 + } 77 + 78 + /// POST /gate - Verify captcha and redirect 79 + pub async fn post_gate( 80 + State(state): State<AppState>, 81 + Query(params): Query<GateQuery>, 82 + Form(form): Form<CaptchaForm>, 83 + ) -> Response { 84 + // Verify hCaptcha response 85 + let hcaptcha_secret = match env::var("PDS_HCAPTCHA_SECRET_KEY") { 86 + Ok(secret) => secret, 87 + Err(_) => { 88 + return json_error_response( 89 + StatusCode::INTERNAL_SERVER_ERROR, 90 + "ServerError", 91 + "hCaptcha is not configured", 92 + ) 93 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 94 + } 95 + }; 96 + 97 + let client = match reqwest::Client::builder() 98 + .timeout(std::time::Duration::from_secs(10)) 99 + .build() 100 + { 101 + Ok(c) => c, 102 + Err(e) => { 103 + log::error!("Failed to create HTTP client: {}", e); 104 + return json_error_response( 105 + StatusCode::INTERNAL_SERVER_ERROR, 106 + "ServerError", 107 + "Failed to verify captcha", 108 + ) 109 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 110 + } 111 + }; 112 + 113 + #[derive(Deserialize, Serialize)] 114 + struct HCaptchaResponse { 115 + success: bool, 116 + challenge_ts: DateTime<Utc>, 117 + hostname: String, 118 + #[serde(rename = "error-codes", default)] 119 + error_codes: Vec<String>, 120 + } 121 + 122 + let verification_result = client 123 + .post("https://api.hcaptcha.com/siteverify") 124 + .form(&[ 125 + ("secret", hcaptcha_secret.as_str()), 126 + ("response", form.h_captcha_response.as_str()), 127 + ]) 128 + .send() 129 + .await; 130 + 131 + let verification_response = match verification_result { 132 + Ok(resp) => resp, 133 + Err(e) => { 134 + log::error!("Failed to verify hCaptcha: {}", e); 135 + 136 + return Redirect::to(&format!( 137 + "/gate?handle={}&state={}&error={}", 138 + url_encode(&params.handle), 139 + url_encode(&params.state), 140 + url_encode("Verification failed. Please try again.") 141 + )) 142 + .into_response(); 143 + } 144 + }; 145 + 146 + let captcha_result: HCaptchaResponse = match verification_response.json().await { 147 + Ok(result) => result, 148 + Err(e) => { 149 + log::error!("Failed to parse hCaptcha response: {}", e); 150 + 151 + return Redirect::to(&format!( 152 + "/gate?handle={}&state={}&error={}", 153 + url_encode(&params.handle), 154 + url_encode(&params.state), 155 + url_encode("Verification failed. Please try again.") 156 + )) 157 + .into_response(); 158 + } 159 + }; 160 + 161 + if !captcha_result.success { 162 + log::warn!( 163 + "hCaptcha verification failed for handle {}: {:?}", 164 + params.handle, 165 + captcha_result.error_codes 166 + ); 167 + return Redirect::to(&format!( 168 + "/gate?handle={}&state={}&error={}", 169 + url_encode(&params.handle), 170 + url_encode(&params.state), 171 + url_encode("Verification failed. Please try again.") 172 + )) 173 + .into_response(); 174 + } 175 + 176 + // Generate secure JWE verification token 177 + let code = match generate_gate_token(&params.handle, &state.app_config.gate_jwe_key) { 178 + Ok(token) => token, 179 + Err(e) => { 180 + log::error!("Failed to generate gate token: {}", e); 181 + return json_error_response( 182 + StatusCode::INTERNAL_SERVER_ERROR, 183 + "ServerError", 184 + "Failed to create verification code", 185 + ) 186 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 187 + } 188 + }; 189 + 190 + let now = Utc::now(); 191 + 192 + // Store the encrypted token in the database 193 + let result = sqlx::query( 194 + "INSERT INTO gate_codes (code, handle, created_at) 195 + VALUES (?, ?, ?)", 196 + ) 197 + .bind(&code) 198 + .bind(&params.handle) 199 + .bind(now) 200 + .execute(&state.pds_gatekeeper_pool) 201 + .await; 202 + 203 + if let Err(e) = result { 204 + log::error!("Failed to store gate code: {}", e); 205 + return json_error_response( 206 + StatusCode::INTERNAL_SERVER_ERROR, 207 + "ServerError", 208 + "Failed to create verification code", 209 + ) 210 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 211 + } 212 + 213 + // Redirects by origin if it's found. If not redirect to the configured URL. 214 + let mut base_redirect = state.app_config.default_successful_redirect_url.clone(); 215 + if let Some(ref redirect_url) = form.redirect_url { 216 + let trimmed = redirect_url.trim(); 217 + if !trimmed.is_empty() 218 + && (trimmed.starts_with("https://") || trimmed.starts_with("http://")) 219 + { 220 + base_redirect = trimmed.trim_end_matches('/').to_string(); 221 + } 222 + } 223 + 224 + let base_redirect = match state 225 + .app_config 226 + .captcha_success_redirects 227 + .contains(&base_redirect) 228 + { 229 + true => base_redirect, 230 + false => state.app_config.default_successful_redirect_url.clone(), 231 + }; 232 + 233 + // Redirect to client app with code and state 234 + let redirect_url = format!( 235 + "{}/?code={}&state={}", 236 + base_redirect, 237 + url_encode(&code), 238 + url_encode(&params.state) 239 + ); 240 + 241 + Redirect::to(&redirect_url).into_response() 242 + } 243 + 244 + /// Simple URL encode function 245 + fn url_encode(s: &str) -> String { 246 + urlencoding::encode(s).to_string() 247 + }
+177 -14
src/helpers.rs
··· 1 1 use crate::AppState; 2 2 use crate::helpers::TokenCheckError::InvalidToken; 3 3 use anyhow::anyhow; 4 - use axum::body::{Body, to_bytes}; 5 - use axum::extract::Request; 6 - use axum::http::header::CONTENT_TYPE; 7 - use axum::http::{HeaderMap, StatusCode, Uri}; 8 - use axum::response::{IntoResponse, Response}; 4 + use axum::{ 5 + body::{Body, to_bytes}, 6 + extract::Request, 7 + http::header::CONTENT_TYPE, 8 + http::{HeaderMap, StatusCode, Uri}, 9 + response::{IntoResponse, Response}, 10 + }; 9 11 use axum_template::TemplateEngine; 10 12 use chrono::Utc; 11 - use lettre::message::{MultiPart, SinglePart, header}; 12 - use lettre::{AsyncTransport, Message}; 13 + use jacquard_common::{ 14 + service_auth, service_auth::PublicKey, types::did::Did, types::did_doc::VerificationMethod, 15 + types::nsid::Nsid, 16 + }; 17 + use jacquard_identity::{PublicResolver, resolver::IdentityResolver}; 18 + use josekit::jwe::alg::direct::DirectJweAlgorithm; 19 + use lettre::{ 20 + AsyncTransport, Message, 21 + message::{MultiPart, SinglePart, header}, 22 + }; 13 23 use rand::Rng; 14 24 use serde::de::DeserializeOwned; 15 25 use serde_json::{Map, Value}; 16 26 use sha2::{Digest, Sha256}; 17 27 use sqlx::SqlitePool; 28 + use std::sync::Arc; 18 29 use tracing::{error, log}; 19 30 20 31 ///Used to generate the email 2fa code ··· 39 50 where 40 51 T: DeserializeOwned, 41 52 { 42 - let uri = format!("{}{}", state.pds_base_url, path); 53 + let uri = format!("{}{}", state.app_config.pds_base_url, path); 43 54 *req.uri_mut() = Uri::try_from(uri).map_err(|_| StatusCode::BAD_REQUEST)?; 44 55 45 56 let result = state ··· 134 145 full_code.push(UPPERCASE_BASE32_CHARS[idx] as char); 135 146 } 136 147 137 - //The PDS implementation creates in lowercase, then converts to uppercase. 138 - //Just going a head and doing uppercase here. 139 - let slice_one = &full_code[0..5].to_ascii_uppercase(); 140 - let slice_two = &full_code[5..10].to_ascii_uppercase(); 148 + let slice_one = &full_code[0..5]; 149 + let slice_two = &full_code[5..10]; 141 150 format!("{slice_one}-{slice_two}") 142 151 } 143 152 ··· 337 346 338 347 let email_message = Message::builder() 339 348 //TODO prob get the proper type in the state 340 - .from(state.mailer_from.parse()?) 349 + .from(state.app_config.mailer_from.parse()?) 341 350 .to(email.parse()?) 342 - .subject("Sign in to Bluesky") 351 + .subject(&state.app_config.email_subject) 343 352 .multipart( 344 353 MultiPart::alternative() // This is composed of two parts. 345 354 .singlepart( ··· 522 531 523 532 format!("{masked_local}@{masked_domain}") 524 533 } 534 + 535 + pub enum VerifyServiceAuthError { 536 + AuthFailed, 537 + Error(anyhow::Error), 538 + } 539 + 540 + /// Verifies the service auth token that is appended to an XRPC proxy request 541 + pub async fn verify_service_auth( 542 + jwt: &str, 543 + lxm: &Nsid<'static>, 544 + public_resolver: Arc<PublicResolver>, 545 + service_did: &Did<'static>, 546 + //The did of the user wanting to create an account 547 + requested_did: &Did<'static>, 548 + ) -> Result<(), VerifyServiceAuthError> { 549 + let parsed = 550 + service_auth::parse_jwt(jwt).map_err(|e| VerifyServiceAuthError::Error(e.into()))?; 551 + 552 + let claims = parsed.claims(); 553 + 554 + let did_doc = public_resolver 555 + .resolve_did_doc(&requested_did) 556 + .await 557 + .map_err(|err| { 558 + log::error!("Error resolving the service auth for: {}", claims.iss); 559 + return VerifyServiceAuthError::Error(err.into()); 560 + })?; 561 + 562 + // Parse the DID document response to get verification methods 563 + let doc = did_doc.parse().map_err(|err| { 564 + log::error!("Error parsing the service auth did doc: {}", claims.iss); 565 + VerifyServiceAuthError::Error(anyhow::anyhow!(err)) 566 + })?; 567 + 568 + let verification_methods = doc.verification_method.as_deref().ok_or_else(|| { 569 + VerifyServiceAuthError::Error(anyhow::anyhow!( 570 + "No verification methods in did doc: {}", 571 + &claims.iss 572 + )) 573 + })?; 574 + 575 + let signing_key = extract_signing_key(verification_methods).ok_or_else(|| { 576 + VerifyServiceAuthError::Error(anyhow::anyhow!( 577 + "No signing key found in did doc: {}", 578 + &claims.iss 579 + )) 580 + })?; 581 + 582 + service_auth::verify_signature(&parsed, &signing_key).map_err(|err| { 583 + log::error!("Error verifying service auth signature: {}", err); 584 + VerifyServiceAuthError::AuthFailed 585 + })?; 586 + 587 + // Now validate claims (audience, expiration, etc.) 588 + claims.validate(service_did).map_err(|e| { 589 + log::error!("Error validating service auth claims: {}", e); 590 + VerifyServiceAuthError::AuthFailed 591 + })?; 592 + 593 + if claims.aud != *service_did { 594 + log::error!("Invalid audience (did:web): {}", claims.aud); 595 + return Err(VerifyServiceAuthError::AuthFailed); 596 + } 597 + 598 + let lxm_from_claims = claims.lxm.as_ref().ok_or_else(|| { 599 + VerifyServiceAuthError::Error(anyhow::anyhow!("No lxm claim in service auth JWT")) 600 + })?; 601 + 602 + if lxm_from_claims != lxm { 603 + return Err(VerifyServiceAuthError::Error(anyhow::anyhow!( 604 + "Invalid XRPC endpoint requested" 605 + ))); 606 + } 607 + Ok(()) 608 + } 609 + 610 + /// Ripped from Jacquard 611 + /// 612 + /// Extract the signing key from a DID document's verification methods. 613 + /// 614 + /// This looks for a key with type "atproto" or the first available key 615 + /// if no atproto-specific key is found. 616 + fn extract_signing_key(methods: &[VerificationMethod]) -> Option<PublicKey> { 617 + // First try to find an atproto-specific key 618 + let atproto_method = methods 619 + .iter() 620 + .find(|m| m.r#type.as_ref() == "Multikey" || m.r#type.as_ref() == "atproto"); 621 + 622 + let method = atproto_method.or_else(|| methods.first())?; 623 + 624 + // Parse the multikey 625 + let public_key_multibase = method.public_key_multibase.as_ref()?; 626 + 627 + // Decode multibase 628 + let (_, key_bytes) = multibase::decode(public_key_multibase.as_ref()).ok()?; 629 + 630 + // First two bytes are the multicodec prefix 631 + if key_bytes.len() < 2 { 632 + return None; 633 + } 634 + 635 + let codec = &key_bytes[..2]; 636 + let key_material = &key_bytes[2..]; 637 + 638 + match codec { 639 + // p256-pub (0x1200) 640 + [0x80, 0x24] => PublicKey::from_p256_bytes(key_material).ok(), 641 + // secp256k1-pub (0xe7) 642 + [0xe7, 0x01] => PublicKey::from_k256_bytes(key_material).ok(), 643 + _ => None, 644 + } 645 + } 646 + 647 + /// Payload for gate JWE tokens 648 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 649 + pub struct GateTokenPayload { 650 + pub handle: String, 651 + pub created_at: String, 652 + } 653 + 654 + /// Generate a secure JWE token for gate verification 655 + pub fn generate_gate_token(handle: &str, encryption_key: &[u8]) -> Result<String, anyhow::Error> { 656 + use josekit::jwe::{JweHeader, alg::direct::DirectJweAlgorithm}; 657 + 658 + let payload = GateTokenPayload { 659 + handle: handle.to_string(), 660 + created_at: Utc::now().to_rfc3339(), 661 + }; 662 + 663 + let payload_json = serde_json::to_string(&payload)?; 664 + 665 + let mut header = JweHeader::new(); 666 + header.set_token_type("JWT"); 667 + header.set_content_encryption("A128CBC-HS256"); 668 + 669 + let encrypter = DirectJweAlgorithm::Dir.encrypter_from_bytes(encryption_key)?; 670 + 671 + // Encrypt 672 + let jwe = josekit::jwe::serialize_compact(payload_json.as_bytes(), &header, &encrypter)?; 673 + 674 + Ok(jwe) 675 + } 676 + 677 + /// Verify and decrypt a gate JWE token, returning the payload if valid 678 + pub fn verify_gate_token( 679 + token: &str, 680 + encryption_key: &[u8], 681 + ) -> Result<GateTokenPayload, anyhow::Error> { 682 + let decrypter = DirectJweAlgorithm::Dir.decrypter_from_bytes(encryption_key)?; 683 + let (payload_bytes, _header) = josekit::jwe::deserialize_compact(token, &decrypter)?; 684 + let payload: GateTokenPayload = serde_json::from_slice(&payload_bytes)?; 685 + 686 + Ok(payload) 687 + }
+214 -35
src/main.rs
··· 1 1 #![warn(clippy::unwrap_used)] 2 + use crate::gate::{get_gate, post_gate}; 2 3 use crate::oauth_provider::sign_in; 3 - use crate::xrpc::com_atproto_server::{create_session, get_session, update_email}; 4 - use axum::body::Body; 5 - use axum::handler::Handler; 6 - use axum::http::{Method, header}; 7 - use axum::middleware as ax_middleware; 8 - use axum::routing::post; 9 - use axum::{Router, routing::get}; 4 + use crate::xrpc::com_atproto_server::{ 5 + create_account, create_session, describe_server, get_session, update_email, 6 + }; 7 + use axum::{ 8 + Router, 9 + body::Body, 10 + handler::Handler, 11 + http::{Method, header}, 12 + middleware as ax_middleware, 13 + routing::get, 14 + routing::post, 15 + }; 10 16 use axum_template::engine::Engine; 11 17 use handlebars::Handlebars; 12 - use hyper_util::client::legacy::connect::HttpConnector; 13 - use hyper_util::rt::TokioExecutor; 18 + use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 19 + use jacquard_common::types::did::Did; 20 + use jacquard_identity::{PublicResolver, resolver::PlcSource}; 14 21 use lettre::{AsyncSmtpTransport, Tokio1Executor}; 22 + use rand::Rng; 15 23 use rust_embed::RustEmbed; 16 24 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; 17 25 use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; 18 26 use std::path::Path; 27 + use std::sync::Arc; 19 28 use std::time::Duration; 20 29 use std::{env, net::SocketAddr}; 21 - use tower_governor::GovernorLayer; 22 - use tower_governor::governor::GovernorConfigBuilder; 23 - use tower_http::compression::CompressionLayer; 24 - use tower_http::cors::{Any, CorsLayer}; 30 + use tower_governor::{ 31 + GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, 32 + }; 33 + use tower_http::{ 34 + compression::CompressionLayer, 35 + cors::{Any, CorsLayer}, 36 + }; 25 37 use tracing::log; 26 38 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 27 39 40 + mod auth; 41 + mod gate; 28 42 pub mod helpers; 29 43 mod middleware; 30 44 mod oauth_provider; ··· 37 51 #[include = "*.hbs"] 38 52 struct EmailTemplates; 39 53 54 + #[derive(RustEmbed)] 55 + #[folder = "html_templates"] 56 + #[include = "*.hbs"] 57 + struct HtmlTemplates; 58 + 59 + /// Mostly the env variables that are used in the app 60 + #[derive(Clone, Debug)] 61 + pub struct AppConfig { 62 + pds_base_url: String, 63 + mailer_from: String, 64 + email_subject: String, 65 + allow_only_migrations: bool, 66 + use_captcha: bool, 67 + //The url to redirect to after a successful captcha. Defaults to https://bsky.app, but you may have another social-app fork you rather your users use 68 + //that need to capture this redirect url for creating an account 69 + default_successful_redirect_url: String, 70 + pds_service_did: Did<'static>, 71 + gate_jwe_key: Vec<u8>, 72 + captcha_success_redirects: Vec<String>, 73 + } 74 + 75 + impl AppConfig { 76 + pub fn new() -> Self { 77 + let pds_base_url = 78 + env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); 79 + let mailer_from = env::var("PDS_EMAIL_FROM_ADDRESS") 80 + .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); 81 + //Hack not my favorite, but it does work 82 + let allow_only_migrations = env::var("GATEKEEPER_ALLOW_ONLY_MIGRATIONS") 83 + .map(|val| val.parse::<bool>().unwrap_or(false)) 84 + .unwrap_or(false); 85 + 86 + let use_captcha = env::var("GATEKEEPER_CREATE_ACCOUNT_CAPTCHA") 87 + .map(|val| val.parse::<bool>().unwrap_or(false)) 88 + .unwrap_or(false); 89 + 90 + // PDS_SERVICE_DID is the did:web if set, if not it's PDS_HOSTNAME 91 + let pds_service_did = 92 + env::var("PDS_SERVICE_DID").unwrap_or_else(|_| match env::var("PDS_HOSTNAME") { 93 + Ok(pds_hostname) => format!("did:web:{}", pds_hostname), 94 + Err(_) => { 95 + panic!("PDS_HOSTNAME or PDS_SERVICE_DID must be set in your pds.env file") 96 + } 97 + }); 98 + 99 + let email_subject = env::var("GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT") 100 + .unwrap_or("Sign in to Bluesky".to_string()); 101 + 102 + // Load or generate JWE encryption key (32 bytes for AES-256) 103 + let gate_jwe_key = env::var("GATEKEEPER_JWE_KEY") 104 + .ok() 105 + .and_then(|key_hex| hex::decode(key_hex).ok()) 106 + .unwrap_or_else(|| { 107 + // Generate a random 32-byte key if not provided 108 + let key: Vec<u8> = (0..32).map(|_| rand::rng().random()).collect(); 109 + log::warn!("WARNING: No GATEKEEPER_JWE_KEY found in the environment. Generated random key (hex): {}", hex::encode(&key)); 110 + log::warn!("This is not strictly needed unless you scale PDS Gatekeeper. Will not also be able to verify tokens between reboots, but they are short lived (5mins)."); 111 + key 112 + }); 113 + 114 + if gate_jwe_key.len() != 32 { 115 + panic!( 116 + "GATEKEEPER_JWE_KEY must be 32 bytes (64 hex characters) for AES-256 encryption" 117 + ); 118 + } 119 + 120 + let captcha_success_redirects = match env::var("GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS") { 121 + Ok(from_env) => from_env.split(",").map(|s| s.trim().to_string()).collect(), 122 + Err(_) => { 123 + vec![ 124 + String::from("https://bsky.app"), 125 + String::from("https://pdsmoover.com"), 126 + String::from("https://blacksky.community"), 127 + String::from("https://tektite.cc"), 128 + ] 129 + } 130 + }; 131 + 132 + AppConfig { 133 + pds_base_url, 134 + mailer_from, 135 + email_subject, 136 + allow_only_migrations, 137 + use_captcha, 138 + default_successful_redirect_url: env::var("GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT") 139 + .unwrap_or("https://bsky.app".to_string()), 140 + pds_service_did: pds_service_did 141 + .parse() 142 + .expect("PDS_SERVICE_DID is not a valid did or could not infer from PDS_HOSTNAME"), 143 + gate_jwe_key, 144 + captcha_success_redirects, 145 + } 146 + } 147 + } 148 + 40 149 #[derive(Clone)] 41 150 pub struct AppState { 42 151 account_pool: SqlitePool, 43 152 pds_gatekeeper_pool: SqlitePool, 44 153 reverse_proxy_client: HyperUtilClient, 45 - pds_base_url: String, 46 154 mailer: AsyncSmtpTransport<Tokio1Executor>, 47 - mailer_from: String, 48 155 template_engine: Engine<Handlebars<'static>>, 156 + resolver: Arc<PublicResolver>, 157 + handle_cache: auth::HandleCache, 158 + app_config: AppConfig, 49 159 } 50 160 51 161 async fn root_handler() -> impl axum::response::IntoResponse { ··· 88 198 #[tokio::main] 89 199 async fn main() -> Result<(), Box<dyn std::error::Error>> { 90 200 setup_tracing(); 91 - //TODO may need to change where this reads from? Like an env variable for it's location? Or arg? 92 - dotenvy::from_path(Path::new("./pds.env"))?; 93 - let pds_root = env::var("PDS_DATA_DIRECTORY")?; 201 + let pds_env_location = 202 + env::var("PDS_ENV_LOCATION").unwrap_or_else(|_| "/pds/pds.env".to_string()); 203 + 204 + let result_of_finding_pds_env = dotenvy::from_path(Path::new(&pds_env_location)); 205 + if let Err(e) = result_of_finding_pds_env { 206 + log::error!( 207 + "Error loading pds.env file (ignore if you loaded your variables in the environment somehow else): {e}" 208 + ); 209 + } 210 + 211 + let pds_root = 212 + env::var("PDS_DATA_DIRECTORY").expect("PDS_DATA_DIRECTORY is not set in your pds.env file"); 94 213 let account_db_url = format!("{pds_root}/account.sqlite"); 95 214 96 215 let account_options = SqliteConnectOptions::new() ··· 127 246 //Emailer set up 128 247 let smtp_url = 129 248 env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 130 - let sent_from = env::var("PDS_EMAIL_FROM_ADDRESS") 131 - .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); 249 + 132 250 let mailer: AsyncSmtpTransport<Tokio1Executor> = 133 251 AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 134 252 //Email templates setup ··· 144 262 let _ = hbs.register_embed_templates::<EmailTemplates>(); 145 263 } 146 264 147 - let pds_base_url = 148 - env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); 265 + let _ = hbs.register_embed_templates::<HtmlTemplates>(); 266 + 267 + //Reads the PLC source from the pds env's or defaults to ol faithful 268 + let plc_source_url = 269 + env::var("PDS_DID_PLC_URL").unwrap_or_else(|_| "https://plc.directory".to_string()); 270 + let plc_source = PlcSource::PlcDirectory { 271 + base: plc_source_url.parse().unwrap(), 272 + }; 273 + let mut resolver = PublicResolver::default(); 274 + resolver = resolver.with_plc_source(plc_source.clone()); 149 275 150 276 let state = AppState { 151 277 account_pool, 152 278 pds_gatekeeper_pool, 153 279 reverse_proxy_client: client, 154 - pds_base_url, 155 280 mailer, 156 - mailer_from: sent_from, 157 281 template_engine: Engine::from(hbs), 282 + resolver: Arc::new(resolver), 283 + handle_cache: auth::HandleCache::new(), 284 + app_config: AppConfig::new(), 158 285 }; 159 286 160 287 // Rate limiting 161 288 //Allows 5 within 60 seconds, and after 60 should drop one off? So hit 5, then goes to 4 after 60 seconds. 162 - let create_session_governor_conf = GovernorConfigBuilder::default() 289 + let captcha_governor_conf = GovernorConfigBuilder::default() 163 290 .per_second(60) 164 291 .burst_size(5) 292 + .key_extractor(SmartIpKeyExtractor) 165 293 .finish() 166 - .expect("failed to create governor config. this should not happen and is a bug"); 294 + .expect("failed to create governor config for create session. this should not happen and is a bug"); 167 295 168 296 // Create a second config with the same settings for the other endpoint 169 297 let sign_in_governor_conf = GovernorConfigBuilder::default() 170 298 .per_second(60) 171 299 .burst_size(5) 300 + .key_extractor(SmartIpKeyExtractor) 172 301 .finish() 173 - .expect("failed to create governor config. this should not happen and is a bug"); 302 + .expect( 303 + "failed to create governor config for sign in. this should not happen and is a bug", 304 + ); 174 305 175 - let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); 306 + let create_account_limiter_time: Option<String> = 307 + env::var("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND").ok(); 308 + let create_account_limiter_burst: Option<String> = 309 + env::var("GATEKEEPER_CREATE_ACCOUNT_BURST").ok(); 310 + 311 + //Default should be 608 requests per 5 minutes, PDS is 300 per 500 so will never hit it ideally 312 + let mut create_account_governor_conf = GovernorConfigBuilder::default(); 313 + if create_account_limiter_time.is_some() { 314 + let time = create_account_limiter_time 315 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND not set") 316 + .parse::<u64>() 317 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND must be a valid integer"); 318 + create_account_governor_conf.per_second(time); 319 + } 320 + 321 + if create_account_limiter_burst.is_some() { 322 + let burst = create_account_limiter_burst 323 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST not set") 324 + .parse::<u32>() 325 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST must be a valid integer"); 326 + create_account_governor_conf.burst_size(burst); 327 + } 328 + 329 + let create_account_governor_conf = create_account_governor_conf 330 + .key_extractor(SmartIpKeyExtractor) 331 + .finish().expect( 332 + "failed to create governor config for create account. this should not happen and is a bug", 333 + ); 334 + 335 + let captcha_governor_limiter = captcha_governor_conf.limiter().clone(); 176 336 let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); 337 + let create_account_governor_limiter = create_account_governor_conf.limiter().clone(); 338 + 339 + let sign_in_governor_layer = GovernorLayer::new(sign_in_governor_conf); 340 + 177 341 let interval = Duration::from_secs(60); 178 342 // a separate background task to clean up 179 343 std::thread::spawn(move || { 180 344 loop { 181 345 std::thread::sleep(interval); 182 - create_session_governor_limiter.retain_recent(); 346 + captcha_governor_limiter.retain_recent(); 183 347 sign_in_governor_limiter.retain_recent(); 348 + create_account_governor_limiter.retain_recent(); 184 349 } 185 350 }); 186 351 ··· 189 354 .allow_methods([Method::GET, Method::OPTIONS, Method::POST]) 190 355 .allow_headers(Any); 191 356 192 - let app = Router::new() 357 + let mut app = Router::new() 193 358 .route("/", get(root_handler)) 359 + .route("/xrpc/com.atproto.server.getSession", get(get_session)) 194 360 .route( 195 - "/xrpc/com.atproto.server.getSession", 196 - get(get_session).layer(ax_middleware::from_fn(middleware::extract_did)), 361 + "/xrpc/com.atproto.server.describeServer", 362 + get(describe_server), 197 363 ) 198 364 .route( 199 365 "/xrpc/com.atproto.server.updateEmail", ··· 201 367 ) 202 368 .route( 203 369 "/@atproto/oauth-provider/~api/sign-in", 204 - post(sign_in).layer(GovernorLayer::new(sign_in_governor_conf)), 370 + post(sign_in).layer(sign_in_governor_layer.clone()), 205 371 ) 206 372 .route( 207 373 "/xrpc/com.atproto.server.createSession", 208 - post(create_session.layer(GovernorLayer::new(create_session_governor_conf))), 374 + post(create_session.layer(sign_in_governor_layer)), 209 375 ) 376 + .route( 377 + "/xrpc/com.atproto.server.createAccount", 378 + post(create_account).layer(GovernorLayer::new(create_account_governor_conf)), 379 + ); 380 + 381 + if state.app_config.use_captcha { 382 + app = app.route( 383 + "/gate/signup", 384 + get(get_gate).post(post_gate.layer(GovernorLayer::new(captcha_governor_conf))), 385 + ); 386 + } 387 + 388 + let app = app 210 389 .layer(CompressionLayer::new()) 211 390 .layer(cors) 212 391 .with_state(state); 213 392 214 - let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 393 + let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); 215 394 let port: u16 = env::var("GATEKEEPER_PORT") 216 395 .ok() 217 396 .and_then(|s| s.parse().ok())
+73 -39
src/middleware.rs
··· 12 12 #[derive(Clone, Debug)] 13 13 pub struct Did(pub Option<String>); 14 14 15 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 + pub enum AuthScheme { 17 + Bearer, 18 + DPoP, 19 + } 20 + 15 21 #[derive(Serialize, Deserialize)] 16 22 pub struct TokenClaims { 17 23 pub sub: String, 18 24 } 19 25 20 26 pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 21 - let token = extract_bearer(req.headers()); 27 + let auth = extract_auth(req.headers()); 22 28 23 - match token { 24 - Ok(token) => { 25 - match token { 29 + match auth { 30 + Ok(auth_opt) => { 31 + match auth_opt { 26 32 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 27 33 .expect("Error creating an error response"), 28 - Some(token) => { 29 - let token = UntrustedToken::new(&token); 30 - if token.is_err() { 31 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 32 - .expect("Error creating an error response"); 33 - } 34 - let parsed_token = token.expect("Already checked for error"); 35 - let claims: Result<Claims<TokenClaims>, ValidationError> = 36 - parsed_token.deserialize_claims_unchecked(); 37 - if claims.is_err() { 38 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 39 - .expect("Error creating an error response"); 40 - } 34 + Some((scheme, token_str)) => { 35 + // For Bearer, validate JWT and extract DID from `sub`. 36 + // For DPoP, we currently only pass through and do not validate here; insert None DID. 37 + match scheme { 38 + AuthScheme::Bearer => { 39 + let token = UntrustedToken::new(&token_str); 40 + if token.is_err() { 41 + return json_error_response( 42 + StatusCode::BAD_REQUEST, 43 + "TokenRequired", 44 + "", 45 + ) 46 + .expect("Error creating an error response"); 47 + } 48 + let parsed_token = token.expect("Already checked for error"); 49 + let claims: Result<Claims<TokenClaims>, ValidationError> = 50 + parsed_token.deserialize_claims_unchecked(); 51 + if claims.is_err() { 52 + return json_error_response( 53 + StatusCode::BAD_REQUEST, 54 + "TokenRequired", 55 + "", 56 + ) 57 + .expect("Error creating an error response"); 58 + } 41 59 42 - let key = Hs256Key::new( 43 - env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"), 44 - ); 45 - let token: Result<Token<TokenClaims>, ValidationError> = 46 - Hs256.validator(&key).validate(&parsed_token); 47 - if token.is_err() { 48 - return json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 49 - .expect("Error creating an error response"); 60 + let key = Hs256Key::new( 61 + env::var("PDS_JWT_SECRET") 62 + .expect("PDS_JWT_SECRET not set in the pds.env"), 63 + ); 64 + let token: Result<Token<TokenClaims>, ValidationError> = 65 + Hs256.validator(&key).validate(&parsed_token); 66 + if token.is_err() { 67 + return json_error_response( 68 + StatusCode::BAD_REQUEST, 69 + "InvalidToken", 70 + "", 71 + ) 72 + .expect("Error creating an error response"); 73 + } 74 + let token = token.expect("Already checked for error,"); 75 + req.extensions_mut() 76 + .insert(Did(Some(token.claims().custom.sub.clone()))); 77 + } 78 + AuthScheme::DPoP => { 79 + //Not going to worry about oauth email update for now, just always forward to the PDS 80 + req.extensions_mut().insert(Did(None)); 81 + } 50 82 } 51 - let token = token.expect("Already checked for error,"); 52 - //Not going to worry about expiration since it still goes to the PDS 53 - req.extensions_mut() 54 - .insert(Did(Some(token.claims().custom.sub.clone()))); 83 + 55 84 next.run(req).await 56 85 } 57 86 } ··· 64 93 } 65 94 } 66 95 67 - fn extract_bearer(headers: &HeaderMap) -> Result<Option<String>, String> { 96 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 68 97 match headers.get(axum::http::header::AUTHORIZATION) { 69 98 None => Ok(None), 70 - Some(hv) => match hv.to_str() { 71 - Err(_) => Err("Authorization header is not valid".into()), 72 - Ok(s) => { 73 - // Accept forms like: "Bearer <token>" (case-sensitive for the scheme here) 74 - let mut parts = s.splitn(2, ' '); 75 - match (parts.next(), parts.next()) { 76 - (Some("Bearer"), Some(tok)) if !tok.is_empty() => Ok(Some(tok.to_string())), 77 - _ => Err("Authorization header must be in format 'Bearer <token>'".into()), 99 + Some(hv) => { 100 + match hv.to_str() { 101 + Err(_) => Err("Authorization header is not valid".into()), 102 + Ok(s) => { 103 + // Accept forms like: "Bearer <token>" or "DPoP <token>" (case-sensitive for the scheme here) 104 + let mut parts = s.splitn(2, ' '); 105 + match (parts.next(), parts.next()) { 106 + (Some("Bearer"), Some(tok)) if !tok.is_empty() => 107 + Ok(Some((AuthScheme::Bearer, tok.to_string()))), 108 + (Some("DPoP"), Some(tok)) if !tok.is_empty() => 109 + Ok(Some((AuthScheme::DPoP, tok.to_string()))), 110 + _ => Err("Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'".into()), 111 + } 78 112 } 79 113 } 80 - }, 114 + } 81 115 } 82 116 }
+4 -6
src/oauth_provider.rs
··· 13 13 pub struct SignInRequest { 14 14 pub username: String, 15 15 pub password: String, 16 - pub remember: bool, 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + pub remember: Option<bool>, 17 18 pub locale: String, 18 19 #[serde(skip_serializing_if = "Option::is_none", rename = "emailOtp")] 19 20 pub email_otp: Option<String>, ··· 36 37 "Invalid identifier or password", 37 38 ), 38 39 AuthResult::TwoFactorRequired(masked_email) => { 39 - // Email sending step can be handled here if needed in the future. 40 - 41 - // {"error":"second_authentication_factor_required","error_description":"emailOtp authentication factor required (hint: 2***0@p***m)","type":"emailOtp","hint":"2***0@p***m"} 42 40 let body_str = match serde_json::to_string(&serde_json::json!({ 43 41 "error": "second_authentication_factor_required", 44 42 "error_description": format!("emailOtp authentication factor required (hint: {})", masked_email), ··· 59 57 //No 2FA or already passed 60 58 let uri = format!( 61 59 "{}{}", 62 - state.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" 60 + state.app_config.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" 63 61 ); 64 62 65 63 let mut req = axum::http::Request::post(uri); ··· 97 95 }, 98 96 Err(err) => { 99 97 log::error!( 100 - "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access:\n {err}" 98 + "Error during pre-auth check. This happens on the oauth signin endpoint when trying to decide if the user has access:\n {err}" 101 99 ); 102 100 oauth_json_error_response( 103 101 StatusCode::BAD_REQUEST,
+394 -53
src/xrpc/com_atproto_server.rs
··· 1 1 use crate::AppState; 2 2 use crate::helpers::{ 3 - AuthResult, ProxiedResult, TokenCheckError, json_error_response, preauth_check, proxy_get_json, 3 + AuthResult, ProxiedResult, TokenCheckError, VerifyServiceAuthError, json_error_response, 4 + preauth_check, proxy_get_json, verify_gate_token, verify_service_auth, 4 5 }; 5 6 use crate::middleware::Did; 6 - use axum::body::Body; 7 + use axum::body::{Body, to_bytes}; 7 8 use axum::extract::State; 8 - use axum::http::{HeaderMap, StatusCode}; 9 + use axum::http::{HeaderMap, StatusCode, header}; 9 10 use axum::response::{IntoResponse, Response}; 10 11 use axum::{Extension, Json, debug_handler, extract, extract::Request}; 12 + use chrono::{Duration, Utc}; 13 + use jacquard_common::types::did::Did as JacquardDid; 11 14 use serde::{Deserialize, Serialize}; 12 15 use serde_json; 13 16 use tracing::log; ··· 61 64 allow_takendown: Option<bool>, 62 65 } 63 66 67 + #[derive(Deserialize, Serialize, Debug)] 68 + #[serde(rename_all = "camelCase")] 69 + pub struct CreateAccountRequest { 70 + handle: String, 71 + #[serde(skip_serializing_if = "Option::is_none")] 72 + email: Option<String>, 73 + #[serde(skip_serializing_if = "Option::is_none")] 74 + password: Option<String>, 75 + #[serde(skip_serializing_if = "Option::is_none")] 76 + did: Option<String>, 77 + #[serde(skip_serializing_if = "Option::is_none")] 78 + invite_code: Option<String>, 79 + #[serde(skip_serializing_if = "Option::is_none")] 80 + verification_code: Option<String>, 81 + #[serde(skip_serializing_if = "Option::is_none")] 82 + plc_op: Option<serde_json::Value>, 83 + } 84 + 85 + #[derive(Deserialize, Serialize, Debug, Clone)] 86 + #[serde(rename_all = "camelCase")] 87 + pub struct DescribeServerContact { 88 + #[serde(skip_serializing_if = "Option::is_none")] 89 + email: Option<String>, 90 + } 91 + 92 + #[derive(Deserialize, Serialize, Debug, Clone)] 93 + #[serde(rename_all = "camelCase")] 94 + pub struct DescribeServerLinks { 95 + #[serde(skip_serializing_if = "Option::is_none")] 96 + privacy_policy: Option<String>, 97 + #[serde(skip_serializing_if = "Option::is_none")] 98 + terms_of_service: Option<String>, 99 + } 100 + 101 + #[derive(Deserialize, Serialize, Debug, Clone)] 102 + #[serde(rename_all = "camelCase")] 103 + pub struct DescribeServerResponse { 104 + #[serde(skip_serializing_if = "Option::is_none")] 105 + invite_code_required: Option<bool>, 106 + #[serde(skip_serializing_if = "Option::is_none")] 107 + phone_verification_required: Option<bool>, 108 + #[serde(skip_serializing_if = "Option::is_none")] 109 + available_user_domains: Option<Vec<String>>, 110 + #[serde(skip_serializing_if = "Option::is_none")] 111 + links: Option<DescribeServerLinks>, 112 + #[serde(skip_serializing_if = "Option::is_none")] 113 + contact: Option<DescribeServerContact>, 114 + #[serde(skip_serializing_if = "Option::is_none")] 115 + did: Option<String>, 116 + } 117 + 64 118 pub async fn create_session( 65 119 State(state): State<AppState>, 66 120 headers: HeaderMap, ··· 87 141 ) 88 142 } 89 143 AuthResult::ProxyThrough => { 90 - log::info!("Proxying through"); 91 144 //No 2FA or already passed 92 145 let uri = format!( 93 146 "{}{}", 94 - state.pds_base_url, "/xrpc/com.atproto.server.createSession" 147 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createSession" 95 148 ); 96 149 97 150 let mut req = axum::http::Request::post(uri); ··· 148 201 //If email auth is set it is to either turn on or off 2fa 149 202 let email_auth_update = payload.email_auth_factor.unwrap_or(false); 150 203 151 - // Email update asked for 152 - if email_auth_update { 153 - let email = payload.email.clone(); 154 - let email_confirmed = sqlx::query_as::<_, (String,)>( 155 - "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 156 - ) 157 - .bind(&email) 158 - .fetch_optional(&state.account_pool) 159 - .await 160 - .map_err(|_| StatusCode::BAD_REQUEST)?; 161 - 162 - //Since the email is already confirmed we can enable 2fa 163 - return match email_confirmed { 164 - None => Err(StatusCode::BAD_REQUEST), 165 - Some(did_row) => { 166 - let _ = sqlx::query( 167 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 168 - ) 169 - .bind(&did_row.0) 170 - .execute(&state.pds_gatekeeper_pool) 171 - .await 172 - .map_err(|_| StatusCode::BAD_REQUEST)?; 173 - 174 - Ok(StatusCode::OK.into_response()) 175 - } 176 - }; 177 - } 204 + //This means the middleware successfully extracted a did from the request, if not it just needs to be forward to the PDS 205 + //This is also empty if it is an oauth request, which is not supported by gatekeeper turning on 2fa since the dpop stuff needs to be implemented 206 + let did_is_not_empty = did.0.is_some(); 178 207 179 - // User wants auth turned off 180 - if !email_auth_update && !email_auth_not_set { 181 - //User wants auth turned off and has a token 182 - if let Some(token) = &payload.token { 183 - let token_found = sqlx::query_as::<_, (String,)>( 184 - "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 208 + if did_is_not_empty { 209 + // Email update asked for 210 + if email_auth_update { 211 + let email = payload.email.clone(); 212 + let email_confirmed = match sqlx::query_as::<_, (String,)>( 213 + "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 185 214 ) 186 - .bind(token) 187 - .bind(&did.0) 215 + .bind(&email) 188 216 .fetch_optional(&state.account_pool) 189 217 .await 190 - .map_err(|_| StatusCode::BAD_REQUEST)?; 218 + { 219 + Ok(row) => row, 220 + Err(err) => { 221 + log::error!("Error checking if email is confirmed: {err}"); 222 + return Err(StatusCode::BAD_REQUEST); 223 + } 224 + }; 191 225 192 - if token_found.is_some() { 193 - let _ = sqlx::query( 194 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 226 + //Since the email is already confirmed we can enable 2fa 227 + return match email_confirmed { 228 + None => Err(StatusCode::BAD_REQUEST), 229 + Some(did_row) => { 230 + let _ = sqlx::query( 231 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 232 + ) 233 + .bind(&did_row.0) 234 + .execute(&state.pds_gatekeeper_pool) 235 + .await 236 + .map_err(|_| StatusCode::BAD_REQUEST)?; 237 + 238 + Ok(StatusCode::OK.into_response()) 239 + } 240 + }; 241 + } 242 + 243 + // User wants auth turned off 244 + if !email_auth_update && !email_auth_not_set { 245 + //User wants auth turned off and has a token 246 + if let Some(token) = &payload.token { 247 + let token_found = match sqlx::query_as::<_, (String,)>( 248 + "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 195 249 ) 196 - .bind(&did.0) 197 - .execute(&state.pds_gatekeeper_pool) 198 - .await 199 - .map_err(|_| StatusCode::BAD_REQUEST)?; 250 + .bind(token) 251 + .bind(&did.0) 252 + .fetch_optional(&state.account_pool) 253 + .await{ 254 + Ok(token) => token, 255 + Err(err) => { 256 + log::error!("Error checking if token is valid: {err}"); 257 + return Err(StatusCode::BAD_REQUEST); 258 + } 259 + }; 200 260 201 - return Ok(StatusCode::OK.into_response()); 202 - } else { 203 - return Err(StatusCode::BAD_REQUEST); 261 + return if token_found.is_some() { 262 + //TODO I think there may be a bug here and need to do some retry logic 263 + // First try was erroring, seconds was allowing 264 + match sqlx::query( 265 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 266 + ) 267 + .bind(&did.0) 268 + .execute(&state.pds_gatekeeper_pool) 269 + .await { 270 + Ok(_) => {} 271 + Err(err) => { 272 + log::error!("Error updating email auth: {err}"); 273 + return Err(StatusCode::BAD_REQUEST); 274 + } 275 + } 276 + 277 + Ok(StatusCode::OK.into_response()) 278 + } else { 279 + Err(StatusCode::BAD_REQUEST) 280 + }; 204 281 } 205 282 } 206 283 } 207 - 208 284 // Updating the actual email address by sending it on to the PDS 209 285 let uri = format!( 210 286 "{}{}", 211 - state.pds_base_url, "/xrpc/com.atproto.server.updateEmail" 287 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.updateEmail" 212 288 ); 213 289 let mut req = axum::http::Request::post(uri); 214 290 if let Some(req_headers) = req.headers_mut() { ··· 260 336 ProxiedResult::Passthrough(resp) => Ok(resp), 261 337 } 262 338 } 339 + 340 + pub async fn describe_server( 341 + State(state): State<AppState>, 342 + req: Request, 343 + ) -> Result<Response<Body>, StatusCode> { 344 + match proxy_get_json::<DescribeServerResponse>( 345 + &state, 346 + req, 347 + "/xrpc/com.atproto.server.describeServer", 348 + ) 349 + .await? 350 + { 351 + ProxiedResult::Parsed { 352 + value: mut server_info, 353 + .. 354 + } => { 355 + //This signifies the server is configured for captcha verification 356 + server_info.phone_verification_required = Some(state.app_config.use_captcha); 357 + Ok(Json(server_info).into_response()) 358 + } 359 + ProxiedResult::Passthrough(resp) => Ok(resp), 360 + } 361 + } 362 + 363 + /// Verify a gate code matches the handle and is not expired 364 + async fn verify_gate_code( 365 + state: &AppState, 366 + code: &str, 367 + handle: &str, 368 + ) -> Result<bool, anyhow::Error> { 369 + // First, decrypt and verify the JWE token 370 + let payload = match verify_gate_token(code, &state.app_config.gate_jwe_key) { 371 + Ok(p) => p, 372 + Err(e) => { 373 + log::warn!("Failed to decrypt gate token: {}", e); 374 + return Ok(false); 375 + } 376 + }; 377 + 378 + // Verify the handle matches 379 + if payload.handle != handle { 380 + log::warn!( 381 + "Gate code handle mismatch: expected {}, got {}", 382 + handle, 383 + payload.handle 384 + ); 385 + return Ok(false); 386 + } 387 + 388 + let created_at = chrono::DateTime::parse_from_rfc3339(&payload.created_at) 389 + .map_err(|e| anyhow::anyhow!("Failed to parse created_at from token: {}", e))? 390 + .with_timezone(&Utc); 391 + 392 + let now = Utc::now(); 393 + let age = now - created_at; 394 + 395 + // Check if the token is expired (5 minutes) 396 + if age > Duration::minutes(5) { 397 + log::warn!("Gate code expired for handle {}", handle); 398 + return Ok(false); 399 + } 400 + 401 + // Verify the token exists in the database (to prevent reuse) 402 + let row: Option<(String,)> = 403 + sqlx::query_as("SELECT code FROM gate_codes WHERE code = ? and handle = ? LIMIT 1") 404 + .bind(code) 405 + .bind(handle) 406 + .fetch_optional(&state.pds_gatekeeper_pool) 407 + .await?; 408 + 409 + if row.is_none() { 410 + log::warn!("Gate code not found in database or already used"); 411 + return Ok(false); 412 + } 413 + 414 + // Token is valid, delete it so it can't be reused 415 + //TODO probably also delete expired codes? Will need to do that at some point probably altho the where is on code and handle 416 + 417 + sqlx::query("DELETE FROM gate_codes WHERE code = ?") 418 + .bind(code) 419 + .execute(&state.pds_gatekeeper_pool) 420 + .await?; 421 + 422 + Ok(true) 423 + } 424 + 425 + pub async fn create_account( 426 + State(state): State<AppState>, 427 + req: Request, 428 + ) -> Result<Response<Body>, StatusCode> { 429 + let headers = req.headers().clone(); 430 + let body_bytes = to_bytes(req.into_body(), usize::MAX) 431 + .await 432 + .map_err(|_| StatusCode::BAD_REQUEST)?; 433 + 434 + // Parse the body to check for verification code 435 + let account_request: CreateAccountRequest = 436 + serde_json::from_slice(&body_bytes).map_err(|e| { 437 + log::error!("Failed to parse create account request: {}", e); 438 + StatusCode::BAD_REQUEST 439 + })?; 440 + 441 + // Check for service auth (migrations) if configured 442 + if state.app_config.allow_only_migrations { 443 + // Expect Authorization: Bearer <jwt> 444 + let auth_header = headers 445 + .get(header::AUTHORIZATION) 446 + .and_then(|v| v.to_str().ok()) 447 + .map(str::to_string); 448 + 449 + let Some(value) = auth_header else { 450 + log::error!("No Authorization header found in the request"); 451 + return json_error_response( 452 + StatusCode::UNAUTHORIZED, 453 + "InvalidAuth", 454 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 455 + ); 456 + }; 457 + 458 + // Ensure Bearer prefix 459 + let token = value.strip_prefix("Bearer ").unwrap_or("").trim(); 460 + if token.is_empty() { 461 + log::error!("No Service Auth token found in the Authorization header"); 462 + return json_error_response( 463 + StatusCode::UNAUTHORIZED, 464 + "InvalidAuth", 465 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 466 + ); 467 + } 468 + 469 + // Ensure a non-empty DID was provided when migrations are enabled 470 + let requested_did_str = match account_request.did.as_deref() { 471 + Some(s) if !s.trim().is_empty() => s, 472 + _ => { 473 + return json_error_response( 474 + StatusCode::BAD_REQUEST, 475 + "InvalidRequest", 476 + "The 'did' field is required when migrations are enforced.", 477 + ); 478 + } 479 + }; 480 + 481 + // Parse the DID into the expected type for verification 482 + let requested_did: JacquardDid<'static> = match requested_did_str.parse() { 483 + Ok(d) => d, 484 + Err(e) => { 485 + log::error!( 486 + "Invalid DID format provided in createAccount: {} | error: {}", 487 + requested_did_str, 488 + e 489 + ); 490 + return json_error_response( 491 + StatusCode::BAD_REQUEST, 492 + "InvalidRequest", 493 + "The 'did' field is not a valid DID.", 494 + ); 495 + } 496 + }; 497 + 498 + let nsid = "com.atproto.server.createAccount".parse().unwrap(); 499 + match verify_service_auth( 500 + token, 501 + &nsid, 502 + state.resolver.clone(), 503 + &state.app_config.pds_service_did, 504 + &requested_did, 505 + ) 506 + .await 507 + { 508 + //Just do nothing if it passes so it continues. 509 + Ok(_) => {} 510 + Err(err) => match err { 511 + VerifyServiceAuthError::AuthFailed => { 512 + return json_error_response( 513 + StatusCode::UNAUTHORIZED, 514 + "InvalidAuth", 515 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 516 + ); 517 + } 518 + VerifyServiceAuthError::Error(err) => { 519 + log::error!("Error verifying service auth token: {err}"); 520 + return json_error_response( 521 + StatusCode::BAD_REQUEST, 522 + "InvalidRequest", 523 + "There has been an error, please contact your PDS administrator for help and for them to review the server logs.", 524 + ); 525 + } 526 + }, 527 + } 528 + } 529 + 530 + // Check for captcha verification if configured 531 + if state.app_config.use_captcha { 532 + if let Some(ref verification_code) = account_request.verification_code { 533 + match verify_gate_code(&state, verification_code, &account_request.handle).await { 534 + //TODO has a few errors to support 535 + 536 + //expired token 537 + // { 538 + // "error": "ExpiredToken", 539 + // "message": "Token has expired" 540 + // } 541 + 542 + //TODO ALSO add rate limits on the /gate endpoints so they can't be abused 543 + Ok(true) => { 544 + log::info!("Gate code verified for handle: {}", account_request.handle); 545 + } 546 + Ok(false) => { 547 + log::warn!( 548 + "Invalid or expired gate code for handle: {}", 549 + account_request.handle 550 + ); 551 + return json_error_response( 552 + StatusCode::BAD_REQUEST, 553 + "InvalidToken", 554 + "Token could not be verified", 555 + ); 556 + } 557 + Err(e) => { 558 + log::error!("Error verifying gate code: {}", e); 559 + return json_error_response( 560 + StatusCode::INTERNAL_SERVER_ERROR, 561 + "InvalidToken", 562 + "Token could not be verified", 563 + ); 564 + } 565 + } 566 + } else { 567 + // No verification code provided but captcha is required 568 + log::warn!( 569 + "No verification code provided for account creation: {}", 570 + account_request.handle 571 + ); 572 + return json_error_response( 573 + StatusCode::BAD_REQUEST, 574 + "InvalidRequest", 575 + "Verification is now required on this server.", 576 + ); 577 + } 578 + } 579 + 580 + // Rebuild the request with the same body and headers 581 + let uri = format!( 582 + "{}{}", 583 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createAccount" 584 + ); 585 + 586 + let mut new_req = axum::http::Request::post(&uri); 587 + if let Some(req_headers) = new_req.headers_mut() { 588 + *req_headers = headers; 589 + } 590 + 591 + let new_req = new_req 592 + .body(Body::from(body_bytes)) 593 + .map_err(|_| StatusCode::BAD_REQUEST)?; 594 + 595 + let proxied = state 596 + .reverse_proxy_client 597 + .request(new_req) 598 + .await 599 + .map_err(|_| StatusCode::BAD_REQUEST)? 600 + .into_response(); 601 + 602 + Ok(proxied) 603 + }