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
+2 -1
.gitignore
··· 1 1 /target 2 - .idea 2 + .idea 3 + pds.env
+2703 -217
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "abnf" 7 + version = "0.13.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" 10 + dependencies = [ 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", 22 + ] 23 + 24 + [[package]] 6 25 name = "addr2line" 7 26 version = "0.24.2" 8 27 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 24 43 checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 25 44 dependencies = [ 26 45 "cfg-if", 27 - "getrandom 0.3.3", 28 46 "once_cell", 29 47 "version_check", 30 48 "zerocopy", ··· 40 58 ] 41 59 42 60 [[package]] 61 + name = "aliasable" 62 + version = "0.1.3" 63 + source = "registry+https://github.com/rust-lang/crates.io-index" 64 + checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" 65 + 66 + [[package]] 43 67 name = "allocator-api2" 44 68 version = "0.2.21" 45 69 source = "registry+https://github.com/rust-lang/crates.io-index" 46 70 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 47 71 48 72 [[package]] 73 + name = "android_system_properties" 74 + version = "0.1.5" 75 + source = "registry+https://github.com/rust-lang/crates.io-index" 76 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 77 + dependencies = [ 78 + "libc", 79 + ] 80 + 81 + [[package]] 82 + name = "anyhow" 83 + version = "1.0.99" 84 + source = "registry+https://github.com/rust-lang/crates.io-index" 85 + checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 86 + 87 + [[package]] 88 + name = "async-compression" 89 + version = "0.4.27" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" 92 + dependencies = [ 93 + "flate2", 94 + "futures-core", 95 + "memchr", 96 + "pin-project-lite", 97 + "tokio", 98 + "zstd", 99 + "zstd-safe", 100 + ] 101 + 102 + [[package]] 49 103 name = "async-trait" 50 104 version = "0.1.89" 51 105 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 66 120 ] 67 121 68 122 [[package]] 123 + name = "atomic-waker" 124 + version = "1.1.2" 125 + source = "registry+https://github.com/rust-lang/crates.io-index" 126 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 127 + 128 + [[package]] 69 129 name = "autocfg" 70 130 version = "1.5.0" 71 131 source = "registry+https://github.com/rust-lang/crates.io-index" 72 132 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 73 133 74 134 [[package]] 135 + name = "aws-lc-rs" 136 + version = "1.13.3" 137 + source = "registry+https://github.com/rust-lang/crates.io-index" 138 + checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" 139 + dependencies = [ 140 + "aws-lc-sys", 141 + "untrusted 0.7.1", 142 + "zeroize", 143 + ] 144 + 145 + [[package]] 146 + name = "aws-lc-sys" 147 + version = "0.30.0" 148 + source = "registry+https://github.com/rust-lang/crates.io-index" 149 + checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" 150 + dependencies = [ 151 + "bindgen", 152 + "cc", 153 + "cmake", 154 + "dunce", 155 + "fs_extra", 156 + ] 157 + 158 + [[package]] 75 159 name = "axum" 76 - version = "0.7.9" 160 + version = "0.8.4" 77 161 source = "registry+https://github.com/rust-lang/crates.io-index" 78 - checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 162 + checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 79 163 dependencies = [ 80 - "async-trait", 81 164 "axum-core", 82 165 "axum-macros", 83 166 "bytes", 167 + "form_urlencoded", 84 168 "futures-util", 85 169 "http", 86 170 "http-body", ··· 108 192 109 193 [[package]] 110 194 name = "axum-core" 111 - version = "0.4.5" 195 + version = "0.5.2" 112 196 source = "registry+https://github.com/rust-lang/crates.io-index" 113 - checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 197 + checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 114 198 dependencies = [ 115 - "async-trait", 116 199 "bytes", 117 - "futures-util", 200 + "futures-core", 118 201 "http", 119 202 "http-body", 120 203 "http-body-util", ··· 129 212 130 213 [[package]] 131 214 name = "axum-macros" 132 - version = "0.4.2" 215 + version = "0.5.0" 133 216 source = "registry+https://github.com/rust-lang/crates.io-index" 134 - checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" 217 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 135 218 dependencies = [ 136 219 "proc-macro2", 137 220 "quote", 138 221 "syn 2.0.105", 222 + ] 223 + 224 + [[package]] 225 + name = "axum-template" 226 + version = "3.0.0" 227 + source = "registry+https://github.com/rust-lang/crates.io-index" 228 + checksum = "3df50f7d669bfc3a8c348f08f536fe37e7acfbeded3cfdffd2ad3d76725fc40c" 229 + dependencies = [ 230 + "axum", 231 + "handlebars", 232 + "serde", 233 + "thiserror 2.0.14", 139 234 ] 140 235 141 236 [[package]] ··· 154 249 ] 155 250 156 251 [[package]] 252 + name = "base-x" 253 + version = "0.2.11" 254 + source = "registry+https://github.com/rust-lang/crates.io-index" 255 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 256 + 257 + [[package]] 258 + name = "base16ct" 259 + version = "0.2.0" 260 + source = "registry+https://github.com/rust-lang/crates.io-index" 261 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 262 + 263 + [[package]] 264 + name = "base256emoji" 265 + version = "1.0.2" 266 + source = "registry+https://github.com/rust-lang/crates.io-index" 267 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 268 + dependencies = [ 269 + "const-str", 270 + "match-lookup", 271 + ] 272 + 273 + [[package]] 157 274 name = "base64" 158 - version = "0.21.7" 275 + version = "0.22.1" 159 276 source = "registry+https://github.com/rust-lang/crates.io-index" 160 - checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 277 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 161 278 162 279 [[package]] 163 280 name = "base64ct" ··· 166 283 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 167 284 168 285 [[package]] 286 + name = "bindgen" 287 + version = "0.69.5" 288 + source = "registry+https://github.com/rust-lang/crates.io-index" 289 + checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" 290 + dependencies = [ 291 + "bitflags", 292 + "cexpr", 293 + "clang-sys", 294 + "itertools", 295 + "lazy_static", 296 + "lazycell", 297 + "log", 298 + "prettyplease", 299 + "proc-macro2", 300 + "quote", 301 + "regex", 302 + "rustc-hash 1.1.0", 303 + "shlex", 304 + "syn 2.0.105", 305 + "which", 306 + ] 307 + 308 + [[package]] 169 309 name = "bitflags" 170 310 version = "2.9.1" 171 311 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 184 324 ] 185 325 186 326 [[package]] 327 + name = "bon" 328 + version = "3.8.1" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" 331 + dependencies = [ 332 + "bon-macros", 333 + "rustversion", 334 + ] 335 + 336 + [[package]] 337 + name = "bon-macros" 338 + version = "3.8.1" 339 + source = "registry+https://github.com/rust-lang/crates.io-index" 340 + checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" 341 + dependencies = [ 342 + "darling 0.21.3", 343 + "ident_case", 344 + "prettyplease", 345 + "proc-macro2", 346 + "quote", 347 + "rustversion", 348 + "syn 2.0.105", 349 + ] 350 + 351 + [[package]] 352 + name = "borsh" 353 + version = "1.6.0" 354 + source = "registry+https://github.com/rust-lang/crates.io-index" 355 + checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" 356 + dependencies = [ 357 + "cfg_aliases", 358 + ] 359 + 360 + [[package]] 361 + name = "bstr" 362 + version = "1.12.0" 363 + source = "registry+https://github.com/rust-lang/crates.io-index" 364 + checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" 365 + dependencies = [ 366 + "memchr", 367 + "serde", 368 + ] 369 + 370 + [[package]] 371 + name = "btree-range-map" 372 + version = "0.7.2" 373 + source = "registry+https://github.com/rust-lang/crates.io-index" 374 + checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" 375 + dependencies = [ 376 + "btree-slab", 377 + "cc-traits", 378 + "range-traits", 379 + "serde", 380 + "slab", 381 + ] 382 + 383 + [[package]] 384 + name = "btree-slab" 385 + version = "0.6.1" 386 + source = "registry+https://github.com/rust-lang/crates.io-index" 387 + checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" 388 + dependencies = [ 389 + "cc-traits", 390 + "slab", 391 + "smallvec", 392 + ] 393 + 394 + [[package]] 395 + name = "bumpalo" 396 + version = "3.19.0" 397 + source = "registry+https://github.com/rust-lang/crates.io-index" 398 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 399 + 400 + [[package]] 187 401 name = "byteorder" 188 402 version = "1.5.0" 189 403 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 194 408 version = "1.10.1" 195 409 source = "registry+https://github.com/rust-lang/crates.io-index" 196 410 checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 411 + dependencies = [ 412 + "serde", 413 + ] 414 + 415 + [[package]] 416 + name = "cbor4ii" 417 + version = "0.2.14" 418 + source = "registry+https://github.com/rust-lang/crates.io-index" 419 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 420 + dependencies = [ 421 + "serde", 422 + ] 197 423 198 424 [[package]] 199 425 name = "cc" ··· 201 427 source = "registry+https://github.com/rust-lang/crates.io-index" 202 428 checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" 203 429 dependencies = [ 430 + "jobserver", 431 + "libc", 204 432 "shlex", 205 433 ] 206 434 207 435 [[package]] 436 + name = "cc-traits" 437 + version = "2.0.0" 438 + source = "registry+https://github.com/rust-lang/crates.io-index" 439 + checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" 440 + dependencies = [ 441 + "slab", 442 + ] 443 + 444 + [[package]] 445 + name = "cexpr" 446 + version = "0.6.0" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 449 + dependencies = [ 450 + "nom 7.1.3", 451 + ] 452 + 453 + [[package]] 208 454 name = "cfg-if" 209 455 version = "1.0.1" 210 456 source = "registry+https://github.com/rust-lang/crates.io-index" 211 457 checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 212 458 213 459 [[package]] 460 + name = "cfg_aliases" 461 + version = "0.2.1" 462 + source = "registry+https://github.com/rust-lang/crates.io-index" 463 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 464 + 465 + [[package]] 466 + name = "chrono" 467 + version = "0.4.42" 468 + source = "registry+https://github.com/rust-lang/crates.io-index" 469 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 470 + dependencies = [ 471 + "iana-time-zone", 472 + "js-sys", 473 + "num-traits", 474 + "serde", 475 + "wasm-bindgen", 476 + "windows-link 0.2.1", 477 + ] 478 + 479 + [[package]] 480 + name = "chumsky" 481 + version = "0.9.3" 482 + source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" 484 + dependencies = [ 485 + "hashbrown 0.14.5", 486 + "stacker", 487 + ] 488 + 489 + [[package]] 490 + name = "ciborium" 491 + version = "0.2.2" 492 + source = "registry+https://github.com/rust-lang/crates.io-index" 493 + checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 494 + dependencies = [ 495 + "ciborium-io", 496 + "ciborium-ll", 497 + "serde", 498 + ] 499 + 500 + [[package]] 501 + name = "ciborium-io" 502 + version = "0.2.2" 503 + source = "registry+https://github.com/rust-lang/crates.io-index" 504 + checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 505 + 506 + [[package]] 507 + name = "ciborium-ll" 508 + version = "0.2.2" 509 + source = "registry+https://github.com/rust-lang/crates.io-index" 510 + checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 511 + dependencies = [ 512 + "ciborium-io", 513 + "half", 514 + ] 515 + 516 + [[package]] 517 + name = "cid" 518 + version = "0.11.1" 519 + source = "registry+https://github.com/rust-lang/crates.io-index" 520 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 521 + dependencies = [ 522 + "core2", 523 + "multibase", 524 + "multihash", 525 + "serde", 526 + "serde_bytes", 527 + "unsigned-varint", 528 + ] 529 + 530 + [[package]] 531 + name = "cipher" 532 + version = "0.4.4" 533 + source = "registry+https://github.com/rust-lang/crates.io-index" 534 + checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 535 + dependencies = [ 536 + "crypto-common", 537 + "inout", 538 + ] 539 + 540 + [[package]] 541 + name = "clang-sys" 542 + version = "1.8.1" 543 + source = "registry+https://github.com/rust-lang/crates.io-index" 544 + checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 545 + dependencies = [ 546 + "glob", 547 + "libc", 548 + "libloading", 549 + ] 550 + 551 + [[package]] 552 + name = "cmake" 553 + version = "0.1.54" 554 + source = "registry+https://github.com/rust-lang/crates.io-index" 555 + checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 556 + dependencies = [ 557 + "cc", 558 + ] 559 + 560 + [[package]] 561 + name = "concurrent-queue" 562 + version = "2.5.0" 563 + source = "registry+https://github.com/rust-lang/crates.io-index" 564 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 565 + dependencies = [ 566 + "crossbeam-utils", 567 + ] 568 + 569 + [[package]] 214 570 name = "const-oid" 215 571 version = "0.9.6" 216 572 source = "registry+https://github.com/rust-lang/crates.io-index" 217 573 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 574 + 575 + [[package]] 576 + name = "const-str" 577 + version = "0.4.3" 578 + source = "registry+https://github.com/rust-lang/crates.io-index" 579 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 580 + 581 + [[package]] 582 + name = "core-foundation" 583 + version = "0.9.4" 584 + source = "registry+https://github.com/rust-lang/crates.io-index" 585 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 586 + dependencies = [ 587 + "core-foundation-sys", 588 + "libc", 589 + ] 590 + 591 + [[package]] 592 + name = "core-foundation-sys" 593 + version = "0.8.7" 594 + source = "registry+https://github.com/rust-lang/crates.io-index" 595 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 596 + 597 + [[package]] 598 + name = "core2" 599 + version = "0.4.0" 600 + source = "registry+https://github.com/rust-lang/crates.io-index" 601 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 602 + dependencies = [ 603 + "memchr", 604 + ] 218 605 219 606 [[package]] 220 607 name = "cpufeatures" ··· 241 628 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 242 629 243 630 [[package]] 631 + name = "crc32fast" 632 + version = "1.5.0" 633 + source = "registry+https://github.com/rust-lang/crates.io-index" 634 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 635 + dependencies = [ 636 + "cfg-if", 637 + ] 638 + 639 + [[package]] 244 640 name = "crossbeam-queue" 245 641 version = "0.3.12" 246 642 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 256 652 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 257 653 258 654 [[package]] 655 + name = "crunchy" 656 + version = "0.2.4" 657 + source = "registry+https://github.com/rust-lang/crates.io-index" 658 + checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 659 + 660 + [[package]] 661 + name = "crypto-bigint" 662 + version = "0.5.5" 663 + source = "registry+https://github.com/rust-lang/crates.io-index" 664 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 665 + dependencies = [ 666 + "generic-array", 667 + "rand_core 0.6.4", 668 + "subtle", 669 + "zeroize", 670 + ] 671 + 672 + [[package]] 259 673 name = "crypto-common" 260 674 version = "0.1.6" 261 675 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 263 677 dependencies = [ 264 678 "generic-array", 265 679 "typenum", 680 + ] 681 + 682 + [[package]] 683 + name = "darling" 684 + version = "0.20.11" 685 + source = "registry+https://github.com/rust-lang/crates.io-index" 686 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 687 + dependencies = [ 688 + "darling_core 0.20.11", 689 + "darling_macro 0.20.11", 690 + ] 691 + 692 + [[package]] 693 + name = "darling" 694 + version = "0.21.3" 695 + source = "registry+https://github.com/rust-lang/crates.io-index" 696 + checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" 697 + dependencies = [ 698 + "darling_core 0.21.3", 699 + "darling_macro 0.21.3", 700 + ] 701 + 702 + [[package]] 703 + name = "darling_core" 704 + version = "0.20.11" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 707 + dependencies = [ 708 + "fnv", 709 + "ident_case", 710 + "proc-macro2", 711 + "quote", 712 + "strsim", 713 + "syn 2.0.105", 714 + ] 715 + 716 + [[package]] 717 + name = "darling_core" 718 + version = "0.21.3" 719 + source = "registry+https://github.com/rust-lang/crates.io-index" 720 + checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" 721 + dependencies = [ 722 + "fnv", 723 + "ident_case", 724 + "proc-macro2", 725 + "quote", 726 + "strsim", 727 + "syn 2.0.105", 728 + ] 729 + 730 + [[package]] 731 + name = "darling_macro" 732 + version = "0.20.11" 733 + source = "registry+https://github.com/rust-lang/crates.io-index" 734 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 735 + dependencies = [ 736 + "darling_core 0.20.11", 737 + "quote", 738 + "syn 2.0.105", 739 + ] 740 + 741 + [[package]] 742 + name = "darling_macro" 743 + version = "0.21.3" 744 + source = "registry+https://github.com/rust-lang/crates.io-index" 745 + checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" 746 + dependencies = [ 747 + "darling_core 0.21.3", 748 + "quote", 749 + "syn 2.0.105", 750 + ] 751 + 752 + [[package]] 753 + name = "dashmap" 754 + version = "6.1.0" 755 + source = "registry+https://github.com/rust-lang/crates.io-index" 756 + checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 757 + dependencies = [ 758 + "cfg-if", 759 + "crossbeam-utils", 760 + "hashbrown 0.14.5", 761 + "lock_api", 762 + "once_cell", 763 + "parking_lot_core", 764 + ] 765 + 766 + [[package]] 767 + name = "data-encoding" 768 + version = "2.9.0" 769 + source = "registry+https://github.com/rust-lang/crates.io-index" 770 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 771 + 772 + [[package]] 773 + name = "data-encoding-macro" 774 + version = "0.1.18" 775 + source = "registry+https://github.com/rust-lang/crates.io-index" 776 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 777 + dependencies = [ 778 + "data-encoding", 779 + "data-encoding-macro-internal", 780 + ] 781 + 782 + [[package]] 783 + name = "data-encoding-macro-internal" 784 + version = "0.1.16" 785 + source = "registry+https://github.com/rust-lang/crates.io-index" 786 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 787 + dependencies = [ 788 + "data-encoding", 789 + "syn 2.0.105", 266 790 ] 267 791 268 792 [[package]] ··· 277 801 ] 278 802 279 803 [[package]] 804 + name = "deranged" 805 + version = "0.5.5" 806 + source = "registry+https://github.com/rust-lang/crates.io-index" 807 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 808 + dependencies = [ 809 + "powerfmt", 810 + "serde_core", 811 + ] 812 + 813 + [[package]] 814 + name = "derive_builder" 815 + version = "0.20.2" 816 + source = "registry+https://github.com/rust-lang/crates.io-index" 817 + checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 818 + dependencies = [ 819 + "derive_builder_macro", 820 + ] 821 + 822 + [[package]] 823 + name = "derive_builder_core" 824 + version = "0.20.2" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 827 + dependencies = [ 828 + "darling 0.20.11", 829 + "proc-macro2", 830 + "quote", 831 + "syn 2.0.105", 832 + ] 833 + 834 + [[package]] 835 + name = "derive_builder_macro" 836 + version = "0.20.2" 837 + source = "registry+https://github.com/rust-lang/crates.io-index" 838 + checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 839 + dependencies = [ 840 + "derive_builder_core", 841 + "syn 2.0.105", 842 + ] 843 + 844 + [[package]] 280 845 name = "digest" 281 846 version = "0.10.7" 282 847 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 306 871 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 307 872 308 873 [[package]] 874 + name = "dunce" 875 + version = "1.0.5" 876 + source = "registry+https://github.com/rust-lang/crates.io-index" 877 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 878 + 879 + [[package]] 880 + name = "dyn-clone" 881 + version = "1.0.20" 882 + source = "registry+https://github.com/rust-lang/crates.io-index" 883 + checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 884 + 885 + [[package]] 886 + name = "ecdsa" 887 + version = "0.16.9" 888 + source = "registry+https://github.com/rust-lang/crates.io-index" 889 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 890 + dependencies = [ 891 + "der", 892 + "digest", 893 + "elliptic-curve", 894 + "rfc6979", 895 + "signature", 896 + "spki", 897 + ] 898 + 899 + [[package]] 309 900 name = "either" 310 901 version = "1.15.0" 311 902 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 315 906 ] 316 907 317 908 [[package]] 909 + name = "elliptic-curve" 910 + version = "0.13.8" 911 + source = "registry+https://github.com/rust-lang/crates.io-index" 912 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 913 + dependencies = [ 914 + "base16ct", 915 + "crypto-bigint", 916 + "digest", 917 + "ff", 918 + "generic-array", 919 + "group", 920 + "pem-rfc7468", 921 + "pkcs8", 922 + "rand_core 0.6.4", 923 + "sec1", 924 + "subtle", 925 + "zeroize", 926 + ] 927 + 928 + [[package]] 929 + name = "email-encoding" 930 + version = "0.4.1" 931 + source = "registry+https://github.com/rust-lang/crates.io-index" 932 + checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" 933 + dependencies = [ 934 + "base64", 935 + "memchr", 936 + ] 937 + 938 + [[package]] 939 + name = "email_address" 940 + version = "0.2.9" 941 + source = "registry+https://github.com/rust-lang/crates.io-index" 942 + checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 943 + 944 + [[package]] 945 + name = "encoding_rs" 946 + version = "0.8.35" 947 + source = "registry+https://github.com/rust-lang/crates.io-index" 948 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 949 + dependencies = [ 950 + "cfg-if", 951 + ] 952 + 953 + [[package]] 318 954 name = "equivalent" 319 955 version = "1.0.2" 320 956 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 327 963 checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 328 964 dependencies = [ 329 965 "libc", 330 - "windows-sys 0.60.2", 966 + "windows-sys 0.59.0", 331 967 ] 332 968 333 969 [[package]] ··· 343 979 344 980 [[package]] 345 981 name = "event-listener" 346 - version = "2.5.3" 982 + version = "5.4.1" 347 983 source = "registry+https://github.com/rust-lang/crates.io-index" 348 - checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 984 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 985 + dependencies = [ 986 + "concurrent-queue", 987 + "parking", 988 + "pin-project-lite", 989 + ] 349 990 350 991 [[package]] 351 992 name = "fastrand" ··· 354 995 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 355 996 356 997 [[package]] 998 + name = "ff" 999 + version = "0.13.1" 1000 + source = "registry+https://github.com/rust-lang/crates.io-index" 1001 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1002 + dependencies = [ 1003 + "rand_core 0.6.4", 1004 + "subtle", 1005 + ] 1006 + 1007 + [[package]] 1008 + name = "flate2" 1009 + version = "1.1.5" 1010 + source = "registry+https://github.com/rust-lang/crates.io-index" 1011 + checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 1012 + dependencies = [ 1013 + "crc32fast", 1014 + "miniz_oxide", 1015 + ] 1016 + 1017 + [[package]] 357 1018 name = "flume" 358 1019 version = "0.11.1" 359 1020 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 371 1032 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 372 1033 373 1034 [[package]] 1035 + name = "foldhash" 1036 + version = "0.1.5" 1037 + source = "registry+https://github.com/rust-lang/crates.io-index" 1038 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1039 + 1040 + [[package]] 1041 + name = "foreign-types" 1042 + version = "0.3.2" 1043 + source = "registry+https://github.com/rust-lang/crates.io-index" 1044 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 1045 + dependencies = [ 1046 + "foreign-types-shared", 1047 + ] 1048 + 1049 + [[package]] 1050 + name = "foreign-types-shared" 1051 + version = "0.1.1" 1052 + source = "registry+https://github.com/rust-lang/crates.io-index" 1053 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 1054 + 1055 + [[package]] 374 1056 name = "form_urlencoded" 375 1057 version = "1.2.1" 376 1058 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 378 1060 dependencies = [ 379 1061 "percent-encoding", 380 1062 ] 1063 + 1064 + [[package]] 1065 + name = "forwarded-header-value" 1066 + version = "0.1.1" 1067 + source = "registry+https://github.com/rust-lang/crates.io-index" 1068 + checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" 1069 + dependencies = [ 1070 + "nonempty", 1071 + "thiserror 1.0.69", 1072 + ] 1073 + 1074 + [[package]] 1075 + name = "fs_extra" 1076 + version = "1.3.0" 1077 + source = "registry+https://github.com/rust-lang/crates.io-index" 1078 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 381 1079 382 1080 [[package]] 383 1081 name = "futures-channel" ··· 424 1122 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 425 1123 426 1124 [[package]] 1125 + name = "futures-macro" 1126 + version = "0.3.31" 1127 + source = "registry+https://github.com/rust-lang/crates.io-index" 1128 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 1129 + dependencies = [ 1130 + "proc-macro2", 1131 + "quote", 1132 + "syn 2.0.105", 1133 + ] 1134 + 1135 + [[package]] 427 1136 name = "futures-sink" 428 1137 version = "0.3.31" 429 1138 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 436 1145 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 437 1146 438 1147 [[package]] 1148 + name = "futures-timer" 1149 + version = "3.0.3" 1150 + source = "registry+https://github.com/rust-lang/crates.io-index" 1151 + checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 1152 + 1153 + [[package]] 439 1154 name = "futures-util" 440 1155 version = "0.3.31" 441 1156 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 443 1158 dependencies = [ 444 1159 "futures-core", 445 1160 "futures-io", 1161 + "futures-macro", 446 1162 "futures-sink", 447 1163 "futures-task", 448 1164 "memchr", ··· 459 1175 dependencies = [ 460 1176 "typenum", 461 1177 "version_check", 1178 + "zeroize", 462 1179 ] 463 1180 464 1181 [[package]] ··· 468 1185 checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 469 1186 dependencies = [ 470 1187 "cfg-if", 1188 + "js-sys", 471 1189 "libc", 472 - "wasi 0.11.1+wasi-snapshot-preview1", 1190 + "wasi", 1191 + "wasm-bindgen", 473 1192 ] 474 1193 475 1194 [[package]] 476 1195 name = "getrandom" 477 - version = "0.3.3" 1196 + version = "0.3.4" 478 1197 source = "registry+https://github.com/rust-lang/crates.io-index" 479 - checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1198 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 480 1199 dependencies = [ 481 1200 "cfg-if", 1201 + "js-sys", 482 1202 "libc", 483 1203 "r-efi", 484 - "wasi 0.14.2+wasi-0.2.4", 1204 + "wasip2", 1205 + "wasm-bindgen", 485 1206 ] 486 1207 487 1208 [[package]] ··· 491 1212 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 492 1213 493 1214 [[package]] 1215 + name = "glob" 1216 + version = "0.3.3" 1217 + source = "registry+https://github.com/rust-lang/crates.io-index" 1218 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 1219 + 1220 + [[package]] 1221 + name = "globset" 1222 + version = "0.4.16" 1223 + source = "registry+https://github.com/rust-lang/crates.io-index" 1224 + checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" 1225 + dependencies = [ 1226 + "aho-corasick", 1227 + "bstr", 1228 + "log", 1229 + "regex-automata 0.4.13", 1230 + "regex-syntax 0.8.5", 1231 + ] 1232 + 1233 + [[package]] 1234 + name = "governor" 1235 + version = "0.10.1" 1236 + source = "registry+https://github.com/rust-lang/crates.io-index" 1237 + checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" 1238 + dependencies = [ 1239 + "cfg-if", 1240 + "dashmap", 1241 + "futures-sink", 1242 + "futures-timer", 1243 + "futures-util", 1244 + "getrandom 0.3.4", 1245 + "hashbrown 0.15.5", 1246 + "nonzero_ext", 1247 + "parking_lot", 1248 + "portable-atomic", 1249 + "quanta", 1250 + "rand 0.9.2", 1251 + "smallvec", 1252 + "spinning_top", 1253 + "web-time", 1254 + ] 1255 + 1256 + [[package]] 1257 + name = "group" 1258 + version = "0.13.0" 1259 + source = "registry+https://github.com/rust-lang/crates.io-index" 1260 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1261 + dependencies = [ 1262 + "ff", 1263 + "rand_core 0.6.4", 1264 + "subtle", 1265 + ] 1266 + 1267 + [[package]] 1268 + name = "h2" 1269 + version = "0.4.12" 1270 + source = "registry+https://github.com/rust-lang/crates.io-index" 1271 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 1272 + dependencies = [ 1273 + "atomic-waker", 1274 + "bytes", 1275 + "fnv", 1276 + "futures-core", 1277 + "futures-sink", 1278 + "http", 1279 + "indexmap 2.10.0", 1280 + "slab", 1281 + "tokio", 1282 + "tokio-util", 1283 + "tracing", 1284 + ] 1285 + 1286 + [[package]] 1287 + name = "half" 1288 + version = "2.6.0" 1289 + source = "registry+https://github.com/rust-lang/crates.io-index" 1290 + checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 1291 + dependencies = [ 1292 + "cfg-if", 1293 + "crunchy", 1294 + ] 1295 + 1296 + [[package]] 1297 + name = "handlebars" 1298 + version = "6.3.2" 1299 + source = "registry+https://github.com/rust-lang/crates.io-index" 1300 + checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" 1301 + dependencies = [ 1302 + "derive_builder", 1303 + "log", 1304 + "num-order", 1305 + "pest", 1306 + "pest_derive", 1307 + "rust-embed", 1308 + "serde", 1309 + "serde_json", 1310 + "thiserror 2.0.14", 1311 + ] 1312 + 1313 + [[package]] 1314 + name = "hashbrown" 1315 + version = "0.12.3" 1316 + source = "registry+https://github.com/rust-lang/crates.io-index" 1317 + checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 1318 + 1319 + [[package]] 494 1320 name = "hashbrown" 495 1321 version = "0.14.5" 496 1322 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 505 1331 version = "0.15.5" 506 1332 source = "registry+https://github.com/rust-lang/crates.io-index" 507 1333 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1334 + dependencies = [ 1335 + "allocator-api2", 1336 + "equivalent", 1337 + "foldhash", 1338 + ] 508 1339 509 1340 [[package]] 510 1341 name = "hashlink" 511 - version = "0.8.4" 1342 + version = "0.10.0" 512 1343 source = "registry+https://github.com/rust-lang/crates.io-index" 513 - checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 1344 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 514 1345 dependencies = [ 515 - "hashbrown 0.14.5", 1346 + "hashbrown 0.15.5", 516 1347 ] 517 1348 518 1349 [[package]] ··· 520 1351 version = "0.4.1" 521 1352 source = "registry+https://github.com/rust-lang/crates.io-index" 522 1353 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 523 - dependencies = [ 524 - "unicode-segmentation", 525 - ] 1354 + 1355 + [[package]] 1356 + name = "heck" 1357 + version = "0.5.0" 1358 + source = "registry+https://github.com/rust-lang/crates.io-index" 1359 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 526 1360 527 1361 [[package]] 528 1362 name = "hex" 529 1363 version = "0.4.3" 530 1364 source = "registry+https://github.com/rust-lang/crates.io-index" 531 1365 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1366 + 1367 + [[package]] 1368 + name = "hex_fmt" 1369 + version = "0.3.0" 1370 + source = "registry+https://github.com/rust-lang/crates.io-index" 1371 + checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" 532 1372 533 1373 [[package]] 534 1374 name = "hkdf" ··· 558 1398 ] 559 1399 560 1400 [[package]] 1401 + name = "html-escape" 1402 + version = "0.2.13" 1403 + source = "registry+https://github.com/rust-lang/crates.io-index" 1404 + checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" 1405 + dependencies = [ 1406 + "utf8-width", 1407 + ] 1408 + 1409 + [[package]] 561 1410 name = "http" 562 1411 version = "1.3.1" 563 1412 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 612 1461 "bytes", 613 1462 "futures-channel", 614 1463 "futures-util", 1464 + "h2", 615 1465 "http", 616 1466 "http-body", 617 1467 "httparse", ··· 620 1470 "pin-project-lite", 621 1471 "smallvec", 622 1472 "tokio", 1473 + "want", 1474 + ] 1475 + 1476 + [[package]] 1477 + name = "hyper-rustls" 1478 + version = "0.27.7" 1479 + source = "registry+https://github.com/rust-lang/crates.io-index" 1480 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1481 + dependencies = [ 1482 + "http", 1483 + "hyper", 1484 + "hyper-util", 1485 + "rustls", 1486 + "rustls-pki-types", 1487 + "tokio", 1488 + "tokio-rustls", 1489 + "tower-service", 1490 + "webpki-roots 1.0.2", 1491 + ] 1492 + 1493 + [[package]] 1494 + name = "hyper-timeout" 1495 + version = "0.5.2" 1496 + source = "registry+https://github.com/rust-lang/crates.io-index" 1497 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 1498 + dependencies = [ 1499 + "hyper", 1500 + "hyper-util", 1501 + "pin-project-lite", 1502 + "tokio", 1503 + "tower-service", 623 1504 ] 624 1505 625 1506 [[package]] ··· 628 1509 source = "registry+https://github.com/rust-lang/crates.io-index" 629 1510 checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 630 1511 dependencies = [ 1512 + "base64", 631 1513 "bytes", 1514 + "futures-channel", 632 1515 "futures-core", 1516 + "futures-util", 633 1517 "http", 634 1518 "http-body", 635 1519 "hyper", 1520 + "ipnet", 1521 + "libc", 1522 + "percent-encoding", 636 1523 "pin-project-lite", 1524 + "socket2", 1525 + "system-configuration", 637 1526 "tokio", 638 1527 "tower-service", 1528 + "tracing", 1529 + "windows-registry", 1530 + ] 1531 + 1532 + [[package]] 1533 + name = "iana-time-zone" 1534 + version = "0.1.63" 1535 + source = "registry+https://github.com/rust-lang/crates.io-index" 1536 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1537 + dependencies = [ 1538 + "android_system_properties", 1539 + "core-foundation-sys", 1540 + "iana-time-zone-haiku", 1541 + "js-sys", 1542 + "log", 1543 + "wasm-bindgen", 1544 + "windows-core", 1545 + ] 1546 + 1547 + [[package]] 1548 + name = "iana-time-zone-haiku" 1549 + version = "0.1.2" 1550 + source = "registry+https://github.com/rust-lang/crates.io-index" 1551 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1552 + dependencies = [ 1553 + "cc", 639 1554 ] 640 1555 641 1556 [[package]] ··· 725 1640 ] 726 1641 727 1642 [[package]] 1643 + name = "ident_case" 1644 + version = "1.0.1" 1645 + source = "registry+https://github.com/rust-lang/crates.io-index" 1646 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1647 + 1648 + [[package]] 728 1649 name = "idna" 729 1650 version = "1.0.3" 730 1651 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 747 1668 748 1669 [[package]] 749 1670 name = "indexmap" 1671 + version = "1.9.3" 1672 + source = "registry+https://github.com/rust-lang/crates.io-index" 1673 + checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1674 + dependencies = [ 1675 + "autocfg", 1676 + "hashbrown 0.12.3", 1677 + "serde", 1678 + ] 1679 + 1680 + [[package]] 1681 + name = "indexmap" 750 1682 version = "2.10.0" 751 1683 source = "registry+https://github.com/rust-lang/crates.io-index" 752 1684 checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 753 1685 dependencies = [ 754 1686 "equivalent", 755 1687 "hashbrown 0.15.5", 1688 + "serde", 1689 + ] 1690 + 1691 + [[package]] 1692 + name = "indoc" 1693 + version = "2.0.7" 1694 + source = "registry+https://github.com/rust-lang/crates.io-index" 1695 + checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" 1696 + dependencies = [ 1697 + "rustversion", 1698 + ] 1699 + 1700 + [[package]] 1701 + name = "inout" 1702 + version = "0.1.4" 1703 + source = "registry+https://github.com/rust-lang/crates.io-index" 1704 + checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 1705 + dependencies = [ 1706 + "generic-array", 1707 + ] 1708 + 1709 + [[package]] 1710 + name = "inventory" 1711 + version = "0.3.21" 1712 + source = "registry+https://github.com/rust-lang/crates.io-index" 1713 + checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" 1714 + dependencies = [ 1715 + "rustversion", 756 1716 ] 757 1717 758 1718 [[package]] ··· 767 1727 ] 768 1728 769 1729 [[package]] 1730 + name = "ipld-core" 1731 + version = "0.4.2" 1732 + source = "registry+https://github.com/rust-lang/crates.io-index" 1733 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1734 + dependencies = [ 1735 + "cid", 1736 + "serde", 1737 + "serde_bytes", 1738 + ] 1739 + 1740 + [[package]] 1741 + name = "ipnet" 1742 + version = "2.11.0" 1743 + source = "registry+https://github.com/rust-lang/crates.io-index" 1744 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1745 + 1746 + [[package]] 1747 + name = "iri-string" 1748 + version = "0.7.9" 1749 + source = "registry+https://github.com/rust-lang/crates.io-index" 1750 + checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 1751 + dependencies = [ 1752 + "memchr", 1753 + "serde", 1754 + ] 1755 + 1756 + [[package]] 1757 + name = "itertools" 1758 + version = "0.12.1" 1759 + source = "registry+https://github.com/rust-lang/crates.io-index" 1760 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 1761 + dependencies = [ 1762 + "either", 1763 + ] 1764 + 1765 + [[package]] 770 1766 name = "itoa" 771 1767 version = "1.0.15" 772 1768 source = "registry+https://github.com/rust-lang/crates.io-index" 773 1769 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 774 1770 775 1771 [[package]] 1772 + name = "jacquard-api" 1773 + version = "0.9.2" 1774 + source = "registry+https://github.com/rust-lang/crates.io-index" 1775 + checksum = "bbbfd6e2b10fa1731f4d4e40c8f791956b0d4f804fb3efef891afec903f20597" 1776 + dependencies = [ 1777 + "bon", 1778 + "bytes", 1779 + "jacquard-common", 1780 + "jacquard-derive", 1781 + "jacquard-lexicon", 1782 + "miette", 1783 + "rustversion", 1784 + "serde", 1785 + "serde_ipld_dagcbor", 1786 + "thiserror 2.0.14", 1787 + "unicode-segmentation", 1788 + ] 1789 + 1790 + [[package]] 1791 + name = "jacquard-common" 1792 + version = "0.9.2" 1793 + source = "registry+https://github.com/rust-lang/crates.io-index" 1794 + checksum = "df86cb117d9f1c2b0251ba67c3f0e3f963fd22abc6cf8de0e02a7fc846c288ca" 1795 + dependencies = [ 1796 + "base64", 1797 + "bon", 1798 + "bytes", 1799 + "chrono", 1800 + "cid", 1801 + "getrandom 0.2.16", 1802 + "getrandom 0.3.4", 1803 + "http", 1804 + "ipld-core", 1805 + "k256", 1806 + "langtag", 1807 + "miette", 1808 + "multibase", 1809 + "multihash", 1810 + "ouroboros", 1811 + "p256", 1812 + "rand 0.9.2", 1813 + "regex", 1814 + "regex-lite", 1815 + "reqwest", 1816 + "serde", 1817 + "serde_html_form", 1818 + "serde_ipld_dagcbor", 1819 + "serde_json", 1820 + "signature", 1821 + "smol_str", 1822 + "thiserror 2.0.14", 1823 + "tokio", 1824 + "tokio-util", 1825 + "trait-variant", 1826 + "url", 1827 + ] 1828 + 1829 + [[package]] 1830 + name = "jacquard-derive" 1831 + version = "0.9.2" 1832 + source = "registry+https://github.com/rust-lang/crates.io-index" 1833 + checksum = "42ca61a69dc7aa8fb2d7163416514ff7df5d79f2e8b22e269f4610afa85572fe" 1834 + dependencies = [ 1835 + "heck 0.5.0", 1836 + "jacquard-lexicon", 1837 + "proc-macro2", 1838 + "quote", 1839 + "syn 2.0.105", 1840 + ] 1841 + 1842 + [[package]] 1843 + name = "jacquard-identity" 1844 + version = "0.9.2" 1845 + source = "registry+https://github.com/rust-lang/crates.io-index" 1846 + checksum = "1ef714cacebfca486558a9f8e205daf466bfba0466c4d0c450fd6d0252400a53" 1847 + dependencies = [ 1848 + "bon", 1849 + "bytes", 1850 + "http", 1851 + "jacquard-api", 1852 + "jacquard-common", 1853 + "jacquard-lexicon", 1854 + "miette", 1855 + "percent-encoding", 1856 + "reqwest", 1857 + "serde", 1858 + "serde_html_form", 1859 + "serde_json", 1860 + "thiserror 2.0.14", 1861 + "tokio", 1862 + "trait-variant", 1863 + "url", 1864 + "urlencoding", 1865 + ] 1866 + 1867 + [[package]] 1868 + name = "jacquard-lexicon" 1869 + version = "0.9.2" 1870 + source = "registry+https://github.com/rust-lang/crates.io-index" 1871 + checksum = "de87f2c938faea1b1f1b32d5b9e0c870e7b5bb5efbf96e3692ae2d8f6b2beb7a" 1872 + dependencies = [ 1873 + "cid", 1874 + "dashmap", 1875 + "heck 0.5.0", 1876 + "inventory", 1877 + "jacquard-common", 1878 + "miette", 1879 + "multihash", 1880 + "prettyplease", 1881 + "proc-macro2", 1882 + "quote", 1883 + "serde", 1884 + "serde_ipld_dagcbor", 1885 + "serde_json", 1886 + "serde_repr", 1887 + "serde_with", 1888 + "sha2", 1889 + "syn 2.0.105", 1890 + "thiserror 2.0.14", 1891 + "unicode-segmentation", 1892 + ] 1893 + 1894 + [[package]] 1895 + name = "jobserver" 1896 + version = "0.1.33" 1897 + source = "registry+https://github.com/rust-lang/crates.io-index" 1898 + checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1899 + dependencies = [ 1900 + "getrandom 0.3.4", 1901 + "libc", 1902 + ] 1903 + 1904 + [[package]] 1905 + name = "josekit" 1906 + version = "0.10.3" 1907 + source = "registry+https://github.com/rust-lang/crates.io-index" 1908 + checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" 1909 + dependencies = [ 1910 + "anyhow", 1911 + "base64", 1912 + "flate2", 1913 + "openssl", 1914 + "regex", 1915 + "serde", 1916 + "serde_json", 1917 + "thiserror 2.0.14", 1918 + "time", 1919 + ] 1920 + 1921 + [[package]] 1922 + name = "js-sys" 1923 + version = "0.3.77" 1924 + source = "registry+https://github.com/rust-lang/crates.io-index" 1925 + checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1926 + dependencies = [ 1927 + "once_cell", 1928 + "wasm-bindgen", 1929 + ] 1930 + 1931 + [[package]] 1932 + name = "jwt-compact" 1933 + version = "0.8.0" 1934 + source = "registry+https://github.com/rust-lang/crates.io-index" 1935 + checksum = "25cb2458ca54de48ef237ac0d68d4e80e512ae81d6aeb9775f4c835da0d193d3" 1936 + dependencies = [ 1937 + "anyhow", 1938 + "base64ct", 1939 + "chrono", 1940 + "ciborium", 1941 + "hmac", 1942 + "lazy_static", 1943 + "rand_core 0.6.4", 1944 + "secp256k1", 1945 + "serde", 1946 + "serde_json", 1947 + "sha2", 1948 + "smallvec", 1949 + "subtle", 1950 + "zeroize", 1951 + ] 1952 + 1953 + [[package]] 1954 + name = "k256" 1955 + version = "0.13.4" 1956 + source = "registry+https://github.com/rust-lang/crates.io-index" 1957 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1958 + dependencies = [ 1959 + "cfg-if", 1960 + "ecdsa", 1961 + "elliptic-curve", 1962 + "sha2", 1963 + ] 1964 + 1965 + [[package]] 1966 + name = "langtag" 1967 + version = "0.4.0" 1968 + source = "registry+https://github.com/rust-lang/crates.io-index" 1969 + checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" 1970 + dependencies = [ 1971 + "serde", 1972 + "static-regular-grammar", 1973 + "thiserror 1.0.69", 1974 + ] 1975 + 1976 + [[package]] 776 1977 name = "lazy_static" 777 1978 version = "1.5.0" 778 1979 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 782 1983 ] 783 1984 784 1985 [[package]] 1986 + name = "lazycell" 1987 + version = "1.3.0" 1988 + source = "registry+https://github.com/rust-lang/crates.io-index" 1989 + checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 1990 + 1991 + [[package]] 1992 + name = "lettre" 1993 + version = "0.11.18" 1994 + source = "registry+https://github.com/rust-lang/crates.io-index" 1995 + checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" 1996 + dependencies = [ 1997 + "async-trait", 1998 + "base64", 1999 + "chumsky", 2000 + "email-encoding", 2001 + "email_address", 2002 + "fastrand", 2003 + "futures-io", 2004 + "futures-util", 2005 + "httpdate", 2006 + "idna", 2007 + "mime", 2008 + "nom 8.0.0", 2009 + "percent-encoding", 2010 + "quoted_printable", 2011 + "rustls", 2012 + "socket2", 2013 + "tokio", 2014 + "tokio-rustls", 2015 + "url", 2016 + "webpki-roots 1.0.2", 2017 + ] 2018 + 2019 + [[package]] 785 2020 name = "libc" 786 2021 version = "0.2.175" 787 2022 source = "registry+https://github.com/rust-lang/crates.io-index" 788 2023 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 789 2024 790 2025 [[package]] 2026 + name = "libloading" 2027 + version = "0.8.8" 2028 + source = "registry+https://github.com/rust-lang/crates.io-index" 2029 + checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 2030 + dependencies = [ 2031 + "cfg-if", 2032 + "windows-targets 0.52.6", 2033 + ] 2034 + 2035 + [[package]] 791 2036 name = "libm" 792 2037 version = "0.2.15" 793 2038 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 806 2051 807 2052 [[package]] 808 2053 name = "libsqlite3-sys" 809 - version = "0.27.0" 2054 + version = "0.30.1" 810 2055 source = "registry+https://github.com/rust-lang/crates.io-index" 811 - checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" 2056 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 812 2057 dependencies = [ 813 2058 "cc", 814 2059 "pkg-config", ··· 817 2062 818 2063 [[package]] 819 2064 name = "linux-raw-sys" 820 - version = "0.9.4" 2065 + version = "0.4.15" 821 2066 source = "registry+https://github.com/rust-lang/crates.io-index" 822 - checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 2067 + checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 823 2068 824 2069 [[package]] 825 2070 name = "litemap" ··· 844 2089 checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 845 2090 846 2091 [[package]] 2092 + name = "lru-slab" 2093 + version = "0.1.2" 2094 + source = "registry+https://github.com/rust-lang/crates.io-index" 2095 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2096 + 2097 + [[package]] 2098 + name = "match-lookup" 2099 + version = "0.1.1" 2100 + source = "registry+https://github.com/rust-lang/crates.io-index" 2101 + checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 2102 + dependencies = [ 2103 + "proc-macro2", 2104 + "quote", 2105 + "syn 1.0.109", 2106 + ] 2107 + 2108 + [[package]] 847 2109 name = "matchers" 848 2110 version = "0.1.0" 849 2111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 854 2116 855 2117 [[package]] 856 2118 name = "matchit" 857 - version = "0.7.3" 2119 + version = "0.8.4" 858 2120 source = "registry+https://github.com/rust-lang/crates.io-index" 859 - checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 2121 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 860 2122 861 2123 [[package]] 862 2124 name = "md-5" ··· 875 2137 checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 876 2138 877 2139 [[package]] 2140 + name = "miette" 2141 + version = "7.6.0" 2142 + source = "registry+https://github.com/rust-lang/crates.io-index" 2143 + checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 2144 + dependencies = [ 2145 + "cfg-if", 2146 + "miette-derive", 2147 + "unicode-width", 2148 + ] 2149 + 2150 + [[package]] 2151 + name = "miette-derive" 2152 + version = "7.6.0" 2153 + source = "registry+https://github.com/rust-lang/crates.io-index" 2154 + checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" 2155 + dependencies = [ 2156 + "proc-macro2", 2157 + "quote", 2158 + "syn 2.0.105", 2159 + ] 2160 + 2161 + [[package]] 878 2162 name = "mime" 879 2163 version = "0.3.17" 880 2164 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 893 2177 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 894 2178 dependencies = [ 895 2179 "adler2", 2180 + "simd-adler32", 896 2181 ] 897 2182 898 2183 [[package]] ··· 902 2187 checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 903 2188 dependencies = [ 904 2189 "libc", 905 - "wasi 0.11.1+wasi-snapshot-preview1", 2190 + "wasi", 906 2191 "windows-sys 0.59.0", 907 2192 ] 908 2193 909 2194 [[package]] 2195 + name = "multibase" 2196 + version = "0.9.2" 2197 + source = "registry+https://github.com/rust-lang/crates.io-index" 2198 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 2199 + dependencies = [ 2200 + "base-x", 2201 + "base256emoji", 2202 + "data-encoding", 2203 + "data-encoding-macro", 2204 + ] 2205 + 2206 + [[package]] 2207 + name = "multihash" 2208 + version = "0.19.3" 2209 + source = "registry+https://github.com/rust-lang/crates.io-index" 2210 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 2211 + dependencies = [ 2212 + "core2", 2213 + "serde", 2214 + "unsigned-varint", 2215 + ] 2216 + 2217 + [[package]] 910 2218 name = "nom" 911 2219 version = "7.1.3" 912 2220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 917 2225 ] 918 2226 919 2227 [[package]] 2228 + name = "nom" 2229 + version = "8.0.0" 2230 + source = "registry+https://github.com/rust-lang/crates.io-index" 2231 + checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 2232 + dependencies = [ 2233 + "memchr", 2234 + ] 2235 + 2236 + [[package]] 2237 + name = "nonempty" 2238 + version = "0.7.0" 2239 + source = "registry+https://github.com/rust-lang/crates.io-index" 2240 + checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" 2241 + 2242 + [[package]] 2243 + name = "nonzero_ext" 2244 + version = "0.3.0" 2245 + source = "registry+https://github.com/rust-lang/crates.io-index" 2246 + checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" 2247 + 2248 + [[package]] 920 2249 name = "nu-ansi-term" 921 2250 version = "0.46.0" 922 2251 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 938 2267 "num-integer", 939 2268 "num-iter", 940 2269 "num-traits", 941 - "rand", 2270 + "rand 0.8.5", 942 2271 "smallvec", 943 2272 "zeroize", 944 2273 ] 945 2274 946 2275 [[package]] 2276 + name = "num-conv" 2277 + version = "0.1.0" 2278 + source = "registry+https://github.com/rust-lang/crates.io-index" 2279 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 2280 + 2281 + [[package]] 947 2282 name = "num-integer" 948 2283 version = "0.1.46" 949 2284 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 964 2299 ] 965 2300 966 2301 [[package]] 2302 + name = "num-modular" 2303 + version = "0.6.1" 2304 + source = "registry+https://github.com/rust-lang/crates.io-index" 2305 + checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" 2306 + 2307 + [[package]] 2308 + name = "num-order" 2309 + version = "1.2.0" 2310 + source = "registry+https://github.com/rust-lang/crates.io-index" 2311 + checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" 2312 + dependencies = [ 2313 + "num-modular", 2314 + ] 2315 + 2316 + [[package]] 967 2317 name = "num-traits" 968 2318 version = "0.2.19" 969 2319 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 989 2339 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 990 2340 991 2341 [[package]] 2342 + name = "openssl" 2343 + version = "0.10.75" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 2346 + dependencies = [ 2347 + "bitflags", 2348 + "cfg-if", 2349 + "foreign-types", 2350 + "libc", 2351 + "once_cell", 2352 + "openssl-macros", 2353 + "openssl-sys", 2354 + ] 2355 + 2356 + [[package]] 2357 + name = "openssl-macros" 2358 + version = "0.1.1" 2359 + source = "registry+https://github.com/rust-lang/crates.io-index" 2360 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 2361 + dependencies = [ 2362 + "proc-macro2", 2363 + "quote", 2364 + "syn 2.0.105", 2365 + ] 2366 + 2367 + [[package]] 2368 + name = "openssl-sys" 2369 + version = "0.9.111" 2370 + source = "registry+https://github.com/rust-lang/crates.io-index" 2371 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 2372 + dependencies = [ 2373 + "cc", 2374 + "libc", 2375 + "pkg-config", 2376 + "vcpkg", 2377 + ] 2378 + 2379 + [[package]] 2380 + name = "ouroboros" 2381 + version = "0.18.5" 2382 + source = "registry+https://github.com/rust-lang/crates.io-index" 2383 + checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" 2384 + dependencies = [ 2385 + "aliasable", 2386 + "ouroboros_macro", 2387 + "static_assertions", 2388 + ] 2389 + 2390 + [[package]] 2391 + name = "ouroboros_macro" 2392 + version = "0.18.5" 2393 + source = "registry+https://github.com/rust-lang/crates.io-index" 2394 + checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" 2395 + dependencies = [ 2396 + "heck 0.4.1", 2397 + "proc-macro2", 2398 + "proc-macro2-diagnostics", 2399 + "quote", 2400 + "syn 2.0.105", 2401 + ] 2402 + 2403 + [[package]] 992 2404 name = "overload" 993 2405 version = "0.1.1" 994 2406 source = "registry+https://github.com/rust-lang/crates.io-index" 995 2407 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 996 2408 997 2409 [[package]] 2410 + name = "p256" 2411 + version = "0.13.2" 2412 + source = "registry+https://github.com/rust-lang/crates.io-index" 2413 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2414 + dependencies = [ 2415 + "ecdsa", 2416 + "elliptic-curve", 2417 + "primeorder", 2418 + "sha2", 2419 + ] 2420 + 2421 + [[package]] 2422 + name = "parking" 2423 + version = "2.2.1" 2424 + source = "registry+https://github.com/rust-lang/crates.io-index" 2425 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2426 + 2427 + [[package]] 998 2428 name = "parking_lot" 999 2429 version = "0.12.4" 1000 2430 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1018 2448 ] 1019 2449 1020 2450 [[package]] 1021 - name = "paste" 1022 - version = "1.0.15" 2451 + name = "password-hash" 2452 + version = "0.5.0" 1023 2453 source = "registry+https://github.com/rust-lang/crates.io-index" 1024 - checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 2454 + checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" 2455 + dependencies = [ 2456 + "base64ct", 2457 + "rand_core 0.6.4", 2458 + "subtle", 2459 + ] 1025 2460 1026 2461 [[package]] 1027 - name = "pds_bells_and_whistles" 1028 - version = "0.1.0" 2462 + name = "pbkdf2" 2463 + version = "0.12.2" 2464 + source = "registry+https://github.com/rust-lang/crates.io-index" 2465 + checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 2466 + dependencies = [ 2467 + "digest", 2468 + "hmac", 2469 + ] 2470 + 2471 + [[package]] 2472 + name = "pds_gatekeeper" 2473 + version = "0.1.2" 1029 2474 dependencies = [ 2475 + "anyhow", 2476 + "aws-lc-rs", 1030 2477 "axum", 2478 + "axum-template", 2479 + "chrono", 1031 2480 "dotenvy", 2481 + "handlebars", 2482 + "hex", 2483 + "html-escape", 2484 + "hyper-util", 2485 + "jacquard-common", 2486 + "jacquard-identity", 2487 + "josekit", 2488 + "jwt-compact", 2489 + "lettre", 2490 + "multibase", 2491 + "rand 0.9.2", 2492 + "reqwest", 2493 + "rust-embed", 2494 + "rustls", 2495 + "scrypt", 1032 2496 "serde", 1033 2497 "serde_json", 2498 + "sha2", 1034 2499 "sqlx", 1035 2500 "tokio", 2501 + "tower-http", 2502 + "tower_governor", 1036 2503 "tracing", 1037 2504 "tracing-subscriber", 2505 + "urlencoding", 1038 2506 ] 1039 2507 1040 2508 [[package]] ··· 1053 2521 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1054 2522 1055 2523 [[package]] 2524 + name = "pest" 2525 + version = "2.8.1" 2526 + source = "registry+https://github.com/rust-lang/crates.io-index" 2527 + checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 2528 + dependencies = [ 2529 + "memchr", 2530 + "thiserror 2.0.14", 2531 + "ucd-trie", 2532 + ] 2533 + 2534 + [[package]] 2535 + name = "pest_derive" 2536 + version = "2.8.1" 2537 + source = "registry+https://github.com/rust-lang/crates.io-index" 2538 + checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 2539 + dependencies = [ 2540 + "pest", 2541 + "pest_generator", 2542 + ] 2543 + 2544 + [[package]] 2545 + name = "pest_generator" 2546 + version = "2.8.1" 2547 + source = "registry+https://github.com/rust-lang/crates.io-index" 2548 + checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 2549 + dependencies = [ 2550 + "pest", 2551 + "pest_meta", 2552 + "proc-macro2", 2553 + "quote", 2554 + "syn 2.0.105", 2555 + ] 2556 + 2557 + [[package]] 2558 + name = "pest_meta" 2559 + version = "2.8.1" 2560 + source = "registry+https://github.com/rust-lang/crates.io-index" 2561 + checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 2562 + dependencies = [ 2563 + "pest", 2564 + "sha2", 2565 + ] 2566 + 2567 + [[package]] 2568 + name = "pin-project" 2569 + version = "1.1.10" 2570 + source = "registry+https://github.com/rust-lang/crates.io-index" 2571 + checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 2572 + dependencies = [ 2573 + "pin-project-internal", 2574 + ] 2575 + 2576 + [[package]] 2577 + name = "pin-project-internal" 2578 + version = "1.1.10" 2579 + source = "registry+https://github.com/rust-lang/crates.io-index" 2580 + checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 2581 + dependencies = [ 2582 + "proc-macro2", 2583 + "quote", 2584 + "syn 2.0.105", 2585 + ] 2586 + 2587 + [[package]] 1056 2588 name = "pin-project-lite" 1057 2589 version = "0.2.16" 1058 2590 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1092 2624 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1093 2625 1094 2626 [[package]] 2627 + name = "portable-atomic" 2628 + version = "1.11.1" 2629 + source = "registry+https://github.com/rust-lang/crates.io-index" 2630 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2631 + 2632 + [[package]] 1095 2633 name = "potential_utf" 1096 2634 version = "0.1.2" 1097 2635 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1101 2639 ] 1102 2640 1103 2641 [[package]] 2642 + name = "powerfmt" 2643 + version = "0.2.0" 2644 + source = "registry+https://github.com/rust-lang/crates.io-index" 2645 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 2646 + 2647 + [[package]] 1104 2648 name = "ppv-lite86" 1105 2649 version = "0.2.21" 1106 2650 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1110 2654 ] 1111 2655 1112 2656 [[package]] 2657 + name = "prettyplease" 2658 + version = "0.2.35" 2659 + source = "registry+https://github.com/rust-lang/crates.io-index" 2660 + checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" 2661 + dependencies = [ 2662 + "proc-macro2", 2663 + "syn 2.0.105", 2664 + ] 2665 + 2666 + [[package]] 2667 + name = "primeorder" 2668 + version = "0.13.6" 2669 + source = "registry+https://github.com/rust-lang/crates.io-index" 2670 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2671 + dependencies = [ 2672 + "elliptic-curve", 2673 + ] 2674 + 2675 + [[package]] 2676 + name = "proc-macro-error" 2677 + version = "1.0.4" 2678 + source = "registry+https://github.com/rust-lang/crates.io-index" 2679 + checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 2680 + dependencies = [ 2681 + "proc-macro-error-attr", 2682 + "proc-macro2", 2683 + "quote", 2684 + "syn 1.0.109", 2685 + "version_check", 2686 + ] 2687 + 2688 + [[package]] 2689 + name = "proc-macro-error-attr" 2690 + version = "1.0.4" 2691 + source = "registry+https://github.com/rust-lang/crates.io-index" 2692 + checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 2693 + dependencies = [ 2694 + "proc-macro2", 2695 + "quote", 2696 + "version_check", 2697 + ] 2698 + 2699 + [[package]] 1113 2700 name = "proc-macro2" 1114 2701 version = "1.0.97" 1115 2702 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1119 2706 ] 1120 2707 1121 2708 [[package]] 2709 + name = "proc-macro2-diagnostics" 2710 + version = "0.10.1" 2711 + source = "registry+https://github.com/rust-lang/crates.io-index" 2712 + checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" 2713 + dependencies = [ 2714 + "proc-macro2", 2715 + "quote", 2716 + "syn 2.0.105", 2717 + "version_check", 2718 + "yansi", 2719 + ] 2720 + 2721 + [[package]] 2722 + name = "psm" 2723 + version = "0.1.26" 2724 + source = "registry+https://github.com/rust-lang/crates.io-index" 2725 + checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" 2726 + dependencies = [ 2727 + "cc", 2728 + ] 2729 + 2730 + [[package]] 2731 + name = "quanta" 2732 + version = "0.12.6" 2733 + source = "registry+https://github.com/rust-lang/crates.io-index" 2734 + checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" 2735 + dependencies = [ 2736 + "crossbeam-utils", 2737 + "libc", 2738 + "once_cell", 2739 + "raw-cpuid", 2740 + "wasi", 2741 + "web-sys", 2742 + "winapi", 2743 + ] 2744 + 2745 + [[package]] 2746 + name = "quinn" 2747 + version = "0.11.9" 2748 + source = "registry+https://github.com/rust-lang/crates.io-index" 2749 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 2750 + dependencies = [ 2751 + "bytes", 2752 + "cfg_aliases", 2753 + "pin-project-lite", 2754 + "quinn-proto", 2755 + "quinn-udp", 2756 + "rustc-hash 2.1.1", 2757 + "rustls", 2758 + "socket2", 2759 + "thiserror 2.0.14", 2760 + "tokio", 2761 + "tracing", 2762 + "web-time", 2763 + ] 2764 + 2765 + [[package]] 2766 + name = "quinn-proto" 2767 + version = "0.11.13" 2768 + source = "registry+https://github.com/rust-lang/crates.io-index" 2769 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 2770 + dependencies = [ 2771 + "bytes", 2772 + "getrandom 0.3.4", 2773 + "lru-slab", 2774 + "rand 0.9.2", 2775 + "ring", 2776 + "rustc-hash 2.1.1", 2777 + "rustls", 2778 + "rustls-pki-types", 2779 + "slab", 2780 + "thiserror 2.0.14", 2781 + "tinyvec", 2782 + "tracing", 2783 + "web-time", 2784 + ] 2785 + 2786 + [[package]] 2787 + name = "quinn-udp" 2788 + version = "0.5.14" 2789 + source = "registry+https://github.com/rust-lang/crates.io-index" 2790 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 2791 + dependencies = [ 2792 + "cfg_aliases", 2793 + "libc", 2794 + "once_cell", 2795 + "socket2", 2796 + "tracing", 2797 + "windows-sys 0.59.0", 2798 + ] 2799 + 2800 + [[package]] 1122 2801 name = "quote" 1123 2802 version = "1.0.40" 1124 2803 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1128 2807 ] 1129 2808 1130 2809 [[package]] 2810 + name = "quoted_printable" 2811 + version = "0.5.1" 2812 + source = "registry+https://github.com/rust-lang/crates.io-index" 2813 + checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" 2814 + 2815 + [[package]] 1131 2816 name = "r-efi" 1132 2817 version = "5.3.0" 1133 2818 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1140 2825 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1141 2826 dependencies = [ 1142 2827 "libc", 1143 - "rand_chacha", 1144 - "rand_core", 2828 + "rand_chacha 0.3.1", 2829 + "rand_core 0.6.4", 2830 + ] 2831 + 2832 + [[package]] 2833 + name = "rand" 2834 + version = "0.9.2" 2835 + source = "registry+https://github.com/rust-lang/crates.io-index" 2836 + checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 2837 + dependencies = [ 2838 + "rand_chacha 0.9.0", 2839 + "rand_core 0.9.3", 1145 2840 ] 1146 2841 1147 2842 [[package]] ··· 1151 2846 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1152 2847 dependencies = [ 1153 2848 "ppv-lite86", 1154 - "rand_core", 2849 + "rand_core 0.6.4", 2850 + ] 2851 + 2852 + [[package]] 2853 + name = "rand_chacha" 2854 + version = "0.9.0" 2855 + source = "registry+https://github.com/rust-lang/crates.io-index" 2856 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2857 + dependencies = [ 2858 + "ppv-lite86", 2859 + "rand_core 0.9.3", 1155 2860 ] 1156 2861 1157 2862 [[package]] ··· 1164 2869 ] 1165 2870 1166 2871 [[package]] 2872 + name = "rand_core" 2873 + version = "0.9.3" 2874 + source = "registry+https://github.com/rust-lang/crates.io-index" 2875 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2876 + dependencies = [ 2877 + "getrandom 0.3.4", 2878 + ] 2879 + 2880 + [[package]] 2881 + name = "range-traits" 2882 + version = "0.3.2" 2883 + source = "registry+https://github.com/rust-lang/crates.io-index" 2884 + checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 2885 + 2886 + [[package]] 2887 + name = "raw-cpuid" 2888 + version = "11.5.0" 2889 + source = "registry+https://github.com/rust-lang/crates.io-index" 2890 + checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" 2891 + dependencies = [ 2892 + "bitflags", 2893 + ] 2894 + 2895 + [[package]] 1167 2896 name = "redox_syscall" 1168 2897 version = "0.5.17" 1169 2898 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1173 2902 ] 1174 2903 1175 2904 [[package]] 2905 + name = "ref-cast" 2906 + version = "1.0.25" 2907 + source = "registry+https://github.com/rust-lang/crates.io-index" 2908 + checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" 2909 + dependencies = [ 2910 + "ref-cast-impl", 2911 + ] 2912 + 2913 + [[package]] 2914 + name = "ref-cast-impl" 2915 + version = "1.0.25" 2916 + source = "registry+https://github.com/rust-lang/crates.io-index" 2917 + checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" 2918 + dependencies = [ 2919 + "proc-macro2", 2920 + "quote", 2921 + "syn 2.0.105", 2922 + ] 2923 + 2924 + [[package]] 1176 2925 name = "regex" 1177 - version = "1.11.1" 2926 + version = "1.12.2" 1178 2927 source = "registry+https://github.com/rust-lang/crates.io-index" 1179 - checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 2928 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 1180 2929 dependencies = [ 1181 2930 "aho-corasick", 1182 2931 "memchr", 1183 - "regex-automata 0.4.9", 2932 + "regex-automata 0.4.13", 1184 2933 "regex-syntax 0.8.5", 1185 2934 ] 1186 2935 ··· 1195 2944 1196 2945 [[package]] 1197 2946 name = "regex-automata" 1198 - version = "0.4.9" 2947 + version = "0.4.13" 1199 2948 source = "registry+https://github.com/rust-lang/crates.io-index" 1200 - checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 2949 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 1201 2950 dependencies = [ 1202 2951 "aho-corasick", 1203 2952 "memchr", ··· 1205 2954 ] 1206 2955 1207 2956 [[package]] 2957 + name = "regex-lite" 2958 + version = "0.1.8" 2959 + source = "registry+https://github.com/rust-lang/crates.io-index" 2960 + checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" 2961 + 2962 + [[package]] 1208 2963 name = "regex-syntax" 1209 2964 version = "0.6.29" 1210 2965 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1217 2972 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1218 2973 1219 2974 [[package]] 2975 + name = "reqwest" 2976 + version = "0.12.24" 2977 + source = "registry+https://github.com/rust-lang/crates.io-index" 2978 + checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 2979 + dependencies = [ 2980 + "async-compression", 2981 + "base64", 2982 + "bytes", 2983 + "encoding_rs", 2984 + "futures-core", 2985 + "futures-util", 2986 + "h2", 2987 + "http", 2988 + "http-body", 2989 + "http-body-util", 2990 + "hyper", 2991 + "hyper-rustls", 2992 + "hyper-util", 2993 + "js-sys", 2994 + "log", 2995 + "mime", 2996 + "percent-encoding", 2997 + "pin-project-lite", 2998 + "quinn", 2999 + "rustls", 3000 + "rustls-pki-types", 3001 + "serde", 3002 + "serde_json", 3003 + "serde_urlencoded", 3004 + "sync_wrapper", 3005 + "tokio", 3006 + "tokio-rustls", 3007 + "tokio-util", 3008 + "tower", 3009 + "tower-http", 3010 + "tower-service", 3011 + "url", 3012 + "wasm-bindgen", 3013 + "wasm-bindgen-futures", 3014 + "wasm-streams", 3015 + "web-sys", 3016 + "webpki-roots 1.0.2", 3017 + ] 3018 + 3019 + [[package]] 3020 + name = "rfc6979" 3021 + version = "0.4.0" 3022 + source = "registry+https://github.com/rust-lang/crates.io-index" 3023 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 3024 + dependencies = [ 3025 + "hmac", 3026 + "subtle", 3027 + ] 3028 + 3029 + [[package]] 1220 3030 name = "ring" 1221 3031 version = "0.17.14" 1222 3032 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1226 3036 "cfg-if", 1227 3037 "getrandom 0.2.16", 1228 3038 "libc", 1229 - "untrusted", 3039 + "untrusted 0.9.0", 1230 3040 "windows-sys 0.52.0", 1231 3041 ] 1232 3042 ··· 1243 3053 "num-traits", 1244 3054 "pkcs1", 1245 3055 "pkcs8", 1246 - "rand_core", 3056 + "rand_core 0.6.4", 1247 3057 "signature", 1248 3058 "spki", 1249 3059 "subtle", ··· 1251 3061 ] 1252 3062 1253 3063 [[package]] 3064 + name = "rust-embed" 3065 + version = "8.7.2" 3066 + source = "registry+https://github.com/rust-lang/crates.io-index" 3067 + checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" 3068 + dependencies = [ 3069 + "rust-embed-impl", 3070 + "rust-embed-utils", 3071 + "walkdir", 3072 + ] 3073 + 3074 + [[package]] 3075 + name = "rust-embed-impl" 3076 + version = "8.7.2" 3077 + source = "registry+https://github.com/rust-lang/crates.io-index" 3078 + checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" 3079 + dependencies = [ 3080 + "proc-macro2", 3081 + "quote", 3082 + "rust-embed-utils", 3083 + "syn 2.0.105", 3084 + "walkdir", 3085 + ] 3086 + 3087 + [[package]] 3088 + name = "rust-embed-utils" 3089 + version = "8.7.2" 3090 + source = "registry+https://github.com/rust-lang/crates.io-index" 3091 + checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" 3092 + dependencies = [ 3093 + "globset", 3094 + "sha2", 3095 + "walkdir", 3096 + ] 3097 + 3098 + [[package]] 1254 3099 name = "rustc-demangle" 1255 3100 version = "0.1.26" 1256 3101 source = "registry+https://github.com/rust-lang/crates.io-index" 1257 3102 checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 1258 3103 1259 3104 [[package]] 3105 + name = "rustc-hash" 3106 + version = "1.1.0" 3107 + source = "registry+https://github.com/rust-lang/crates.io-index" 3108 + checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 3109 + 3110 + [[package]] 3111 + name = "rustc-hash" 3112 + version = "2.1.1" 3113 + source = "registry+https://github.com/rust-lang/crates.io-index" 3114 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3115 + 3116 + [[package]] 1260 3117 name = "rustix" 1261 - version = "1.0.8" 3118 + version = "0.38.44" 1262 3119 source = "registry+https://github.com/rust-lang/crates.io-index" 1263 - checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 3120 + checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1264 3121 dependencies = [ 1265 3122 "bitflags", 1266 3123 "errno", 1267 3124 "libc", 1268 3125 "linux-raw-sys", 1269 - "windows-sys 0.60.2", 3126 + "windows-sys 0.59.0", 1270 3127 ] 1271 3128 1272 3129 [[package]] 1273 3130 name = "rustls" 1274 - version = "0.21.12" 3131 + version = "0.23.31" 1275 3132 source = "registry+https://github.com/rust-lang/crates.io-index" 1276 - checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 3133 + checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" 1277 3134 dependencies = [ 3135 + "aws-lc-rs", 3136 + "log", 3137 + "once_cell", 1278 3138 "ring", 3139 + "rustls-pki-types", 1279 3140 "rustls-webpki", 1280 - "sct", 3141 + "subtle", 3142 + "zeroize", 1281 3143 ] 1282 3144 1283 3145 [[package]] 1284 - name = "rustls-pemfile" 1285 - version = "1.0.4" 3146 + name = "rustls-pki-types" 3147 + version = "1.12.0" 1286 3148 source = "registry+https://github.com/rust-lang/crates.io-index" 1287 - checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 3149 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1288 3150 dependencies = [ 1289 - "base64", 3151 + "web-time", 3152 + "zeroize", 1290 3153 ] 1291 3154 1292 3155 [[package]] 1293 3156 name = "rustls-webpki" 1294 - version = "0.101.7" 3157 + version = "0.103.4" 1295 3158 source = "registry+https://github.com/rust-lang/crates.io-index" 1296 - checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 3159 + checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" 1297 3160 dependencies = [ 3161 + "aws-lc-rs", 1298 3162 "ring", 1299 - "untrusted", 3163 + "rustls-pki-types", 3164 + "untrusted 0.9.0", 1300 3165 ] 1301 3166 1302 3167 [[package]] ··· 1312 3177 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1313 3178 1314 3179 [[package]] 3180 + name = "salsa20" 3181 + version = "0.10.2" 3182 + source = "registry+https://github.com/rust-lang/crates.io-index" 3183 + checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" 3184 + dependencies = [ 3185 + "cipher", 3186 + ] 3187 + 3188 + [[package]] 3189 + name = "same-file" 3190 + version = "1.0.6" 3191 + source = "registry+https://github.com/rust-lang/crates.io-index" 3192 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 3193 + dependencies = [ 3194 + "winapi-util", 3195 + ] 3196 + 3197 + [[package]] 3198 + name = "schemars" 3199 + version = "0.9.0" 3200 + source = "registry+https://github.com/rust-lang/crates.io-index" 3201 + checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" 3202 + dependencies = [ 3203 + "dyn-clone", 3204 + "ref-cast", 3205 + "serde", 3206 + "serde_json", 3207 + ] 3208 + 3209 + [[package]] 3210 + name = "schemars" 3211 + version = "1.1.0" 3212 + source = "registry+https://github.com/rust-lang/crates.io-index" 3213 + checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" 3214 + dependencies = [ 3215 + "dyn-clone", 3216 + "ref-cast", 3217 + "serde", 3218 + "serde_json", 3219 + ] 3220 + 3221 + [[package]] 1315 3222 name = "scopeguard" 1316 3223 version = "1.2.0" 1317 3224 source = "registry+https://github.com/rust-lang/crates.io-index" 1318 3225 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1319 3226 1320 3227 [[package]] 1321 - name = "sct" 1322 - version = "0.7.1" 3228 + name = "scrypt" 3229 + version = "0.11.0" 3230 + source = "registry+https://github.com/rust-lang/crates.io-index" 3231 + checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" 3232 + dependencies = [ 3233 + "password-hash", 3234 + "pbkdf2", 3235 + "salsa20", 3236 + "sha2", 3237 + ] 3238 + 3239 + [[package]] 3240 + name = "sec1" 3241 + version = "0.7.3" 3242 + source = "registry+https://github.com/rust-lang/crates.io-index" 3243 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 3244 + dependencies = [ 3245 + "base16ct", 3246 + "der", 3247 + "generic-array", 3248 + "pkcs8", 3249 + "subtle", 3250 + "zeroize", 3251 + ] 3252 + 3253 + [[package]] 3254 + name = "secp256k1" 3255 + version = "0.28.2" 3256 + source = "registry+https://github.com/rust-lang/crates.io-index" 3257 + checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" 3258 + dependencies = [ 3259 + "secp256k1-sys", 3260 + ] 3261 + 3262 + [[package]] 3263 + name = "secp256k1-sys" 3264 + version = "0.9.2" 1323 3265 source = "registry+https://github.com/rust-lang/crates.io-index" 1324 - checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 3266 + checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" 1325 3267 dependencies = [ 1326 - "ring", 1327 - "untrusted", 3268 + "cc", 1328 3269 ] 1329 3270 1330 3271 [[package]] 1331 3272 name = "serde" 1332 - version = "1.0.219" 3273 + version = "1.0.228" 3274 + source = "registry+https://github.com/rust-lang/crates.io-index" 3275 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 3276 + dependencies = [ 3277 + "serde_core", 3278 + "serde_derive", 3279 + ] 3280 + 3281 + [[package]] 3282 + name = "serde_bytes" 3283 + version = "0.11.19" 1333 3284 source = "registry+https://github.com/rust-lang/crates.io-index" 1334 - checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 3285 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 3286 + dependencies = [ 3287 + "serde", 3288 + "serde_core", 3289 + ] 3290 + 3291 + [[package]] 3292 + name = "serde_core" 3293 + version = "1.0.228" 3294 + source = "registry+https://github.com/rust-lang/crates.io-index" 3295 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1335 3296 dependencies = [ 1336 3297 "serde_derive", 1337 3298 ] 1338 3299 1339 3300 [[package]] 1340 3301 name = "serde_derive" 1341 - version = "1.0.219" 3302 + version = "1.0.228" 1342 3303 source = "registry+https://github.com/rust-lang/crates.io-index" 1343 - checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 3304 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1344 3305 dependencies = [ 1345 3306 "proc-macro2", 1346 3307 "quote", ··· 1348 3309 ] 1349 3310 1350 3311 [[package]] 3312 + name = "serde_html_form" 3313 + version = "0.2.8" 3314 + source = "registry+https://github.com/rust-lang/crates.io-index" 3315 + checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 3316 + dependencies = [ 3317 + "form_urlencoded", 3318 + "indexmap 2.10.0", 3319 + "itoa", 3320 + "ryu", 3321 + "serde_core", 3322 + ] 3323 + 3324 + [[package]] 3325 + name = "serde_ipld_dagcbor" 3326 + version = "0.6.4" 3327 + source = "registry+https://github.com/rust-lang/crates.io-index" 3328 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 3329 + dependencies = [ 3330 + "cbor4ii", 3331 + "ipld-core", 3332 + "scopeguard", 3333 + "serde", 3334 + ] 3335 + 3336 + [[package]] 1351 3337 name = "serde_json" 1352 - version = "1.0.142" 3338 + version = "1.0.145" 1353 3339 source = "registry+https://github.com/rust-lang/crates.io-index" 1354 - checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 3340 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 1355 3341 dependencies = [ 3342 + "indexmap 2.10.0", 1356 3343 "itoa", 1357 3344 "memchr", 1358 3345 "ryu", 1359 3346 "serde", 3347 + "serde_core", 1360 3348 ] 1361 3349 1362 3350 [[package]] ··· 1370 3358 ] 1371 3359 1372 3360 [[package]] 3361 + name = "serde_repr" 3362 + version = "0.1.20" 3363 + source = "registry+https://github.com/rust-lang/crates.io-index" 3364 + checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 3365 + dependencies = [ 3366 + "proc-macro2", 3367 + "quote", 3368 + "syn 2.0.105", 3369 + ] 3370 + 3371 + [[package]] 1373 3372 name = "serde_urlencoded" 1374 3373 version = "0.7.1" 1375 3374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1382 3381 ] 1383 3382 1384 3383 [[package]] 3384 + name = "serde_with" 3385 + version = "3.16.0" 3386 + source = "registry+https://github.com/rust-lang/crates.io-index" 3387 + checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" 3388 + dependencies = [ 3389 + "base64", 3390 + "chrono", 3391 + "hex", 3392 + "indexmap 1.9.3", 3393 + "indexmap 2.10.0", 3394 + "schemars 0.9.0", 3395 + "schemars 1.1.0", 3396 + "serde_core", 3397 + "serde_json", 3398 + "serde_with_macros", 3399 + "time", 3400 + ] 3401 + 3402 + [[package]] 3403 + name = "serde_with_macros" 3404 + version = "3.16.0" 3405 + source = "registry+https://github.com/rust-lang/crates.io-index" 3406 + checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" 3407 + dependencies = [ 3408 + "darling 0.21.3", 3409 + "proc-macro2", 3410 + "quote", 3411 + "syn 2.0.105", 3412 + ] 3413 + 3414 + [[package]] 1385 3415 name = "sha1" 1386 3416 version = "0.10.6" 1387 3417 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1434 3464 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1435 3465 dependencies = [ 1436 3466 "digest", 1437 - "rand_core", 3467 + "rand_core 0.6.4", 1438 3468 ] 3469 + 3470 + [[package]] 3471 + name = "simd-adler32" 3472 + version = "0.3.7" 3473 + source = "registry+https://github.com/rust-lang/crates.io-index" 3474 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1439 3475 1440 3476 [[package]] 1441 3477 name = "slab" ··· 1448 3484 version = "1.15.1" 1449 3485 source = "registry+https://github.com/rust-lang/crates.io-index" 1450 3486 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 3487 + dependencies = [ 3488 + "serde", 3489 + ] 3490 + 3491 + [[package]] 3492 + name = "smol_str" 3493 + version = "0.3.4" 3494 + source = "registry+https://github.com/rust-lang/crates.io-index" 3495 + checksum = "3498b0a27f93ef1402f20eefacfaa1691272ac4eca1cdc8c596cb0a245d6cbf5" 3496 + dependencies = [ 3497 + "borsh", 3498 + "serde_core", 3499 + ] 1451 3500 1452 3501 [[package]] 1453 3502 name = "socket2" ··· 1469 3518 ] 1470 3519 1471 3520 [[package]] 1472 - name = "spki" 1473 - version = "0.7.3" 3521 + name = "spinning_top" 3522 + version = "0.3.0" 1474 3523 source = "registry+https://github.com/rust-lang/crates.io-index" 1475 - checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 3524 + checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" 1476 3525 dependencies = [ 1477 - "base64ct", 1478 - "der", 3526 + "lock_api", 1479 3527 ] 1480 3528 1481 3529 [[package]] 1482 - name = "sqlformat" 1483 - version = "0.2.6" 3530 + name = "spki" 3531 + version = "0.7.3" 1484 3532 source = "registry+https://github.com/rust-lang/crates.io-index" 1485 - checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" 3533 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1486 3534 dependencies = [ 1487 - "nom", 1488 - "unicode_categories", 3535 + "base64ct", 3536 + "der", 1489 3537 ] 1490 3538 1491 3539 [[package]] 1492 3540 name = "sqlx" 1493 - version = "0.7.4" 3541 + version = "0.8.6" 1494 3542 source = "registry+https://github.com/rust-lang/crates.io-index" 1495 - checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" 3543 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 1496 3544 dependencies = [ 1497 3545 "sqlx-core", 1498 3546 "sqlx-macros", ··· 1503 3551 1504 3552 [[package]] 1505 3553 name = "sqlx-core" 1506 - version = "0.7.4" 3554 + version = "0.8.6" 1507 3555 source = "registry+https://github.com/rust-lang/crates.io-index" 1508 - checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" 3556 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 1509 3557 dependencies = [ 1510 - "ahash", 1511 - "atoi", 1512 - "byteorder", 3558 + "base64", 1513 3559 "bytes", 3560 + "chrono", 1514 3561 "crc", 1515 3562 "crossbeam-queue", 1516 3563 "either", 1517 3564 "event-listener", 1518 - "futures-channel", 1519 3565 "futures-core", 1520 3566 "futures-intrusive", 1521 3567 "futures-io", 1522 3568 "futures-util", 3569 + "hashbrown 0.15.5", 1523 3570 "hashlink", 1524 - "hex", 1525 - "indexmap", 3571 + "indexmap 2.10.0", 1526 3572 "log", 1527 3573 "memchr", 1528 3574 "once_cell", 1529 - "paste", 1530 3575 "percent-encoding", 1531 3576 "rustls", 1532 - "rustls-pemfile", 1533 3577 "serde", 1534 3578 "serde_json", 1535 3579 "sha2", 1536 3580 "smallvec", 1537 - "sqlformat", 1538 - "thiserror", 3581 + "thiserror 2.0.14", 1539 3582 "tokio", 1540 3583 "tokio-stream", 1541 3584 "tracing", 1542 3585 "url", 1543 - "webpki-roots", 3586 + "webpki-roots 0.26.11", 1544 3587 ] 1545 3588 1546 3589 [[package]] 1547 3590 name = "sqlx-macros" 1548 - version = "0.7.4" 3591 + version = "0.8.6" 1549 3592 source = "registry+https://github.com/rust-lang/crates.io-index" 1550 - checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" 3593 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 1551 3594 dependencies = [ 1552 3595 "proc-macro2", 1553 3596 "quote", 1554 3597 "sqlx-core", 1555 3598 "sqlx-macros-core", 1556 - "syn 1.0.109", 3599 + "syn 2.0.105", 1557 3600 ] 1558 3601 1559 3602 [[package]] 1560 3603 name = "sqlx-macros-core" 1561 - version = "0.7.4" 3604 + version = "0.8.6" 1562 3605 source = "registry+https://github.com/rust-lang/crates.io-index" 1563 - checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" 3606 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 1564 3607 dependencies = [ 1565 3608 "dotenvy", 1566 3609 "either", 1567 - "heck", 3610 + "heck 0.5.0", 1568 3611 "hex", 1569 3612 "once_cell", 1570 3613 "proc-macro2", ··· 1574 3617 "sha2", 1575 3618 "sqlx-core", 1576 3619 "sqlx-mysql", 3620 + "sqlx-postgres", 1577 3621 "sqlx-sqlite", 1578 - "syn 1.0.109", 1579 - "tempfile", 3622 + "syn 2.0.105", 1580 3623 "tokio", 1581 3624 "url", 1582 3625 ] 1583 3626 1584 3627 [[package]] 1585 3628 name = "sqlx-mysql" 1586 - version = "0.7.4" 3629 + version = "0.8.6" 1587 3630 source = "registry+https://github.com/rust-lang/crates.io-index" 1588 - checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" 3631 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 1589 3632 dependencies = [ 1590 3633 "atoi", 1591 3634 "base64", 1592 3635 "bitflags", 1593 3636 "byteorder", 1594 3637 "bytes", 3638 + "chrono", 1595 3639 "crc", 1596 3640 "digest", 1597 3641 "dotenvy", ··· 1610 3654 "memchr", 1611 3655 "once_cell", 1612 3656 "percent-encoding", 1613 - "rand", 3657 + "rand 0.8.5", 1614 3658 "rsa", 1615 3659 "serde", 1616 3660 "sha1", ··· 1618 3662 "smallvec", 1619 3663 "sqlx-core", 1620 3664 "stringprep", 1621 - "thiserror", 3665 + "thiserror 2.0.14", 1622 3666 "tracing", 1623 3667 "whoami", 1624 3668 ] 1625 3669 1626 3670 [[package]] 1627 3671 name = "sqlx-postgres" 1628 - version = "0.7.4" 3672 + version = "0.8.6" 1629 3673 source = "registry+https://github.com/rust-lang/crates.io-index" 1630 - checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" 3674 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 1631 3675 dependencies = [ 1632 3676 "atoi", 1633 3677 "base64", 1634 3678 "bitflags", 1635 3679 "byteorder", 3680 + "chrono", 1636 3681 "crc", 1637 3682 "dotenvy", 1638 3683 "etcetera", 1639 3684 "futures-channel", 1640 3685 "futures-core", 1641 - "futures-io", 1642 3686 "futures-util", 1643 3687 "hex", 1644 3688 "hkdf", ··· 1649 3693 "md-5", 1650 3694 "memchr", 1651 3695 "once_cell", 1652 - "rand", 3696 + "rand 0.8.5", 1653 3697 "serde", 1654 3698 "serde_json", 1655 3699 "sha2", 1656 3700 "smallvec", 1657 3701 "sqlx-core", 1658 3702 "stringprep", 1659 - "thiserror", 3703 + "thiserror 2.0.14", 1660 3704 "tracing", 1661 3705 "whoami", 1662 3706 ] 1663 3707 1664 3708 [[package]] 1665 3709 name = "sqlx-sqlite" 1666 - version = "0.7.4" 3710 + version = "0.8.6" 1667 3711 source = "registry+https://github.com/rust-lang/crates.io-index" 1668 - checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" 3712 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 1669 3713 dependencies = [ 1670 3714 "atoi", 3715 + "chrono", 1671 3716 "flume", 1672 3717 "futures-channel", 1673 3718 "futures-core", ··· 1678 3723 "log", 1679 3724 "percent-encoding", 1680 3725 "serde", 3726 + "serde_urlencoded", 1681 3727 "sqlx-core", 3728 + "thiserror 2.0.14", 1682 3729 "tracing", 1683 3730 "url", 1684 - "urlencoding", 1685 3731 ] 1686 3732 1687 3733 [[package]] ··· 1691 3737 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1692 3738 1693 3739 [[package]] 3740 + name = "stacker" 3741 + version = "0.1.21" 3742 + source = "registry+https://github.com/rust-lang/crates.io-index" 3743 + checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" 3744 + dependencies = [ 3745 + "cc", 3746 + "cfg-if", 3747 + "libc", 3748 + "psm", 3749 + "windows-sys 0.59.0", 3750 + ] 3751 + 3752 + [[package]] 3753 + name = "static-regular-grammar" 3754 + version = "2.0.2" 3755 + source = "registry+https://github.com/rust-lang/crates.io-index" 3756 + checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" 3757 + dependencies = [ 3758 + "abnf", 3759 + "btree-range-map", 3760 + "ciborium", 3761 + "hex_fmt", 3762 + "indoc", 3763 + "proc-macro-error", 3764 + "proc-macro2", 3765 + "quote", 3766 + "serde", 3767 + "sha2", 3768 + "syn 2.0.105", 3769 + "thiserror 1.0.69", 3770 + ] 3771 + 3772 + [[package]] 3773 + name = "static_assertions" 3774 + version = "1.1.0" 3775 + source = "registry+https://github.com/rust-lang/crates.io-index" 3776 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 3777 + 3778 + [[package]] 1694 3779 name = "stringprep" 1695 3780 version = "0.1.5" 1696 3781 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1700 3785 "unicode-normalization", 1701 3786 "unicode-properties", 1702 3787 ] 3788 + 3789 + [[package]] 3790 + name = "strsim" 3791 + version = "0.11.1" 3792 + source = "registry+https://github.com/rust-lang/crates.io-index" 3793 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1703 3794 1704 3795 [[package]] 1705 3796 name = "subtle" ··· 1734 3825 version = "1.0.2" 1735 3826 source = "registry+https://github.com/rust-lang/crates.io-index" 1736 3827 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3828 + dependencies = [ 3829 + "futures-core", 3830 + ] 1737 3831 1738 3832 [[package]] 1739 3833 name = "synstructure" ··· 1747 3841 ] 1748 3842 1749 3843 [[package]] 1750 - name = "tempfile" 1751 - version = "3.20.0" 3844 + name = "system-configuration" 3845 + version = "0.6.1" 3846 + source = "registry+https://github.com/rust-lang/crates.io-index" 3847 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3848 + dependencies = [ 3849 + "bitflags", 3850 + "core-foundation", 3851 + "system-configuration-sys", 3852 + ] 3853 + 3854 + [[package]] 3855 + name = "system-configuration-sys" 3856 + version = "0.6.0" 1752 3857 source = "registry+https://github.com/rust-lang/crates.io-index" 1753 - checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 3858 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1754 3859 dependencies = [ 1755 - "fastrand", 1756 - "getrandom 0.3.3", 1757 - "once_cell", 1758 - "rustix", 1759 - "windows-sys 0.59.0", 3860 + "core-foundation-sys", 3861 + "libc", 1760 3862 ] 1761 3863 1762 3864 [[package]] ··· 1765 3867 source = "registry+https://github.com/rust-lang/crates.io-index" 1766 3868 checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1767 3869 dependencies = [ 1768 - "thiserror-impl", 3870 + "thiserror-impl 1.0.69", 3871 + ] 3872 + 3873 + [[package]] 3874 + name = "thiserror" 3875 + version = "2.0.14" 3876 + source = "registry+https://github.com/rust-lang/crates.io-index" 3877 + checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" 3878 + dependencies = [ 3879 + "thiserror-impl 2.0.14", 1769 3880 ] 1770 3881 1771 3882 [[package]] ··· 1780 3891 ] 1781 3892 1782 3893 [[package]] 3894 + name = "thiserror-impl" 3895 + version = "2.0.14" 3896 + source = "registry+https://github.com/rust-lang/crates.io-index" 3897 + checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" 3898 + dependencies = [ 3899 + "proc-macro2", 3900 + "quote", 3901 + "syn 2.0.105", 3902 + ] 3903 + 3904 + [[package]] 1783 3905 name = "thread_local" 1784 3906 version = "1.1.9" 1785 3907 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1789 3911 ] 1790 3912 1791 3913 [[package]] 3914 + name = "time" 3915 + version = "0.3.44" 3916 + source = "registry+https://github.com/rust-lang/crates.io-index" 3917 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 3918 + dependencies = [ 3919 + "deranged", 3920 + "itoa", 3921 + "num-conv", 3922 + "powerfmt", 3923 + "serde", 3924 + "time-core", 3925 + "time-macros", 3926 + ] 3927 + 3928 + [[package]] 3929 + name = "time-core" 3930 + version = "0.1.6" 3931 + source = "registry+https://github.com/rust-lang/crates.io-index" 3932 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 3933 + 3934 + [[package]] 3935 + name = "time-macros" 3936 + version = "0.2.24" 3937 + source = "registry+https://github.com/rust-lang/crates.io-index" 3938 + checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 3939 + dependencies = [ 3940 + "num-conv", 3941 + "time-core", 3942 + ] 3943 + 3944 + [[package]] 1792 3945 name = "tinystr" 1793 3946 version = "0.8.1" 1794 3947 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1844 3997 ] 1845 3998 1846 3999 [[package]] 4000 + name = "tokio-rustls" 4001 + version = "0.26.2" 4002 + source = "registry+https://github.com/rust-lang/crates.io-index" 4003 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 4004 + dependencies = [ 4005 + "rustls", 4006 + "tokio", 4007 + ] 4008 + 4009 + [[package]] 1847 4010 name = "tokio-stream" 1848 4011 version = "0.1.17" 1849 4012 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1855 4018 ] 1856 4019 1857 4020 [[package]] 4021 + name = "tokio-util" 4022 + version = "0.7.17" 4023 + source = "registry+https://github.com/rust-lang/crates.io-index" 4024 + checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 4025 + dependencies = [ 4026 + "bytes", 4027 + "futures-core", 4028 + "futures-sink", 4029 + "pin-project-lite", 4030 + "tokio", 4031 + ] 4032 + 4033 + [[package]] 4034 + name = "tonic" 4035 + version = "0.14.1" 4036 + source = "registry+https://github.com/rust-lang/crates.io-index" 4037 + checksum = "67ac5a8627ada0968acec063a4746bf79588aa03ccb66db2f75d7dce26722a40" 4038 + dependencies = [ 4039 + "async-trait", 4040 + "axum", 4041 + "base64", 4042 + "bytes", 4043 + "h2", 4044 + "http", 4045 + "http-body", 4046 + "http-body-util", 4047 + "hyper", 4048 + "hyper-timeout", 4049 + "hyper-util", 4050 + "percent-encoding", 4051 + "pin-project", 4052 + "socket2", 4053 + "sync_wrapper", 4054 + "tokio", 4055 + "tokio-stream", 4056 + "tower", 4057 + "tower-layer", 4058 + "tower-service", 4059 + "tracing", 4060 + ] 4061 + 4062 + [[package]] 1858 4063 name = "tower" 1859 4064 version = "0.5.2" 1860 4065 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1862 4067 dependencies = [ 1863 4068 "futures-core", 1864 4069 "futures-util", 4070 + "indexmap 2.10.0", 1865 4071 "pin-project-lite", 4072 + "slab", 1866 4073 "sync_wrapper", 1867 4074 "tokio", 4075 + "tokio-util", 1868 4076 "tower-layer", 1869 4077 "tower-service", 1870 4078 "tracing", 1871 4079 ] 1872 4080 1873 4081 [[package]] 4082 + name = "tower-http" 4083 + version = "0.6.6" 4084 + source = "registry+https://github.com/rust-lang/crates.io-index" 4085 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 4086 + dependencies = [ 4087 + "async-compression", 4088 + "bitflags", 4089 + "bytes", 4090 + "futures-core", 4091 + "futures-util", 4092 + "http", 4093 + "http-body", 4094 + "iri-string", 4095 + "pin-project-lite", 4096 + "tokio", 4097 + "tokio-util", 4098 + "tower", 4099 + "tower-layer", 4100 + "tower-service", 4101 + ] 4102 + 4103 + [[package]] 1874 4104 name = "tower-layer" 1875 4105 version = "0.3.3" 1876 4106 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1881 4111 version = "0.3.3" 1882 4112 source = "registry+https://github.com/rust-lang/crates.io-index" 1883 4113 checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 4114 + 4115 + [[package]] 4116 + name = "tower_governor" 4117 + version = "0.8.0" 4118 + source = "registry+https://github.com/rust-lang/crates.io-index" 4119 + checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6" 4120 + dependencies = [ 4121 + "axum", 4122 + "forwarded-header-value", 4123 + "governor", 4124 + "http", 4125 + "pin-project", 4126 + "thiserror 2.0.14", 4127 + "tonic", 4128 + "tower", 4129 + "tracing", 4130 + ] 1884 4131 1885 4132 [[package]] 1886 4133 name = "tracing" ··· 1945 4192 ] 1946 4193 1947 4194 [[package]] 4195 + name = "trait-variant" 4196 + version = "0.1.2" 4197 + source = "registry+https://github.com/rust-lang/crates.io-index" 4198 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 4199 + dependencies = [ 4200 + "proc-macro2", 4201 + "quote", 4202 + "syn 2.0.105", 4203 + ] 4204 + 4205 + [[package]] 4206 + name = "try-lock" 4207 + version = "0.2.5" 4208 + source = "registry+https://github.com/rust-lang/crates.io-index" 4209 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 4210 + 4211 + [[package]] 1948 4212 name = "typenum" 1949 4213 version = "1.18.0" 1950 4214 source = "registry+https://github.com/rust-lang/crates.io-index" 1951 4215 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 4216 + 4217 + [[package]] 4218 + name = "ucd-trie" 4219 + version = "0.1.7" 4220 + source = "registry+https://github.com/rust-lang/crates.io-index" 4221 + checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1952 4222 1953 4223 [[package]] 1954 4224 name = "unicode-bidi" ··· 1984 4254 checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1985 4255 1986 4256 [[package]] 1987 - name = "unicode_categories" 1988 - version = "0.1.1" 4257 + name = "unicode-width" 4258 + version = "0.1.14" 4259 + source = "registry+https://github.com/rust-lang/crates.io-index" 4260 + checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 4261 + 4262 + [[package]] 4263 + name = "unsigned-varint" 4264 + version = "0.8.0" 4265 + source = "registry+https://github.com/rust-lang/crates.io-index" 4266 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 4267 + 4268 + [[package]] 4269 + name = "untrusted" 4270 + version = "0.7.1" 1989 4271 source = "registry+https://github.com/rust-lang/crates.io-index" 1990 - checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 4272 + checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1991 4273 1992 4274 [[package]] 1993 4275 name = "untrusted" ··· 2004 4286 "form_urlencoded", 2005 4287 "idna", 2006 4288 "percent-encoding", 4289 + "serde", 2007 4290 ] 2008 4291 2009 4292 [[package]] ··· 2013 4296 checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 2014 4297 2015 4298 [[package]] 4299 + name = "utf8-width" 4300 + version = "0.1.8" 4301 + source = "registry+https://github.com/rust-lang/crates.io-index" 4302 + checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" 4303 + 4304 + [[package]] 2016 4305 name = "utf8_iter" 2017 4306 version = "1.0.4" 2018 4307 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2037 4326 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2038 4327 2039 4328 [[package]] 4329 + name = "walkdir" 4330 + version = "2.5.0" 4331 + source = "registry+https://github.com/rust-lang/crates.io-index" 4332 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 4333 + dependencies = [ 4334 + "same-file", 4335 + "winapi-util", 4336 + ] 4337 + 4338 + [[package]] 4339 + name = "want" 4340 + version = "0.3.1" 4341 + source = "registry+https://github.com/rust-lang/crates.io-index" 4342 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 4343 + dependencies = [ 4344 + "try-lock", 4345 + ] 4346 + 4347 + [[package]] 2040 4348 name = "wasi" 2041 4349 version = "0.11.1+wasi-snapshot-preview1" 2042 4350 source = "registry+https://github.com/rust-lang/crates.io-index" 2043 4351 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2044 4352 2045 4353 [[package]] 2046 - name = "wasi" 2047 - version = "0.14.2+wasi-0.2.4" 4354 + name = "wasip2" 4355 + version = "1.0.1+wasi-0.2.4" 2048 4356 source = "registry+https://github.com/rust-lang/crates.io-index" 2049 - checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 4357 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 2050 4358 dependencies = [ 2051 - "wit-bindgen-rt", 4359 + "wit-bindgen", 2052 4360 ] 2053 4361 2054 4362 [[package]] ··· 2058 4366 checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2059 4367 2060 4368 [[package]] 4369 + name = "wasm-bindgen" 4370 + version = "0.2.100" 4371 + source = "registry+https://github.com/rust-lang/crates.io-index" 4372 + checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 4373 + dependencies = [ 4374 + "cfg-if", 4375 + "once_cell", 4376 + "rustversion", 4377 + "wasm-bindgen-macro", 4378 + ] 4379 + 4380 + [[package]] 4381 + name = "wasm-bindgen-backend" 4382 + version = "0.2.100" 4383 + source = "registry+https://github.com/rust-lang/crates.io-index" 4384 + checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 4385 + dependencies = [ 4386 + "bumpalo", 4387 + "log", 4388 + "proc-macro2", 4389 + "quote", 4390 + "syn 2.0.105", 4391 + "wasm-bindgen-shared", 4392 + ] 4393 + 4394 + [[package]] 4395 + name = "wasm-bindgen-futures" 4396 + version = "0.4.50" 4397 + source = "registry+https://github.com/rust-lang/crates.io-index" 4398 + checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 4399 + dependencies = [ 4400 + "cfg-if", 4401 + "js-sys", 4402 + "once_cell", 4403 + "wasm-bindgen", 4404 + "web-sys", 4405 + ] 4406 + 4407 + [[package]] 4408 + name = "wasm-bindgen-macro" 4409 + version = "0.2.100" 4410 + source = "registry+https://github.com/rust-lang/crates.io-index" 4411 + checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 4412 + dependencies = [ 4413 + "quote", 4414 + "wasm-bindgen-macro-support", 4415 + ] 4416 + 4417 + [[package]] 4418 + name = "wasm-bindgen-macro-support" 4419 + version = "0.2.100" 4420 + source = "registry+https://github.com/rust-lang/crates.io-index" 4421 + checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 4422 + dependencies = [ 4423 + "proc-macro2", 4424 + "quote", 4425 + "syn 2.0.105", 4426 + "wasm-bindgen-backend", 4427 + "wasm-bindgen-shared", 4428 + ] 4429 + 4430 + [[package]] 4431 + name = "wasm-bindgen-shared" 4432 + version = "0.2.100" 4433 + source = "registry+https://github.com/rust-lang/crates.io-index" 4434 + checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 4435 + dependencies = [ 4436 + "unicode-ident", 4437 + ] 4438 + 4439 + [[package]] 4440 + name = "wasm-streams" 4441 + version = "0.4.2" 4442 + source = "registry+https://github.com/rust-lang/crates.io-index" 4443 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 4444 + dependencies = [ 4445 + "futures-util", 4446 + "js-sys", 4447 + "wasm-bindgen", 4448 + "wasm-bindgen-futures", 4449 + "web-sys", 4450 + ] 4451 + 4452 + [[package]] 4453 + name = "web-sys" 4454 + version = "0.3.77" 4455 + source = "registry+https://github.com/rust-lang/crates.io-index" 4456 + checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 4457 + dependencies = [ 4458 + "js-sys", 4459 + "wasm-bindgen", 4460 + ] 4461 + 4462 + [[package]] 4463 + name = "web-time" 4464 + version = "1.1.0" 4465 + source = "registry+https://github.com/rust-lang/crates.io-index" 4466 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 4467 + dependencies = [ 4468 + "js-sys", 4469 + "wasm-bindgen", 4470 + ] 4471 + 4472 + [[package]] 2061 4473 name = "webpki-roots" 2062 - version = "0.25.4" 4474 + version = "0.26.11" 4475 + source = "registry+https://github.com/rust-lang/crates.io-index" 4476 + checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 4477 + dependencies = [ 4478 + "webpki-roots 1.0.2", 4479 + ] 4480 + 4481 + [[package]] 4482 + name = "webpki-roots" 4483 + version = "1.0.2" 4484 + source = "registry+https://github.com/rust-lang/crates.io-index" 4485 + checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 4486 + dependencies = [ 4487 + "rustls-pki-types", 4488 + ] 4489 + 4490 + [[package]] 4491 + name = "which" 4492 + version = "4.4.2" 2063 4493 source = "registry+https://github.com/rust-lang/crates.io-index" 2064 - checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 4494 + checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 4495 + dependencies = [ 4496 + "either", 4497 + "home", 4498 + "once_cell", 4499 + "rustix", 4500 + ] 2065 4501 2066 4502 [[package]] 2067 4503 name = "whoami" ··· 2090 4526 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2091 4527 2092 4528 [[package]] 4529 + name = "winapi-util" 4530 + version = "0.1.9" 4531 + source = "registry+https://github.com/rust-lang/crates.io-index" 4532 + checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 4533 + dependencies = [ 4534 + "windows-sys 0.59.0", 4535 + ] 4536 + 4537 + [[package]] 2093 4538 name = "winapi-x86_64-pc-windows-gnu" 2094 4539 version = "0.4.0" 2095 4540 source = "registry+https://github.com/rust-lang/crates.io-index" 2096 4541 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2097 4542 2098 4543 [[package]] 4544 + name = "windows-core" 4545 + version = "0.61.2" 4546 + source = "registry+https://github.com/rust-lang/crates.io-index" 4547 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 4548 + dependencies = [ 4549 + "windows-implement", 4550 + "windows-interface", 4551 + "windows-link 0.1.3", 4552 + "windows-result", 4553 + "windows-strings", 4554 + ] 4555 + 4556 + [[package]] 4557 + name = "windows-implement" 4558 + version = "0.60.0" 4559 + source = "registry+https://github.com/rust-lang/crates.io-index" 4560 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 4561 + dependencies = [ 4562 + "proc-macro2", 4563 + "quote", 4564 + "syn 2.0.105", 4565 + ] 4566 + 4567 + [[package]] 4568 + name = "windows-interface" 4569 + version = "0.59.1" 4570 + source = "registry+https://github.com/rust-lang/crates.io-index" 4571 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 4572 + dependencies = [ 4573 + "proc-macro2", 4574 + "quote", 4575 + "syn 2.0.105", 4576 + ] 4577 + 4578 + [[package]] 2099 4579 name = "windows-link" 2100 4580 version = "0.1.3" 2101 4581 source = "registry+https://github.com/rust-lang/crates.io-index" 2102 4582 checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2103 4583 2104 4584 [[package]] 4585 + name = "windows-link" 4586 + version = "0.2.1" 4587 + source = "registry+https://github.com/rust-lang/crates.io-index" 4588 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 4589 + 4590 + [[package]] 4591 + name = "windows-registry" 4592 + version = "0.5.3" 4593 + source = "registry+https://github.com/rust-lang/crates.io-index" 4594 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 4595 + dependencies = [ 4596 + "windows-link 0.1.3", 4597 + "windows-result", 4598 + "windows-strings", 4599 + ] 4600 + 4601 + [[package]] 4602 + name = "windows-result" 4603 + version = "0.3.4" 4604 + source = "registry+https://github.com/rust-lang/crates.io-index" 4605 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 4606 + dependencies = [ 4607 + "windows-link 0.1.3", 4608 + ] 4609 + 4610 + [[package]] 4611 + name = "windows-strings" 4612 + version = "0.4.2" 4613 + source = "registry+https://github.com/rust-lang/crates.io-index" 4614 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 4615 + dependencies = [ 4616 + "windows-link 0.1.3", 4617 + ] 4618 + 4619 + [[package]] 2105 4620 name = "windows-sys" 2106 4621 version = "0.48.0" 2107 4622 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2126 4641 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2127 4642 dependencies = [ 2128 4643 "windows-targets 0.52.6", 2129 - ] 2130 - 2131 - [[package]] 2132 - name = "windows-sys" 2133 - version = "0.60.2" 2134 - source = "registry+https://github.com/rust-lang/crates.io-index" 2135 - checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 2136 - dependencies = [ 2137 - "windows-targets 0.53.3", 2138 4644 ] 2139 4645 2140 4646 [[package]] ··· 2161 4667 "windows_aarch64_gnullvm 0.52.6", 2162 4668 "windows_aarch64_msvc 0.52.6", 2163 4669 "windows_i686_gnu 0.52.6", 2164 - "windows_i686_gnullvm 0.52.6", 4670 + "windows_i686_gnullvm", 2165 4671 "windows_i686_msvc 0.52.6", 2166 4672 "windows_x86_64_gnu 0.52.6", 2167 4673 "windows_x86_64_gnullvm 0.52.6", ··· 2169 4675 ] 2170 4676 2171 4677 [[package]] 2172 - name = "windows-targets" 2173 - version = "0.53.3" 2174 - source = "registry+https://github.com/rust-lang/crates.io-index" 2175 - checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 2176 - dependencies = [ 2177 - "windows-link", 2178 - "windows_aarch64_gnullvm 0.53.0", 2179 - "windows_aarch64_msvc 0.53.0", 2180 - "windows_i686_gnu 0.53.0", 2181 - "windows_i686_gnullvm 0.53.0", 2182 - "windows_i686_msvc 0.53.0", 2183 - "windows_x86_64_gnu 0.53.0", 2184 - "windows_x86_64_gnullvm 0.53.0", 2185 - "windows_x86_64_msvc 0.53.0", 2186 - ] 2187 - 2188 - [[package]] 2189 4678 name = "windows_aarch64_gnullvm" 2190 4679 version = "0.48.5" 2191 4680 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2198 4687 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2199 4688 2200 4689 [[package]] 2201 - name = "windows_aarch64_gnullvm" 2202 - version = "0.53.0" 2203 - source = "registry+https://github.com/rust-lang/crates.io-index" 2204 - checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2205 - 2206 - [[package]] 2207 4690 name = "windows_aarch64_msvc" 2208 4691 version = "0.48.5" 2209 4692 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2216 4699 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2217 4700 2218 4701 [[package]] 2219 - name = "windows_aarch64_msvc" 2220 - version = "0.53.0" 2221 - source = "registry+https://github.com/rust-lang/crates.io-index" 2222 - checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2223 - 2224 - [[package]] 2225 4702 name = "windows_i686_gnu" 2226 4703 version = "0.48.5" 2227 4704 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2234 4711 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2235 4712 2236 4713 [[package]] 2237 - name = "windows_i686_gnu" 2238 - version = "0.53.0" 2239 - source = "registry+https://github.com/rust-lang/crates.io-index" 2240 - checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2241 - 2242 - [[package]] 2243 4714 name = "windows_i686_gnullvm" 2244 4715 version = "0.52.6" 2245 4716 source = "registry+https://github.com/rust-lang/crates.io-index" 2246 4717 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2247 4718 2248 4719 [[package]] 2249 - name = "windows_i686_gnullvm" 2250 - version = "0.53.0" 2251 - source = "registry+https://github.com/rust-lang/crates.io-index" 2252 - checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2253 - 2254 - [[package]] 2255 4720 name = "windows_i686_msvc" 2256 4721 version = "0.48.5" 2257 4722 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2264 4729 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2265 4730 2266 4731 [[package]] 2267 - name = "windows_i686_msvc" 2268 - version = "0.53.0" 2269 - source = "registry+https://github.com/rust-lang/crates.io-index" 2270 - checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2271 - 2272 - [[package]] 2273 4732 name = "windows_x86_64_gnu" 2274 4733 version = "0.48.5" 2275 4734 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2282 4741 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2283 4742 2284 4743 [[package]] 2285 - name = "windows_x86_64_gnu" 2286 - version = "0.53.0" 2287 - source = "registry+https://github.com/rust-lang/crates.io-index" 2288 - checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2289 - 2290 - [[package]] 2291 4744 name = "windows_x86_64_gnullvm" 2292 4745 version = "0.48.5" 2293 4746 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2300 4753 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2301 4754 2302 4755 [[package]] 2303 - name = "windows_x86_64_gnullvm" 2304 - version = "0.53.0" 2305 - source = "registry+https://github.com/rust-lang/crates.io-index" 2306 - checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2307 - 2308 - [[package]] 2309 4756 name = "windows_x86_64_msvc" 2310 4757 version = "0.48.5" 2311 4758 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2318 4765 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2319 4766 2320 4767 [[package]] 2321 - name = "windows_x86_64_msvc" 2322 - version = "0.53.0" 4768 + name = "wit-bindgen" 4769 + version = "0.46.0" 2323 4770 source = "registry+https://github.com/rust-lang/crates.io-index" 2324 - checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 4771 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 2325 4772 2326 4773 [[package]] 2327 - name = "wit-bindgen-rt" 2328 - version = "0.39.0" 4774 + name = "writeable" 4775 + version = "0.6.1" 2329 4776 source = "registry+https://github.com/rust-lang/crates.io-index" 2330 - checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2331 - dependencies = [ 2332 - "bitflags", 2333 - ] 4777 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2334 4778 2335 4779 [[package]] 2336 - name = "writeable" 2337 - version = "0.6.1" 4780 + name = "yansi" 4781 + version = "1.0.1" 2338 4782 source = "registry+https://github.com/rust-lang/crates.io-index" 2339 - checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 4783 + checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 2340 4784 2341 4785 [[package]] 2342 4786 name = "yoke" ··· 2408 4852 version = "1.8.1" 2409 4853 source = "registry+https://github.com/rust-lang/crates.io-index" 2410 4854 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 4855 + dependencies = [ 4856 + "zeroize_derive", 4857 + ] 4858 + 4859 + [[package]] 4860 + name = "zeroize_derive" 4861 + version = "1.4.2" 4862 + source = "registry+https://github.com/rust-lang/crates.io-index" 4863 + checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 4864 + dependencies = [ 4865 + "proc-macro2", 4866 + "quote", 4867 + "syn 2.0.105", 4868 + ] 2411 4869 2412 4870 [[package]] 2413 4871 name = "zerotrie" ··· 2441 4899 "quote", 2442 4900 "syn 2.0.105", 2443 4901 ] 4902 + 4903 + [[package]] 4904 + name = "zstd" 4905 + version = "0.13.3" 4906 + source = "registry+https://github.com/rust-lang/crates.io-index" 4907 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 4908 + dependencies = [ 4909 + "zstd-safe", 4910 + ] 4911 + 4912 + [[package]] 4913 + name = "zstd-safe" 4914 + version = "7.2.4" 4915 + source = "registry+https://github.com/rust-lang/crates.io-index" 4916 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 4917 + dependencies = [ 4918 + "zstd-sys", 4919 + ] 4920 + 4921 + [[package]] 4922 + name = "zstd-sys" 4923 + version = "2.0.15+zstd.1.5.7" 4924 + source = "registry+https://github.com/rust-lang/crates.io-index" 4925 + checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 4926 + dependencies = [ 4927 + "cc", 4928 + "pkg-config", 4929 + ]
+30 -5
Cargo.toml
··· 1 1 [package] 2 - name = "pds_bells_and_whistles" 3 - version = "0.1.0" 2 + name = "pds_gatekeeper" 3 + version = "0.1.2" 4 4 edition = "2024" 5 + license = "MIT" 5 6 6 7 [dependencies] 7 - axum = { version = "0.7", features = ["macros", "json"] } 8 - tokio = { version = "1.39", features = ["rt-multi-thread", "macros", "signal"] } 9 - sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] } 8 + axum = { version = "0.8.4", features = ["macros", "json"] } 9 + tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "signal"] } 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"] } 16 + hyper-util = { version = "0.1.16", features = ["client", "client-legacy"] } 17 + tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 18 + tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 19 + hex = "0.4" 20 + jwt-compact = { version = "0.8.0", features = ["es256k"] } 21 + scrypt = "0.11" 22 + #Leaveing these two cause I think it is needed by the email crate for ssl 23 + aws-lc-rs = "1.13.0" 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.3.2", features = ["rust-embed"] } 27 + rust-embed = "8.7.2" 28 + axum-template = { version = "3.0.0", features = ["handlebars"] } 29 + rand = "0.9.2" 30 + anyhow = "1.0.99" 31 + chrono = { version = "0.4.42", features = ["default", "serde"] } 32 + sha2 = "0.10" 33 + jacquard-common = "0.9.2" 34 + jacquard-identity = "0.9.2" 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"
+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.
+207
README.md
··· 1 + # PDS gatekeeper 2 + 3 + A microservice that sits on the same server as the PDS to add some of the security that the entryway does. 4 + 5 + ![Picture in black and white of a grassy hill with a gate at the top](./images/gate.jpg) 6 + 7 + PDS gatekeeper works by overriding some of the PDS endpoints inside your Caddyfile to provide gatekeeping to certain 8 + endpoints. Mainly, the ability to have 2FA on a self hosted PDS like it does on a Bluesky mushroom(PDS). Most of the 9 + logic of these endpoints still happens on the PDS via a proxied request, just some are gatekept. 10 + 11 + # Features 12 + 13 + ## 2FA 14 + 15 + - Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins 16 + - Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA 17 + 18 + ## Captcha on account creation 19 + 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. 48 + 49 + # Setup 50 + 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 + ``` 123 + 124 + ## Caddy setup 125 + 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. 129 + 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 + } 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 { 153 + @gatekeeper { 154 + path /xrpc/com.atproto.server.getSession 155 + path /xrpc/com.atproto.server.describeServer 156 + path /xrpc/com.atproto.server.updateEmail 157 + path /xrpc/com.atproto.server.createSession 158 + path /xrpc/com.atproto.server.createAccount 159 + path /@atproto/oauth-provider/~api/sign-in 160 + path /gate/* 161 + } 162 + 163 + handle @gatekeeper { 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} 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. 187 + 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` 196 + 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 +
+5
build.rs
··· 1 + // generated by `sqlx migrate build-script` 2 + fn main() { 3 + // trigger recompilation when a new migration is added 4 + println!("cargo:rerun-if-changed=migrations"); 5 + }
+159
email_templates/two_factor_code.hbs
··· 1 + <html dir="ltr" lang="en"> 2 + 3 + <head> 4 + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> 5 + <meta name="x-apple-disable-message-reformatting"/> 6 + <title>Sign in to Bluesky</title> 7 + <meta 8 + name="description" 9 + content="We received a sign in request for your account." 10 + /> 11 + </head> 12 + <div 13 + style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0" 14 + >We received a sign-in request for the account @{{handle}}. Use the code below to sign in 15 + <div 16 + > 17 + ย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟย โ€Œโ€‹โ€โ€Žโ€๏ปฟ 18 + </div> 19 + </div> 20 + 21 + <body 22 + style="padding:12px;padding-bottom:40px;background-color:hsl(211, 20%, 95.3%)" 23 + > 24 + <table 25 + align="center" 26 + width="100%" 27 + border="0" 28 + cellPadding="0" 29 + cellSpacing="0" 30 + role="presentation" 31 + style="max-width:37.5em" 32 + > 33 + <tbody> 34 + <tr style="width:100%"> 35 + <td> 36 + <table 37 + align="center" 38 + width="100%" 39 + border="0" 40 + cellPadding="0" 41 + cellSpacing="0" 42 + role="presentation" 43 + style="padding-top:24px;padding-bottom:24px" 44 + > 45 + <tbody> 46 + <tr> 47 + <td><img 48 + alt="Bluesky" 49 + src="https://bsky.social/about/images/email/email_logo_default.png" 50 + style="display:block;outline:none;border:none;text-decoration:none;width:110px;margin:0 auto" 51 + /></td> 52 + </tr> 53 + </tbody> 54 + </table> 55 + <table 56 + align="center" 57 + width="100%" 58 + border="0" 59 + cellPadding="0" 60 + cellSpacing="0" 61 + role="presentation" 62 + style="padding:24px;padding-bottom:16px;border-radius:12px;background-color:#FFFFFF" 63 + > 64 + <tbody> 65 + <tr> 66 + <td> 67 + <h1 68 + style="font-size:26px;letter-spacing:0.25px;color:#000000;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0" 69 + >Sign in to Bluesky</h1> 70 + <p 71 + style="font-size:16px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211, 24%, 34.2%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;padding-top:12px;padding-bottom:12px" 72 + >We received a sign-in request for the account @{{handle}}. Use the code below to sign in.</p> 73 + <code 74 + style="display:block;padding:16px;border-radius:8px;border-width:1px;border-style:solid;background-color:hsl(211, 20%, 95.3%);border-color:hsl(211, 20%, 85.89999999999999%);font-size:14px;letter-spacing:0.25px;font-family:monospace;text-transform:uppercase" 75 + >{{token}}</code> 76 + <p style="font-size:14px;line-height:1.4;margin:0px 0px;letter-spacing:0.25px;color:hsl(211,20%,53%);font-family:-apple-system,BlinkMacSystemFont,'Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif;padding-top:12px"> 77 + If this wasn't you, we recommend taking steps to 78 + protect your account by 79 + <a href="https://bsky.app/settings" 80 + style="color:hsl(211,20%,53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system,BlinkMacSystemFont,'Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px" 81 + target="_blank" 82 + data-saferedirecturl="https://bsky.app/settings">changing 83 + your password.</a></p> 84 + <table 85 + align="center" 86 + width="100%" 87 + border="0" 88 + cellPadding="0" 89 + cellSpacing="0" 90 + role="presentation" 91 + style="padding-top:24px" 92 + > 93 + <tbody> 94 + <tr> 95 + <td> 96 + <hr 97 + style="width:100%;border:none;border-top:1px solid #eaeaea;margin:0" 98 + /> 99 + <table 100 + align="center" 101 + width="100%" 102 + border="0" 103 + cellPadding="0" 104 + cellSpacing="0" 105 + role="presentation" 106 + style="padding-top:16px;vertical-align:middle" 107 + > 108 + <tbody style="width:100%"> 109 + <tr style="width:100%"> 110 + <td data-id="__react-email-column"> 111 + <p 112 + style="font-size:14px;line-height:1.4;margin:0px 0px;color:hsl(211, 20%, 53%);font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;letter-spacing:0.25px" 113 + ><a 114 + href="https://bsky.app" 115 + style="color:hsl(211, 20%, 53%);text-decoration:none;text-decoration-line:underline;font-family:-apple-system, BlinkMacSystemFont, &#x27;Roboto&#x27;, &#x27;Oxygen&#x27;, &#x27;Ubuntu&#x27;, &#x27;Cantarell&#x27;, &#x27;Fira Sans&#x27;, &#x27;Droid Sans&#x27;, &#x27;Helvetica Neue&#x27;, sans-serif;margin:0px 0px;line-height:1.0;font-size:14px;letter-spacing:0.25px" 116 + target="_blank" 117 + >Bluesky</a>, the social internet</p> 118 + </td> 119 + <td 120 + data-id="__react-email-column" 121 + style="width:24px" 122 + ><img 123 + alt="๐Ÿฆ‹" 124 + src="https://bsky.social/about/images/email/email_mark_dark.png" 125 + style="display:block;outline:none;border:none;text-decoration:none;width:24px" 126 + /></td> 127 + </tr> 128 + </tbody> 129 + </table> 130 + </td> 131 + </tr> 132 + </tbody> 133 + </table> 134 + </td> 135 + </tr> 136 + </tbody> 137 + </table> 138 + <table 139 + align="center" 140 + width="100%" 141 + border="0" 142 + cellPadding="0" 143 + cellSpacing="0" 144 + role="presentation" 145 + style="height:500px" 146 + > 147 + <tbody> 148 + <tr> 149 + <td></td> 150 + </tr> 151 + </tbody> 152 + </table> 153 + </td> 154 + </tr> 155 + </tbody> 156 + </table> 157 + </body> 158 + 159 + </html>
+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>
images/gate.jpg

This is a binary file and will not be displayed.

+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 .
+6
migrations/20250814232704_two_factor_accounts.sql
··· 1 + -- Add migration script here 2 + CREATE TABLE IF NOT EXISTS two_factor_accounts 3 + ( 4 + did VARCHAR PRIMARY KEY, 5 + required INT2 NOT NULL 6 + );
+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);
+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 + }
+687
src/helpers.rs
··· 1 + use crate::AppState; 2 + use crate::helpers::TokenCheckError::InvalidToken; 3 + use anyhow::anyhow; 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 + }; 11 + use axum_template::TemplateEngine; 12 + use chrono::Utc; 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 + }; 23 + use rand::Rng; 24 + use serde::de::DeserializeOwned; 25 + use serde_json::{Map, Value}; 26 + use sha2::{Digest, Sha256}; 27 + use sqlx::SqlitePool; 28 + use std::sync::Arc; 29 + use tracing::{error, log}; 30 + 31 + ///Used to generate the email 2fa code 32 + const UPPERCASE_BASE32_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 33 + 34 + /// The result of a proxied call that attempts to parse JSON. 35 + pub enum ProxiedResult<T> { 36 + /// Successfully parsed JSON body along with original response headers. 37 + Parsed { value: T, _headers: HeaderMap }, 38 + /// Could not or should not parse: return the original (or rebuilt) response as-is. 39 + Passthrough(Response<Body>), 40 + } 41 + 42 + /// Proxy the incoming request to the PDS base URL plus the provided path and attempt to parse 43 + /// the successful response body as JSON into `T`. 44 + /// 45 + pub async fn proxy_get_json<T>( 46 + state: &AppState, 47 + mut req: Request, 48 + path: &str, 49 + ) -> Result<ProxiedResult<T>, StatusCode> 50 + where 51 + T: DeserializeOwned, 52 + { 53 + let uri = format!("{}{}", state.app_config.pds_base_url, path); 54 + *req.uri_mut() = Uri::try_from(uri).map_err(|_| StatusCode::BAD_REQUEST)?; 55 + 56 + let result = state 57 + .reverse_proxy_client 58 + .request(req) 59 + .await 60 + .map_err(|_| StatusCode::BAD_REQUEST)? 61 + .into_response(); 62 + 63 + if result.status() != StatusCode::OK { 64 + return Ok(ProxiedResult::Passthrough(result)); 65 + } 66 + 67 + let response_headers = result.headers().clone(); 68 + let body = result.into_body(); 69 + let body_bytes = to_bytes(body, usize::MAX) 70 + .await 71 + .map_err(|_| StatusCode::BAD_REQUEST)?; 72 + 73 + match serde_json::from_slice::<T>(&body_bytes) { 74 + Ok(value) => Ok(ProxiedResult::Parsed { 75 + value, 76 + _headers: response_headers, 77 + }), 78 + Err(err) => { 79 + error!(%err, "failed to parse proxied JSON response; returning original body"); 80 + let mut builder = Response::builder().status(StatusCode::OK); 81 + if let Some(headers) = builder.headers_mut() { 82 + *headers = response_headers; 83 + } 84 + let resp = builder 85 + .body(Body::from(body_bytes)) 86 + .map_err(|_| StatusCode::BAD_REQUEST)?; 87 + Ok(ProxiedResult::Passthrough(resp)) 88 + } 89 + } 90 + } 91 + 92 + /// Build a JSON error response with the required Content-Type header 93 + /// Content-Type: application/json;charset=utf-8 94 + /// Body shape: { "error": string, "message": string } 95 + pub fn json_error_response( 96 + status: StatusCode, 97 + error: impl Into<String>, 98 + message: impl Into<String>, 99 + ) -> Result<Response<Body>, StatusCode> { 100 + let body_str = match serde_json::to_string(&serde_json::json!({ 101 + "error": error.into(), 102 + "message": message.into(), 103 + })) { 104 + Ok(s) => s, 105 + Err(_) => return Err(StatusCode::BAD_REQUEST), 106 + }; 107 + 108 + Response::builder() 109 + .status(status) 110 + .header(CONTENT_TYPE, "application/json;charset=utf-8") 111 + .body(Body::from(body_str)) 112 + .map_err(|_| StatusCode::BAD_REQUEST) 113 + } 114 + 115 + /// Build a JSON error response with the required Content-Type header 116 + /// Content-Type: application/json (oauth endpoint does not like utf ending) 117 + /// Body shape: { "error": string, "error_description": string } 118 + pub fn oauth_json_error_response( 119 + status: StatusCode, 120 + error: impl Into<String>, 121 + message: impl Into<String>, 122 + ) -> Result<Response<Body>, StatusCode> { 123 + let body_str = match serde_json::to_string(&serde_json::json!({ 124 + "error": error.into(), 125 + "error_description": message.into(), 126 + })) { 127 + Ok(s) => s, 128 + Err(_) => return Err(StatusCode::BAD_REQUEST), 129 + }; 130 + 131 + Response::builder() 132 + .status(status) 133 + .header(CONTENT_TYPE, "application/json") 134 + .body(Body::from(body_str)) 135 + .map_err(|_| StatusCode::BAD_REQUEST) 136 + } 137 + 138 + /// Creates a random token of 10 characters for email 2FA 139 + pub fn get_random_token() -> String { 140 + let mut rng = rand::rng(); 141 + 142 + let mut full_code = String::with_capacity(10); 143 + for _ in 0..10 { 144 + let idx = rng.random_range(0..UPPERCASE_BASE32_CHARS.len()); 145 + full_code.push(UPPERCASE_BASE32_CHARS[idx] as char); 146 + } 147 + 148 + let slice_one = &full_code[0..5]; 149 + let slice_two = &full_code[5..10]; 150 + format!("{slice_one}-{slice_two}") 151 + } 152 + 153 + pub enum TokenCheckError { 154 + InvalidToken, 155 + ExpiredToken, 156 + } 157 + 158 + pub enum AuthResult { 159 + WrongIdentityOrPassword, 160 + /// The string here is the email address to create a hint for oauth 161 + TwoFactorRequired(String), 162 + /// User does not have 2FA enabled, or using an app password, or passes it 163 + ProxyThrough, 164 + TokenCheckFailed(TokenCheckError), 165 + } 166 + 167 + pub enum IdentifierType { 168 + Email, 169 + Did, 170 + Handle, 171 + } 172 + 173 + impl IdentifierType { 174 + fn what_is_it(identifier: String) -> Self { 175 + if identifier.contains("@") { 176 + IdentifierType::Email 177 + } else if identifier.contains("did:") { 178 + IdentifierType::Did 179 + } else { 180 + IdentifierType::Handle 181 + } 182 + } 183 + } 184 + 185 + /// Creates a hex string from the password and salt to find app passwords 186 + fn scrypt_hex(password: &str, salt: &str) -> anyhow::Result<String> { 187 + let params = scrypt::Params::new(14, 8, 1, 64)?; 188 + let mut derived = [0u8; 64]; 189 + scrypt::scrypt(password.as_bytes(), salt.as_bytes(), &params, &mut derived)?; 190 + Ok(hex::encode(derived)) 191 + } 192 + 193 + /// Hashes the app password. did is used as the salt. 194 + pub fn hash_app_password(did: &str, password: &str) -> anyhow::Result<String> { 195 + let mut hasher = Sha256::new(); 196 + hasher.update(did.as_bytes()); 197 + let sha = hasher.finalize(); 198 + let salt = hex::encode(&sha[..16]); 199 + let hash_hex = scrypt_hex(password, &salt)?; 200 + Ok(format!("{salt}:{hash_hex}")) 201 + } 202 + 203 + async fn verify_password(password: &str, password_scrypt: &str) -> anyhow::Result<bool> { 204 + // Expected format: "salt:hash" where hash is hex of scrypt(password, salt, 64 bytes) 205 + let mut parts = password_scrypt.splitn(2, ':'); 206 + let salt = match parts.next() { 207 + Some(s) if !s.is_empty() => s, 208 + _ => return Ok(false), 209 + }; 210 + let stored_hash_hex = match parts.next() { 211 + Some(h) if !h.is_empty() => h, 212 + _ => return Ok(false), 213 + }; 214 + 215 + // Derive using the shared helper and compare 216 + let derived_hex = match scrypt_hex(password, salt) { 217 + Ok(h) => h, 218 + Err(_) => return Ok(false), 219 + }; 220 + 221 + Ok(derived_hex.as_str() == stored_hash_hex) 222 + } 223 + 224 + /// Handles the auth checks along with sending a 2fa email 225 + pub async fn preauth_check( 226 + state: &AppState, 227 + identifier: &str, 228 + password: &str, 229 + two_factor_code: Option<String>, 230 + oauth: bool, 231 + ) -> anyhow::Result<AuthResult> { 232 + // Determine identifier type 233 + let id_type = IdentifierType::what_is_it(identifier.to_string()); 234 + 235 + // Query account DB for did and passwordScrypt based on identifier type 236 + let account_row: Option<(String, String, String, String)> = match id_type { 237 + IdentifierType::Email => { 238 + sqlx::query_as::<_, (String, String, String, String)>( 239 + "SELECT account.did, account.passwordScrypt, account.email, actor.handle 240 + FROM actor 241 + LEFT JOIN account ON actor.did = account.did 242 + where account.email = ? LIMIT 1", 243 + ) 244 + .bind(identifier) 245 + .fetch_optional(&state.account_pool) 246 + .await? 247 + } 248 + IdentifierType::Handle => { 249 + sqlx::query_as::<_, (String, String, String, String)>( 250 + "SELECT account.did, account.passwordScrypt, account.email, actor.handle 251 + FROM actor 252 + LEFT JOIN account ON actor.did = account.did 253 + where actor.handle = ? LIMIT 1", 254 + ) 255 + .bind(identifier) 256 + .fetch_optional(&state.account_pool) 257 + .await? 258 + } 259 + IdentifierType::Did => { 260 + sqlx::query_as::<_, (String, String, String, String)>( 261 + "SELECT account.did, account.passwordScrypt, account.email, actor.handle 262 + FROM actor 263 + LEFT JOIN account ON actor.did = account.did 264 + where account.did = ? LIMIT 1", 265 + ) 266 + .bind(identifier) 267 + .fetch_optional(&state.account_pool) 268 + .await? 269 + } 270 + }; 271 + 272 + if let Some((did, password_scrypt, email, handle)) = account_row { 273 + // Verify password before proceeding to 2FA email step 274 + let verified = verify_password(password, &password_scrypt).await?; 275 + if !verified { 276 + if oauth { 277 + //OAuth does not allow app password logins so just go ahead and send it along it's way 278 + return Ok(AuthResult::WrongIdentityOrPassword); 279 + } 280 + //Theres a chance it could be an app password so check that as well 281 + return match verify_app_password(&state.account_pool, &did, password).await { 282 + Ok(valid) => { 283 + if valid { 284 + //Was a valid app password up to the PDS now 285 + Ok(AuthResult::ProxyThrough) 286 + } else { 287 + Ok(AuthResult::WrongIdentityOrPassword) 288 + } 289 + } 290 + Err(err) => { 291 + log::error!("Error checking the app password: {err}"); 292 + Err(err) 293 + } 294 + }; 295 + } 296 + 297 + // Check two-factor requirement for this DID in the gatekeeper DB 298 + let required_opt = sqlx::query_as::<_, (u8,)>( 299 + "SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1", 300 + ) 301 + .bind(did.clone()) 302 + .fetch_optional(&state.pds_gatekeeper_pool) 303 + .await?; 304 + 305 + let two_factor_required = match required_opt { 306 + Some(row) => row.0 != 0, 307 + None => false, 308 + }; 309 + 310 + if two_factor_required { 311 + //Two factor is required and a taken was provided 312 + if let Some(two_factor_code) = two_factor_code { 313 + //if the two_factor_code is set need to see if we have a valid token 314 + if !two_factor_code.is_empty() { 315 + return match assert_valid_token( 316 + &state.account_pool, 317 + did.clone(), 318 + two_factor_code, 319 + ) 320 + .await 321 + { 322 + Ok(_) => { 323 + let result_of_cleanup = 324 + delete_all_email_tokens(&state.account_pool, did.clone()).await; 325 + if result_of_cleanup.is_err() { 326 + log::error!( 327 + "There was an error deleting the email tokens after login: {:?}", 328 + result_of_cleanup.err() 329 + ) 330 + } 331 + Ok(AuthResult::ProxyThrough) 332 + } 333 + Err(err) => Ok(AuthResult::TokenCheckFailed(err)), 334 + }; 335 + } 336 + } 337 + 338 + return match create_two_factor_token(&state.account_pool, did).await { 339 + Ok(code) => { 340 + let mut email_data = Map::new(); 341 + email_data.insert("token".to_string(), Value::from(code.clone())); 342 + email_data.insert("handle".to_string(), Value::from(handle.clone())); 343 + let email_body = state 344 + .template_engine 345 + .render("two_factor_code.hbs", email_data)?; 346 + 347 + let email_message = Message::builder() 348 + //TODO prob get the proper type in the state 349 + .from(state.app_config.mailer_from.parse()?) 350 + .to(email.parse()?) 351 + .subject(&state.app_config.email_subject) 352 + .multipart( 353 + MultiPart::alternative() // This is composed of two parts. 354 + .singlepart( 355 + SinglePart::builder() 356 + .header(header::ContentType::TEXT_PLAIN) 357 + .body(format!("We received a sign-in request for the account @{handle}. Use the code: {code} to sign in. If this wasn't you, we recommend taking steps to protect your account by changing your password at https://bsky.app/settings.")), // Every message should have a plain text fallback. 358 + ) 359 + .singlepart( 360 + SinglePart::builder() 361 + .header(header::ContentType::TEXT_HTML) 362 + .body(email_body), 363 + ), 364 + )?; 365 + match state.mailer.send(email_message).await { 366 + Ok(_) => Ok(AuthResult::TwoFactorRequired(mask_email(email))), 367 + Err(err) => { 368 + log::error!("Error sending the 2FA email: {err}"); 369 + Err(anyhow!(err)) 370 + } 371 + } 372 + } 373 + Err(err) => { 374 + log::error!("error on creating a 2fa token: {err}"); 375 + Err(anyhow!(err)) 376 + } 377 + }; 378 + } 379 + } 380 + 381 + // No local 2FA requirement (or account not found) 382 + Ok(AuthResult::ProxyThrough) 383 + } 384 + 385 + pub async fn create_two_factor_token( 386 + account_db: &SqlitePool, 387 + did: String, 388 + ) -> anyhow::Result<String> { 389 + let purpose = "2fa_code"; 390 + 391 + let token = get_random_token(); 392 + let right_now = Utc::now(); 393 + 394 + let res = sqlx::query( 395 + "INSERT INTO email_token (purpose, did, token, requestedAt) 396 + VALUES (?, ?, ?, ?) 397 + ON CONFLICT(purpose, did) DO UPDATE SET 398 + token=excluded.token, 399 + requestedAt=excluded.requestedAt 400 + WHERE did=excluded.did", 401 + ) 402 + .bind(purpose) 403 + .bind(&did) 404 + .bind(&token) 405 + .bind(right_now) 406 + .execute(account_db) 407 + .await; 408 + 409 + match res { 410 + Ok(_) => Ok(token), 411 + Err(err) => { 412 + log::error!("Error creating a two factor token: {err}"); 413 + Err(anyhow::anyhow!(err)) 414 + } 415 + } 416 + } 417 + 418 + pub async fn delete_all_email_tokens(account_db: &SqlitePool, did: String) -> anyhow::Result<()> { 419 + sqlx::query("DELETE FROM email_token WHERE did = ?") 420 + .bind(did) 421 + .execute(account_db) 422 + .await?; 423 + Ok(()) 424 + } 425 + 426 + pub async fn assert_valid_token( 427 + account_db: &SqlitePool, 428 + did: String, 429 + token: String, 430 + ) -> Result<(), TokenCheckError> { 431 + let token_upper = token.to_ascii_uppercase(); 432 + let purpose = "2fa_code"; 433 + 434 + let row: Option<(String,)> = sqlx::query_as( 435 + "SELECT requestedAt FROM email_token WHERE purpose = ? AND did = ? AND token = ? LIMIT 1", 436 + ) 437 + .bind(purpose) 438 + .bind(did) 439 + .bind(token_upper) 440 + .fetch_optional(account_db) 441 + .await 442 + .map_err(|err| { 443 + log::error!("Error getting the 2fa token: {err}"); 444 + InvalidToken 445 + })?; 446 + 447 + match row { 448 + None => Err(InvalidToken), 449 + Some(row) => { 450 + // Token lives for 15 minutes 451 + let expiration_ms = 15 * 60_000; 452 + 453 + let requested_at_utc = match chrono::DateTime::parse_from_rfc3339(&row.0) { 454 + Ok(dt) => dt.with_timezone(&Utc), 455 + Err(_) => { 456 + return Err(TokenCheckError::InvalidToken); 457 + } 458 + }; 459 + 460 + let now = Utc::now(); 461 + let age_ms = (now - requested_at_utc).num_milliseconds(); 462 + let expired = age_ms > expiration_ms; 463 + if expired { 464 + return Err(TokenCheckError::ExpiredToken); 465 + } 466 + 467 + Ok(()) 468 + } 469 + } 470 + } 471 + 472 + /// We just need to confirm if it's there or not. Will let the PDS do the actual figuring of permissions 473 + pub async fn verify_app_password( 474 + account_db: &SqlitePool, 475 + did: &str, 476 + password: &str, 477 + ) -> anyhow::Result<bool> { 478 + let password_scrypt = hash_app_password(did, password)?; 479 + 480 + let row: Option<(i64,)> = sqlx::query_as( 481 + "SELECT Count(*) FROM app_password WHERE did = ? AND passwordScrypt = ? LIMIT 1", 482 + ) 483 + .bind(did) 484 + .bind(password_scrypt) 485 + .fetch_optional(account_db) 486 + .await?; 487 + 488 + Ok(match row { 489 + None => false, 490 + Some((count,)) => count > 0, 491 + }) 492 + } 493 + 494 + /// Mask an email address into a hint like "2***0@p***m". 495 + pub fn mask_email(email: String) -> String { 496 + // Basic split on first '@' 497 + let mut parts = email.splitn(2, '@'); 498 + let local = match parts.next() { 499 + Some(l) => l, 500 + None => return email.to_string(), 501 + }; 502 + let domain_rest = match parts.next() { 503 + Some(d) if !d.is_empty() => d, 504 + _ => return email.to_string(), 505 + }; 506 + 507 + // Helper to mask a single label (keep first and last, middle becomes ***). 508 + fn mask_label(s: &str) -> String { 509 + let chars: Vec<char> = s.chars().collect(); 510 + match chars.len() { 511 + 0 => String::new(), 512 + 1 => format!("{}***", chars[0]), 513 + 2 => format!("{}***{}", chars[0], chars[1]), 514 + _ => format!("{}***{}", chars[0], chars[chars.len() - 1]), 515 + } 516 + } 517 + 518 + // Mask local 519 + let masked_local = mask_label(local); 520 + 521 + // Mask first domain label only, keep the rest of the domain intact 522 + let mut dom_parts = domain_rest.splitn(2, '.'); 523 + let first_label = dom_parts.next().unwrap_or(""); 524 + let rest = dom_parts.next(); 525 + let masked_first = mask_label(first_label); 526 + let masked_domain = if let Some(rest) = rest { 527 + format!("{}.{rest}", masked_first) 528 + } else { 529 + masked_first 530 + }; 531 + 532 + format!("{masked_local}@{masked_domain}") 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 + }
+387 -58
src/main.rs
··· 1 + #![warn(clippy::unwrap_used)] 2 + use crate::gate::{get_gate, post_gate}; 3 + use crate::oauth_provider::sign_in; 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 + }; 16 + use axum_template::engine::Engine; 17 + use handlebars::Handlebars; 18 + use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 19 + use jacquard_common::types::did::Did; 20 + use jacquard_identity::{PublicResolver, resolver::PlcSource}; 21 + use lettre::{AsyncSmtpTransport, Tokio1Executor}; 22 + use rand::Rng; 23 + use rust_embed::RustEmbed; 24 + use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; 25 + use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; 26 + use std::path::Path; 27 + use std::sync::Arc; 28 + use std::time::Duration; 1 29 use std::{env, net::SocketAddr}; 2 - use axum::{extract::State, routing::get, Json, Router}; 3 - // use dotenvy::dotenv; 4 - use serde::Serialize; 5 - use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; 6 - use tracing::{error, info, log}; 7 - use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 30 + use tower_governor::{ 31 + GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, 32 + }; 33 + use tower_http::{ 34 + compression::CompressionLayer, 35 + cors::{Any, CorsLayer}, 36 + }; 37 + use tracing::log; 38 + use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 8 39 40 + mod gate; 41 + pub mod helpers; 42 + mod middleware; 43 + mod oauth_provider; 9 44 mod xrpc; 10 45 46 + type HyperUtilClient = hyper_util::client::legacy::Client<HttpConnector, Body>; 47 + 48 + #[derive(RustEmbed)] 49 + #[folder = "email_templates"] 50 + #[include = "*.hbs"] 51 + struct EmailTemplates; 52 + 53 + #[derive(RustEmbed)] 54 + #[folder = "html_templates"] 55 + #[include = "*.hbs"] 56 + struct HtmlTemplates; 57 + 58 + /// Mostly the env variables that are used in the app 59 + #[derive(Clone, Debug)] 60 + pub struct AppConfig { 61 + pds_base_url: String, 62 + mailer_from: String, 63 + email_subject: String, 64 + allow_only_migrations: bool, 65 + use_captcha: bool, 66 + //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 67 + //that need to capture this redirect url for creating an account 68 + default_successful_redirect_url: String, 69 + pds_service_did: Did<'static>, 70 + gate_jwe_key: Vec<u8>, 71 + captcha_success_redirects: Vec<String>, 72 + } 73 + 74 + impl AppConfig { 75 + pub fn new() -> Self { 76 + let pds_base_url = 77 + env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); 78 + let mailer_from = env::var("PDS_EMAIL_FROM_ADDRESS") 79 + .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); 80 + //Hack not my favorite, but it does work 81 + let allow_only_migrations = env::var("GATEKEEPER_ALLOW_ONLY_MIGRATIONS") 82 + .map(|val| val.parse::<bool>().unwrap_or(false)) 83 + .unwrap_or(false); 84 + 85 + let use_captcha = env::var("GATEKEEPER_CREATE_ACCOUNT_CAPTCHA") 86 + .map(|val| val.parse::<bool>().unwrap_or(false)) 87 + .unwrap_or(false); 88 + 89 + // PDS_SERVICE_DID is the did:web if set, if not it's PDS_HOSTNAME 90 + let pds_service_did = 91 + env::var("PDS_SERVICE_DID").unwrap_or_else(|_| match env::var("PDS_HOSTNAME") { 92 + Ok(pds_hostname) => format!("did:web:{}", pds_hostname), 93 + Err(_) => { 94 + panic!("PDS_HOSTNAME or PDS_SERVICE_DID must be set in your pds.env file") 95 + } 96 + }); 97 + 98 + let email_subject = env::var("GATEKEEPER_TWO_FACTOR_EMAIL_SUBJECT") 99 + .unwrap_or("Sign in to Bluesky".to_string()); 100 + 101 + // Load or generate JWE encryption key (32 bytes for AES-256) 102 + let gate_jwe_key = env::var("GATEKEEPER_JWE_KEY") 103 + .ok() 104 + .and_then(|key_hex| hex::decode(key_hex).ok()) 105 + .unwrap_or_else(|| { 106 + // Generate a random 32-byte key if not provided 107 + let key: Vec<u8> = (0..32).map(|_| rand::rng().random()).collect(); 108 + log::warn!("WARNING: No GATEKEEPER_JWE_KEY found in the environment. Generated random key (hex): {}", hex::encode(&key)); 109 + 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)."); 110 + key 111 + }); 112 + 113 + if gate_jwe_key.len() != 32 { 114 + panic!( 115 + "GATEKEEPER_JWE_KEY must be 32 bytes (64 hex characters) for AES-256 encryption" 116 + ); 117 + } 118 + 119 + let captcha_success_redirects = match env::var("GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS") { 120 + Ok(from_env) => from_env.split(",").map(|s| s.trim().to_string()).collect(), 121 + Err(_) => { 122 + vec![ 123 + String::from("https://bsky.app"), 124 + String::from("https://pdsmoover.com"), 125 + String::from("https://blacksky.community"), 126 + String::from("https://tektite.cc"), 127 + ] 128 + } 129 + }; 130 + 131 + AppConfig { 132 + pds_base_url, 133 + mailer_from, 134 + email_subject, 135 + allow_only_migrations, 136 + use_captcha, 137 + default_successful_redirect_url: env::var("GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT") 138 + .unwrap_or("https://bsky.app".to_string()), 139 + pds_service_did: pds_service_did 140 + .parse() 141 + .expect("PDS_SERVICE_DID is not a valid did or could not infer from PDS_HOSTNAME"), 142 + gate_jwe_key, 143 + captcha_success_redirects, 144 + } 145 + } 146 + } 147 + 11 148 #[derive(Clone)] 12 - struct AppState { 13 - pool: SqlitePool, 149 + pub struct AppState { 150 + account_pool: SqlitePool, 151 + pds_gatekeeper_pool: SqlitePool, 152 + reverse_proxy_client: HyperUtilClient, 153 + mailer: AsyncSmtpTransport<Tokio1Executor>, 154 + template_engine: Engine<Handlebars<'static>>, 155 + resolver: Arc<PublicResolver>, 156 + app_config: AppConfig, 14 157 } 15 158 16 - #[derive(Serialize)] 17 - struct HealthResponse { 18 - status: &'static str, 19 - } 159 + async fn root_handler() -> impl axum::response::IntoResponse { 160 + let body = r" 161 + 162 + ...oO _.--X~~OO~~X--._ ...oOO 163 + _.-~ / \ II / \ ~-._ 164 + [].-~ \ / \||/ \ / ~-.[] ...o 165 + ...o _ ||/ \ / || \ / \|| _ 166 + (_) |X X || X X| (_) 167 + _-~-_ ||\ / \ || / \ /|| _-~-_ 168 + ||||| || \ / \ /||\ / \ / || ||||| 169 + | |_|| \ / \ / || \ / \ / ||_| | 170 + | |~|| X X || X X ||~| | 171 + ==============| | || / \ / \ || / \ / \ || | |============== 172 + ______________| | || / \ / \||/ \ / \ || | |______________ 173 + . . | | ||/ \ / || \ / \|| | | . . 174 + / | | |X X || X X| | | / / 175 + / . | | ||\ / \ || / \ /|| | | . / . 176 + . / | | || \ / \ /||\ / \ / || | | . . 177 + . . | | || \ / \ / || \ / \ / || | | . 178 + / | | || X X || X X || | | . / . / 179 + / . | | || / \ / \ || / \ / \ || | | / 180 + / | | || / \ / \||/ \ / \ || | | . / 181 + . . . | | ||/ \ / /||\ \ / \|| | | /. . 182 + | |_|X X / II \ X X|_| | . . / 183 + ==============| |~II~~~~~~~~~~~~~~OO~~~~~~~~~~~~~~II~| |============== 184 + "; 20 185 21 - #[derive(Serialize)] 22 - struct DbPingResponse { 23 - db: &'static str, 24 - value: i64, 186 + let intro = "\n\nThis is a PDS gatekeeper\n\nCode: https://tangled.sh/@baileytownsend.dev/pds-gatekeeper\n"; 187 + 188 + let banner = format!(" {body}\n{intro}"); 189 + 190 + ( 191 + [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], 192 + banner, 193 + ) 25 194 } 26 195 27 196 #[tokio::main] 28 197 async fn main() -> Result<(), Box<dyn std::error::Error>> { 29 198 setup_tracing(); 30 - //TODO prod 31 - // dotenvy::from_path(Path::new("/pds.env"))?; 32 - // let pds_root = env::var("PDS_DATA_DIRECTORY")?; 33 - let pds_root = "/home/baileytownsend/Documents/code/docker_compose/pds/pds_data"; 34 - let account_db_url = format!("{}/account.sqlite", pds_root); 35 - log::info!("accounts_db_url: {}", account_db_url); 36 - let max_connections: u32 = env::var("DATABASE_MAX_CONNECTIONS") 37 - .ok() 38 - .and_then(|s| s.parse().ok()) 39 - .unwrap_or(5); 199 + let pds_env_location = 200 + env::var("PDS_ENV_LOCATION").unwrap_or_else(|_| "/pds/pds.env".to_string()); 201 + 202 + let result_of_finding_pds_env = dotenvy::from_path(Path::new(&pds_env_location)); 203 + if let Err(e) = result_of_finding_pds_env { 204 + log::error!( 205 + "Error loading pds.env file (ignore if you loaded your variables in the environment somehow else): {e}" 206 + ); 207 + } 208 + 209 + let pds_root = 210 + env::var("PDS_DATA_DIRECTORY").expect("PDS_DATA_DIRECTORY is not set in your pds.env file"); 211 + let account_db_url = format!("{pds_root}/account.sqlite"); 212 + 213 + let account_options = SqliteConnectOptions::new() 214 + .filename(account_db_url) 215 + .busy_timeout(Duration::from_secs(5)); 216 + 217 + let account_pool = SqlitePoolOptions::new() 218 + .max_connections(5) 219 + .connect_with(account_options) 220 + .await?; 40 221 41 - //TODO may need to add journal_mode=WAL ? 42 - let pool = SqlitePoolOptions::new() 43 - .max_connections(max_connections) 44 - .connect(&account_db_url) 222 + let bells_db_url = format!("{pds_root}/pds_gatekeeper.sqlite"); 223 + let options = SqliteConnectOptions::new() 224 + .journal_mode(SqliteJournalMode::Wal) 225 + .filename(bells_db_url) 226 + .create_if_missing(true) 227 + .busy_timeout(Duration::from_secs(5)); 228 + let pds_gatekeeper_pool = SqlitePoolOptions::new() 229 + .max_connections(5) 230 + .connect_with(options) 45 231 .await?; 46 232 47 - let state = AppState { pool }; 233 + // Run migrations for the extra database 234 + // Note: the migrations are embedded at compile time from the given directory 235 + // sqlx 236 + sqlx::migrate!("./migrations") 237 + .run(&pds_gatekeeper_pool) 238 + .await?; 48 239 49 - let app = Router::new() 50 - .route("/health", get(health)) 51 - .route("/db/ping", get(db_ping)) 52 - .with_state(state); 240 + let client: HyperUtilClient = 241 + hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new()) 242 + .build(HttpConnector::new()); 243 + 244 + //Emailer set up 245 + let smtp_url = 246 + env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 247 + 248 + let mailer: AsyncSmtpTransport<Tokio1Executor> = 249 + AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 250 + //Email templates setup 251 + let mut hbs = Handlebars::new(); 252 + 253 + let users_email_directory = env::var("GATEKEEPER_EMAIL_TEMPLATES_DIRECTORY"); 254 + if let Ok(users_email_directory) = users_email_directory { 255 + hbs.register_template_file( 256 + "two_factor_code.hbs", 257 + format!("{users_email_directory}/two_factor_code.hbs"), 258 + )?; 259 + } else { 260 + let _ = hbs.register_embed_templates::<EmailTemplates>(); 261 + } 262 + 263 + let _ = hbs.register_embed_templates::<HtmlTemplates>(); 264 + 265 + //Reads the PLC source from the pds env's or defaults to ol faithful 266 + let plc_source_url = 267 + env::var("PDS_DID_PLC_URL").unwrap_or_else(|_| "https://plc.directory".to_string()); 268 + let plc_source = PlcSource::PlcDirectory { 269 + base: plc_source_url.parse().unwrap(), 270 + }; 271 + let mut resolver = PublicResolver::default(); 272 + resolver = resolver.with_plc_source(plc_source.clone()); 53 273 54 - let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 55 - let port: u16 = env::var("PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(8080); 56 - let addr: SocketAddr = format!("{host}:{port}").parse().expect("valid socket address"); 274 + let state = AppState { 275 + account_pool, 276 + pds_gatekeeper_pool, 277 + reverse_proxy_client: client, 278 + mailer, 279 + template_engine: Engine::from(hbs), 280 + resolver: Arc::new(resolver), 281 + app_config: AppConfig::new(), 282 + }; 57 283 58 - info!(%addr, %account_db_url, "starting server"); 284 + // Rate limiting 285 + //Allows 5 within 60 seconds, and after 60 should drop one off? So hit 5, then goes to 4 after 60 seconds. 286 + let captcha_governor_conf = GovernorConfigBuilder::default() 287 + .per_second(60) 288 + .burst_size(5) 289 + .key_extractor(SmartIpKeyExtractor) 290 + .finish() 291 + .expect("failed to create governor config for create session. this should not happen and is a bug"); 59 292 60 - let listener = tokio::net::TcpListener::bind(addr).await?; 293 + // Create a second config with the same settings for the other endpoint 294 + let sign_in_governor_conf = GovernorConfigBuilder::default() 295 + .per_second(60) 296 + .burst_size(5) 297 + .key_extractor(SmartIpKeyExtractor) 298 + .finish() 299 + .expect( 300 + "failed to create governor config for sign in. this should not happen and is a bug", 301 + ); 61 302 62 - let server = axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()); 303 + let create_account_limiter_time: Option<String> = 304 + env::var("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND").ok(); 305 + let create_account_limiter_burst: Option<String> = 306 + env::var("GATEKEEPER_CREATE_ACCOUNT_BURST").ok(); 63 307 64 - if let Err(err) = server.await { 65 - error!(error = %err, "server error"); 308 + //Default should be 608 requests per 5 minutes, PDS is 300 per 500 so will never hit it ideally 309 + let mut create_account_governor_conf = GovernorConfigBuilder::default(); 310 + if create_account_limiter_time.is_some() { 311 + let time = create_account_limiter_time 312 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND not set") 313 + .parse::<u64>() 314 + .expect("GATEKEEPER_CREATE_ACCOUNT_PER_SECOND must be a valid integer"); 315 + create_account_governor_conf.per_second(time); 66 316 } 67 317 68 - Ok(()) 69 - } 318 + if create_account_limiter_burst.is_some() { 319 + let burst = create_account_limiter_burst 320 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST not set") 321 + .parse::<u32>() 322 + .expect("GATEKEEPER_CREATE_ACCOUNT_BURST must be a valid integer"); 323 + create_account_governor_conf.burst_size(burst); 324 + } 325 + 326 + let create_account_governor_conf = create_account_governor_conf 327 + .key_extractor(SmartIpKeyExtractor) 328 + .finish().expect( 329 + "failed to create governor config for create account. this should not happen and is a bug", 330 + ); 331 + 332 + let captcha_governor_limiter = captcha_governor_conf.limiter().clone(); 333 + let sign_in_governor_limiter = sign_in_governor_conf.limiter().clone(); 334 + let create_account_governor_limiter = create_account_governor_conf.limiter().clone(); 335 + 336 + let sign_in_governor_layer = GovernorLayer::new(sign_in_governor_conf); 337 + 338 + let interval = Duration::from_secs(60); 339 + // a separate background task to clean up 340 + std::thread::spawn(move || { 341 + loop { 342 + std::thread::sleep(interval); 343 + captcha_governor_limiter.retain_recent(); 344 + sign_in_governor_limiter.retain_recent(); 345 + create_account_governor_limiter.retain_recent(); 346 + } 347 + }); 348 + 349 + let cors = CorsLayer::new() 350 + .allow_origin(Any) 351 + .allow_methods([Method::GET, Method::OPTIONS, Method::POST]) 352 + .allow_headers(Any); 353 + 354 + let mut app = Router::new() 355 + .route("/", get(root_handler)) 356 + .route("/xrpc/com.atproto.server.getSession", get(get_session)) 357 + .route( 358 + "/xrpc/com.atproto.server.describeServer", 359 + get(describe_server), 360 + ) 361 + .route( 362 + "/xrpc/com.atproto.server.updateEmail", 363 + post(update_email).layer(ax_middleware::from_fn(middleware::extract_did)), 364 + ) 365 + .route( 366 + "/@atproto/oauth-provider/~api/sign-in", 367 + post(sign_in).layer(sign_in_governor_layer.clone()), 368 + ) 369 + .route( 370 + "/xrpc/com.atproto.server.createSession", 371 + post(create_session.layer(sign_in_governor_layer)), 372 + ) 373 + .route( 374 + "/xrpc/com.atproto.server.createAccount", 375 + post(create_account).layer(GovernorLayer::new(create_account_governor_conf)), 376 + ); 377 + 378 + if state.app_config.use_captcha { 379 + app = app.route( 380 + "/gate/signup", 381 + get(get_gate).post(post_gate.layer(GovernorLayer::new(captcha_governor_conf))), 382 + ); 383 + } 384 + 385 + let app = app 386 + .layer(CompressionLayer::new()) 387 + .layer(cors) 388 + .with_state(state); 389 + 390 + let host = env::var("GATEKEEPER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 391 + let port: u16 = env::var("GATEKEEPER_PORT") 392 + .ok() 393 + .and_then(|s| s.parse().ok()) 394 + .unwrap_or(8080); 395 + let addr: SocketAddr = format!("{host}:{port}") 396 + .parse() 397 + .expect("valid socket address"); 398 + 399 + let listener = tokio::net::TcpListener::bind(addr).await?; 70 400 71 - async fn health() -> Json<HealthResponse> { 72 - Json(HealthResponse { status: "ok" }) 73 - } 401 + let server = axum::serve( 402 + listener, 403 + app.into_make_service_with_connect_info::<SocketAddr>(), 404 + ) 405 + .with_graceful_shutdown(shutdown_signal()); 74 406 75 - async fn db_ping(State(state): State<AppState>) -> Result<Json<DbPingResponse>, axum::http::StatusCode> { 76 - // Run a DB-agnostic ping that doesn't depend on user tables. 77 - // In SQLite, SELECT 1 returns a single row with value 1. 78 - let v: i64 = sqlx::query_scalar("SELECT 1") 79 - .fetch_one(&state.pool) 80 - .await 81 - .map_err(|_| axum::http::StatusCode::SERVICE_UNAVAILABLE)?; 407 + if let Err(err) = server.await { 408 + log::error!("server error:{err}"); 409 + } 82 410 83 - Ok(Json(DbPingResponse { db: "ok", value: v })) 411 + Ok(()) 84 412 } 85 413 86 414 fn setup_tracing() { ··· 101 429 102 430 #[cfg(unix)] 103 431 let terminate = async { 104 - use tokio::signal::unix::{signal, SignalKind}; 432 + use tokio::signal::unix::{SignalKind, signal}; 105 433 106 - let mut sigterm = signal(SignalKind::terminate()).expect("failed to install signal handler"); 434 + let mut sigterm = 435 + signal(SignalKind::terminate()).expect("failed to install signal handler"); 107 436 sigterm.recv().await; 108 437 }; 109 438
+116
src/middleware.rs
··· 1 + use crate::helpers::json_error_response; 2 + use axum::extract::Request; 3 + use axum::http::{HeaderMap, StatusCode}; 4 + use axum::middleware::Next; 5 + use axum::response::IntoResponse; 6 + use jwt_compact::alg::{Hs256, Hs256Key}; 7 + use jwt_compact::{AlgorithmExt, Claims, Token, UntrustedToken, ValidationError}; 8 + use serde::{Deserialize, Serialize}; 9 + use std::env; 10 + use tracing::log; 11 + 12 + #[derive(Clone, Debug)] 13 + pub struct Did(pub Option<String>); 14 + 15 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 + pub enum AuthScheme { 17 + Bearer, 18 + DPoP, 19 + } 20 + 21 + #[derive(Serialize, Deserialize)] 22 + pub struct TokenClaims { 23 + pub sub: String, 24 + } 25 + 26 + pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 27 + let auth = extract_auth(req.headers()); 28 + 29 + match auth { 30 + Ok(auth_opt) => { 31 + match auth_opt { 32 + None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 33 + .expect("Error creating an error response"), 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 + } 59 + 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 + } 82 + } 83 + 84 + next.run(req).await 85 + } 86 + } 87 + } 88 + Err(err) => { 89 + log::error!("Error extracting token: {err}"); 90 + json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 91 + .expect("Error creating an error response") 92 + } 93 + } 94 + } 95 + 96 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 97 + match headers.get(axum::http::header::AUTHORIZATION) { 98 + None => Ok(None), 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 + } 112 + } 113 + } 114 + } 115 + } 116 + }
+139
src/oauth_provider.rs
··· 1 + use crate::AppState; 2 + use crate::helpers::{AuthResult, oauth_json_error_response, preauth_check}; 3 + use axum::body::Body; 4 + use axum::extract::State; 5 + use axum::http::header::CONTENT_TYPE; 6 + use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; 7 + use axum::response::{IntoResponse, Response}; 8 + use axum::{Json, extract}; 9 + use serde::{Deserialize, Serialize}; 10 + use tracing::log; 11 + 12 + #[derive(Serialize, Deserialize, Clone)] 13 + pub struct SignInRequest { 14 + pub username: String, 15 + pub password: String, 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + pub remember: Option<bool>, 18 + pub locale: String, 19 + #[serde(skip_serializing_if = "Option::is_none", rename = "emailOtp")] 20 + pub email_otp: Option<String>, 21 + } 22 + 23 + pub async fn sign_in( 24 + State(state): State<AppState>, 25 + headers: HeaderMap, 26 + Json(mut payload): extract::Json<SignInRequest>, 27 + ) -> Result<Response<Body>, StatusCode> { 28 + let identifier = payload.username.clone(); 29 + let password = payload.password.clone(); 30 + let auth_factor_token = payload.email_otp.clone(); 31 + 32 + match preauth_check(&state, &identifier, &password, auth_factor_token, true).await { 33 + Ok(result) => match result { 34 + AuthResult::WrongIdentityOrPassword => oauth_json_error_response( 35 + StatusCode::BAD_REQUEST, 36 + "invalid_request", 37 + "Invalid identifier or password", 38 + ), 39 + AuthResult::TwoFactorRequired(masked_email) => { 40 + let body_str = match serde_json::to_string(&serde_json::json!({ 41 + "error": "second_authentication_factor_required", 42 + "error_description": format!("emailOtp authentication factor required (hint: {})", masked_email), 43 + "type": "emailOtp", 44 + "hint": masked_email, 45 + })) { 46 + Ok(s) => s, 47 + Err(_) => return Err(StatusCode::BAD_REQUEST), 48 + }; 49 + 50 + Response::builder() 51 + .status(StatusCode::BAD_REQUEST) 52 + .header(CONTENT_TYPE, "application/json") 53 + .body(Body::from(body_str)) 54 + .map_err(|_| StatusCode::BAD_REQUEST) 55 + } 56 + AuthResult::ProxyThrough => { 57 + //No 2FA or already passed 58 + let uri = format!( 59 + "{}{}", 60 + state.app_config.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" 61 + ); 62 + 63 + let mut req = axum::http::Request::post(uri); 64 + if let Some(req_headers) = req.headers_mut() { 65 + // Copy headers but remove problematic ones. There was an issue with the PDS not parsing the body fully if i forwarded all headers 66 + copy_filtered_headers(&headers, req_headers); 67 + //Setting the content type to application/json manually 68 + req_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 69 + } 70 + 71 + //Clears the email_otp because the pds will reject a request with it. 72 + payload.email_otp = None; 73 + let payload_bytes = 74 + serde_json::to_vec(&payload).map_err(|_| StatusCode::BAD_REQUEST)?; 75 + 76 + let req = req 77 + .body(Body::from(payload_bytes)) 78 + .map_err(|_| StatusCode::BAD_REQUEST)?; 79 + 80 + let proxied = state 81 + .reverse_proxy_client 82 + .request(req) 83 + .await 84 + .map_err(|_| StatusCode::BAD_REQUEST)? 85 + .into_response(); 86 + 87 + Ok(proxied) 88 + } 89 + //Ignoring the type of token check failure. Looks like oauth on the entry treads them the same. 90 + AuthResult::TokenCheckFailed(_) => oauth_json_error_response( 91 + StatusCode::BAD_REQUEST, 92 + "invalid_request", 93 + "Unable to sign-in due to an unexpected server error", 94 + ), 95 + }, 96 + Err(err) => { 97 + log::error!( 98 + "Error during pre-auth check. This happens on the oauth signin endpoint when trying to decide if the user has access:\n {err}" 99 + ); 100 + oauth_json_error_response( 101 + StatusCode::BAD_REQUEST, 102 + "pds_gatekeeper_error", 103 + "This error was not generated by the PDS, but PDS Gatekeeper. Please contact your PDS administrator for help and for them to review the server logs.", 104 + ) 105 + } 106 + } 107 + } 108 + 109 + fn is_disallowed_header(name: &HeaderName) -> bool { 110 + // possible problematic headers with proxying 111 + matches!( 112 + name.as_str(), 113 + "connection" 114 + | "keep-alive" 115 + | "proxy-authenticate" 116 + | "proxy-authorization" 117 + | "te" 118 + | "trailer" 119 + | "transfer-encoding" 120 + | "upgrade" 121 + | "host" 122 + | "content-length" 123 + | "content-encoding" 124 + | "expect" 125 + | "accept-encoding" 126 + ) 127 + } 128 + 129 + fn copy_filtered_headers(src: &HeaderMap, dst: &mut HeaderMap) { 130 + for (name, value) in src.iter() { 131 + if is_disallowed_header(name) { 132 + continue; 133 + } 134 + // Only copy valid headers 135 + if let Ok(hv) = HeaderValue::from_bytes(value.as_bytes()) { 136 + dst.insert(name.clone(), hv); 137 + } 138 + } 139 + }
+597 -17
src/xrpc/com_atproto_server.rs
··· 1 + use crate::AppState; 2 + use crate::helpers::{ 3 + AuthResult, ProxiedResult, TokenCheckError, VerifyServiceAuthError, json_error_response, 4 + preauth_check, proxy_get_json, verify_gate_token, verify_service_auth, 5 + }; 6 + use crate::middleware::Did; 7 + use axum::body::{Body, to_bytes}; 1 8 use axum::extract::State; 2 - use axum::{extract, Json}; 3 - use serde::Deserialize; 4 - use crate::{AppState, DbPingResponse}; 9 + use axum::http::{HeaderMap, StatusCode, header}; 10 + use axum::response::{IntoResponse, Response}; 11 + use axum::{Extension, Json, debug_handler, extract, extract::Request}; 12 + use chrono::{Duration, Utc}; 13 + use jacquard_common::types::did::Did as JacquardDid; 14 + use serde::{Deserialize, Serialize}; 15 + use serde_json; 16 + use tracing::log; 17 + 18 + #[derive(Serialize, Deserialize, Debug, Clone)] 19 + #[serde(rename_all = "camelCase")] 20 + enum AccountStatus { 21 + Takendown, 22 + Suspended, 23 + Deactivated, 24 + } 25 + 26 + #[derive(Serialize, Deserialize, Debug, Clone)] 27 + #[serde(rename_all = "camelCase")] 28 + struct GetSessionResponse { 29 + handle: String, 30 + did: String, 31 + #[serde(skip_serializing_if = "Option::is_none")] 32 + email: Option<String>, 33 + #[serde(skip_serializing_if = "Option::is_none")] 34 + email_confirmed: Option<bool>, 35 + #[serde(skip_serializing_if = "Option::is_none")] 36 + email_auth_factor: Option<bool>, 37 + #[serde(skip_serializing_if = "Option::is_none")] 38 + did_doc: Option<String>, 39 + #[serde(skip_serializing_if = "Option::is_none")] 40 + active: Option<bool>, 41 + #[serde(skip_serializing_if = "Option::is_none")] 42 + status: Option<AccountStatus>, 43 + } 5 44 45 + #[derive(Serialize, Deserialize, Debug, Clone)] 46 + #[serde(rename_all = "camelCase")] 47 + pub struct UpdateEmailResponse { 48 + email: String, 49 + #[serde(skip_serializing_if = "Option::is_none")] 50 + email_auth_factor: Option<bool>, 51 + #[serde(skip_serializing_if = "Option::is_none")] 52 + token: Option<String>, 53 + } 6 54 7 - #[derive(Deserialize)] 8 - struct CreateSessionRequest { 55 + #[allow(dead_code)] 56 + #[derive(Deserialize, Serialize)] 57 + #[serde(rename_all = "camelCase")] 58 + pub struct CreateSessionRequest { 9 59 identifier: String, 10 60 password: String, 11 - #[serde(rename = "authFactorToken")] 12 - auth_factor_token: String, 13 - #[serde(rename = "allowTakendown")] 14 - allow_takendown: bool, 61 + #[serde(skip_serializing_if = "Option::is_none")] 62 + auth_factor_token: Option<String>, 63 + #[serde(skip_serializing_if = "Option::is_none")] 64 + allow_takendown: Option<bool>, 65 + } 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 + 118 + pub async fn create_session( 119 + State(state): State<AppState>, 120 + headers: HeaderMap, 121 + Json(payload): extract::Json<CreateSessionRequest>, 122 + ) -> Result<Response<Body>, StatusCode> { 123 + let identifier = payload.identifier.clone(); 124 + let password = payload.password.clone(); 125 + let auth_factor_token = payload.auth_factor_token.clone(); 126 + 127 + // Run the shared pre-auth logic to validate and check 2FA requirement 128 + match preauth_check(&state, &identifier, &password, auth_factor_token, false).await { 129 + Ok(result) => match result { 130 + AuthResult::WrongIdentityOrPassword => json_error_response( 131 + StatusCode::UNAUTHORIZED, 132 + "AuthenticationRequired", 133 + "Invalid identifier or password", 134 + ), 135 + AuthResult::TwoFactorRequired(_) => { 136 + // Email sending step can be handled here if needed in the future. 137 + json_error_response( 138 + StatusCode::UNAUTHORIZED, 139 + "AuthFactorTokenRequired", 140 + "A sign in code has been sent to your email address", 141 + ) 142 + } 143 + AuthResult::ProxyThrough => { 144 + //No 2FA or already passed 145 + let uri = format!( 146 + "{}{}", 147 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createSession" 148 + ); 149 + 150 + let mut req = axum::http::Request::post(uri); 151 + if let Some(req_headers) = req.headers_mut() { 152 + req_headers.extend(headers.clone()); 153 + } 154 + 155 + let payload_bytes = 156 + serde_json::to_vec(&payload).map_err(|_| StatusCode::BAD_REQUEST)?; 157 + let req = req 158 + .body(Body::from(payload_bytes)) 159 + .map_err(|_| StatusCode::BAD_REQUEST)?; 160 + 161 + let proxied = state 162 + .reverse_proxy_client 163 + .request(req) 164 + .await 165 + .map_err(|_| StatusCode::BAD_REQUEST)? 166 + .into_response(); 167 + 168 + Ok(proxied) 169 + } 170 + AuthResult::TokenCheckFailed(err) => match err { 171 + TokenCheckError::InvalidToken => { 172 + json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "Token is invalid") 173 + } 174 + TokenCheckError::ExpiredToken => { 175 + json_error_response(StatusCode::BAD_REQUEST, "ExpiredToken", "Token is expired") 176 + } 177 + }, 178 + }, 179 + Err(err) => { 180 + log::error!( 181 + "Error during pre-auth check. This happens on the create_session endpoint when trying to decide if the user has access:\n {err}" 182 + ); 183 + json_error_response( 184 + StatusCode::INTERNAL_SERVER_ERROR, 185 + "InternalServerError", 186 + "This error was not generated by the PDS, but PDS Gatekeeper. Please contact your PDS administrator for help and for them to review the server logs.", 187 + ) 188 + } 189 + } 15 190 } 16 191 17 - async fn create_session(State(state): State<AppState>, extract::Json(payload): extract::Json<CreateSessionRequest>) -> Result<Json<DbPingResponse>, axum::http::StatusCode> { 18 - // Run a DB-agnostic ping that doesn't depend on user tables. 19 - // In SQLite, SELECT 1 returns a single row with value 1. 20 - let v: i64 = sqlx::query_scalar("SELECT 1") 21 - .fetch_one(&state.pool) 192 + #[debug_handler] 193 + pub async fn update_email( 194 + State(state): State<AppState>, 195 + Extension(did): Extension<Did>, 196 + headers: HeaderMap, 197 + Json(payload): extract::Json<UpdateEmailResponse>, 198 + ) -> Result<Response<Body>, StatusCode> { 199 + //If email auth is not set at all it is a update email address 200 + let email_auth_not_set = payload.email_auth_factor.is_none(); 201 + //If email auth is set it is to either turn on or off 2fa 202 + let email_auth_update = payload.email_auth_factor.unwrap_or(false); 203 + 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(); 207 + 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 = ?", 214 + ) 215 + .bind(&email) 216 + .fetch_optional(&state.account_pool) 217 + .await 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 + }; 225 + 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(|err| { 237 + log::error!("Error enabling 2FA: {err}"); 238 + StatusCode::BAD_REQUEST 239 + })?; 240 + 241 + Ok(StatusCode::OK.into_response()) 242 + } 243 + }; 244 + } 245 + 246 + // User wants auth turned off 247 + if !email_auth_update && !email_auth_not_set { 248 + //User wants auth turned off and has a token 249 + if let Some(token) = &payload.token { 250 + let token_found = match sqlx::query_as::<_, (String,)>( 251 + "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 252 + ) 253 + .bind(token) 254 + .bind(&did.0) 255 + .fetch_optional(&state.account_pool) 256 + .await{ 257 + Ok(token) => token, 258 + Err(err) => { 259 + log::error!("Error checking if token is valid: {err}"); 260 + return Err(StatusCode::BAD_REQUEST); 261 + } 262 + }; 263 + 264 + return if token_found.is_some() { 265 + //TODO I think there may be a bug here and need to do some retry logic 266 + // First try was erroring, seconds was allowing 267 + match sqlx::query( 268 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 269 + ) 270 + .bind(&did.0) 271 + .execute(&state.pds_gatekeeper_pool) 272 + .await { 273 + Ok(_) => {} 274 + Err(err) => { 275 + log::error!("Error updating email auth: {err}"); 276 + return Err(StatusCode::BAD_REQUEST); 277 + } 278 + } 279 + 280 + Ok(StatusCode::OK.into_response()) 281 + } else { 282 + Err(StatusCode::BAD_REQUEST) 283 + }; 284 + } 285 + } 286 + } 287 + // Updating the actual email address by sending it on to the PDS 288 + let uri = format!( 289 + "{}{}", 290 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.updateEmail" 291 + ); 292 + let mut req = axum::http::Request::post(uri); 293 + if let Some(req_headers) = req.headers_mut() { 294 + req_headers.extend(headers.clone()); 295 + } 296 + 297 + let payload_bytes = serde_json::to_vec(&payload).map_err(|_| StatusCode::BAD_REQUEST)?; 298 + let req = req 299 + .body(Body::from(payload_bytes)) 300 + .map_err(|_| StatusCode::BAD_REQUEST)?; 301 + 302 + let proxied = state 303 + .reverse_proxy_client 304 + .request(req) 22 305 .await 23 - .map_err(|_| axum::http::StatusCode::SERVICE_UNAVAILABLE)?; 306 + .map_err(|_| StatusCode::BAD_REQUEST)? 307 + .into_response(); 308 + 309 + Ok(proxied) 310 + } 311 + 312 + pub async fn get_session( 313 + State(state): State<AppState>, 314 + req: Request, 315 + ) -> Result<Response<Body>, StatusCode> { 316 + match proxy_get_json::<GetSessionResponse>(&state, req, "/xrpc/com.atproto.server.getSession") 317 + .await? 318 + { 319 + ProxiedResult::Parsed { 320 + value: mut session, .. 321 + } => { 322 + let did = session.did.clone(); 323 + let required_opt = sqlx::query_as::<_, (u8,)>( 324 + "SELECT required FROM two_factor_accounts WHERE did = ? LIMIT 1", 325 + ) 326 + .bind(&did) 327 + .fetch_optional(&state.pds_gatekeeper_pool) 328 + .await 329 + .map_err(|_| StatusCode::BAD_REQUEST)?; 330 + 331 + let email_auth_factor = match required_opt { 332 + Some(row) => row.0 != 0, 333 + None => false, 334 + }; 335 + 336 + session.email_auth_factor = Some(email_auth_factor); 337 + Ok(Json(session).into_response()) 338 + } 339 + ProxiedResult::Passthrough(resp) => Ok(resp), 340 + } 341 + } 342 + 343 + pub async fn describe_server( 344 + State(state): State<AppState>, 345 + req: Request, 346 + ) -> Result<Response<Body>, StatusCode> { 347 + match proxy_get_json::<DescribeServerResponse>( 348 + &state, 349 + req, 350 + "/xrpc/com.atproto.server.describeServer", 351 + ) 352 + .await? 353 + { 354 + ProxiedResult::Parsed { 355 + value: mut server_info, 356 + .. 357 + } => { 358 + //This signifies the server is configured for captcha verification 359 + server_info.phone_verification_required = Some(state.app_config.use_captcha); 360 + Ok(Json(server_info).into_response()) 361 + } 362 + ProxiedResult::Passthrough(resp) => Ok(resp), 363 + } 364 + } 365 + 366 + /// Verify a gate code matches the handle and is not expired 367 + async fn verify_gate_code( 368 + state: &AppState, 369 + code: &str, 370 + handle: &str, 371 + ) -> Result<bool, anyhow::Error> { 372 + // First, decrypt and verify the JWE token 373 + let payload = match verify_gate_token(code, &state.app_config.gate_jwe_key) { 374 + Ok(p) => p, 375 + Err(e) => { 376 + log::warn!("Failed to decrypt gate token: {}", e); 377 + return Ok(false); 378 + } 379 + }; 380 + 381 + // Verify the handle matches 382 + if payload.handle != handle { 383 + log::warn!( 384 + "Gate code handle mismatch: expected {}, got {}", 385 + handle, 386 + payload.handle 387 + ); 388 + return Ok(false); 389 + } 390 + 391 + let created_at = chrono::DateTime::parse_from_rfc3339(&payload.created_at) 392 + .map_err(|e| anyhow::anyhow!("Failed to parse created_at from token: {}", e))? 393 + .with_timezone(&Utc); 394 + 395 + let now = Utc::now(); 396 + let age = now - created_at; 397 + 398 + // Check if the token is expired (5 minutes) 399 + if age > Duration::minutes(5) { 400 + log::warn!("Gate code expired for handle {}", handle); 401 + return Ok(false); 402 + } 403 + 404 + // Verify the token exists in the database (to prevent reuse) 405 + let row: Option<(String,)> = 406 + sqlx::query_as("SELECT code FROM gate_codes WHERE code = ? and handle = ? LIMIT 1") 407 + .bind(code) 408 + .bind(handle) 409 + .fetch_optional(&state.pds_gatekeeper_pool) 410 + .await?; 411 + 412 + if row.is_none() { 413 + log::warn!("Gate code not found in database or already used"); 414 + return Ok(false); 415 + } 416 + 417 + // Token is valid, delete it so it can't be reused 418 + //TODO probably also delete expired codes? Will need to do that at some point probably altho the where is on code and handle 419 + 420 + sqlx::query("DELETE FROM gate_codes WHERE code = ?") 421 + .bind(code) 422 + .execute(&state.pds_gatekeeper_pool) 423 + .await?; 424 + 425 + Ok(true) 426 + } 427 + 428 + pub async fn create_account( 429 + State(state): State<AppState>, 430 + req: Request, 431 + ) -> Result<Response<Body>, StatusCode> { 432 + let headers = req.headers().clone(); 433 + let body_bytes = to_bytes(req.into_body(), usize::MAX) 434 + .await 435 + .map_err(|_| StatusCode::BAD_REQUEST)?; 436 + 437 + // Parse the body to check for verification code 438 + let account_request: CreateAccountRequest = 439 + serde_json::from_slice(&body_bytes).map_err(|e| { 440 + log::error!("Failed to parse create account request: {}", e); 441 + StatusCode::BAD_REQUEST 442 + })?; 443 + 444 + // Check for service auth (migrations) if configured 445 + if state.app_config.allow_only_migrations { 446 + // Expect Authorization: Bearer <jwt> 447 + let auth_header = headers 448 + .get(header::AUTHORIZATION) 449 + .and_then(|v| v.to_str().ok()) 450 + .map(str::to_string); 451 + 452 + let Some(value) = auth_header else { 453 + log::error!("No Authorization header found in the request"); 454 + return json_error_response( 455 + StatusCode::UNAUTHORIZED, 456 + "InvalidAuth", 457 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 458 + ); 459 + }; 460 + 461 + // Ensure Bearer prefix 462 + let token = value.strip_prefix("Bearer ").unwrap_or("").trim(); 463 + if token.is_empty() { 464 + log::error!("No Service Auth token found in the Authorization header"); 465 + return json_error_response( 466 + StatusCode::UNAUTHORIZED, 467 + "InvalidAuth", 468 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 469 + ); 470 + } 471 + 472 + // Ensure a non-empty DID was provided when migrations are enabled 473 + let requested_did_str = match account_request.did.as_deref() { 474 + Some(s) if !s.trim().is_empty() => s, 475 + _ => { 476 + return json_error_response( 477 + StatusCode::BAD_REQUEST, 478 + "InvalidRequest", 479 + "The 'did' field is required when migrations are enforced.", 480 + ); 481 + } 482 + }; 483 + 484 + // Parse the DID into the expected type for verification 485 + let requested_did: JacquardDid<'static> = match requested_did_str.parse() { 486 + Ok(d) => d, 487 + Err(e) => { 488 + log::error!( 489 + "Invalid DID format provided in createAccount: {} | error: {}", 490 + requested_did_str, 491 + e 492 + ); 493 + return json_error_response( 494 + StatusCode::BAD_REQUEST, 495 + "InvalidRequest", 496 + "The 'did' field is not a valid DID.", 497 + ); 498 + } 499 + }; 500 + 501 + let nsid = "com.atproto.server.createAccount".parse().unwrap(); 502 + match verify_service_auth( 503 + token, 504 + &nsid, 505 + state.resolver.clone(), 506 + &state.app_config.pds_service_did, 507 + &requested_did, 508 + ) 509 + .await 510 + { 511 + //Just do nothing if it passes so it continues. 512 + Ok(_) => {} 513 + Err(err) => match err { 514 + VerifyServiceAuthError::AuthFailed => { 515 + return json_error_response( 516 + StatusCode::UNAUTHORIZED, 517 + "InvalidAuth", 518 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 519 + ); 520 + } 521 + VerifyServiceAuthError::Error(err) => { 522 + log::error!("Error verifying service auth token: {err}"); 523 + return json_error_response( 524 + StatusCode::BAD_REQUEST, 525 + "InvalidRequest", 526 + "There has been an error, please contact your PDS administrator for help and for them to review the server logs.", 527 + ); 528 + } 529 + }, 530 + } 531 + } 532 + 533 + // Check for captcha verification if configured 534 + if state.app_config.use_captcha { 535 + if let Some(ref verification_code) = account_request.verification_code { 536 + match verify_gate_code(&state, verification_code, &account_request.handle).await { 537 + //TODO has a few errors to support 538 + 539 + //expired token 540 + // { 541 + // "error": "ExpiredToken", 542 + // "message": "Token has expired" 543 + // } 24 544 25 - Ok(Json(DbPingResponse { db: "ok", value: v })) 26 - } 545 + //TODO ALSO add rate limits on the /gate endpoints so they can't be abused 546 + Ok(true) => { 547 + log::info!("Gate code verified for handle: {}", account_request.handle); 548 + } 549 + Ok(false) => { 550 + log::warn!( 551 + "Invalid or expired gate code for handle: {}", 552 + account_request.handle 553 + ); 554 + return json_error_response( 555 + StatusCode::BAD_REQUEST, 556 + "InvalidToken", 557 + "Token could not be verified", 558 + ); 559 + } 560 + Err(e) => { 561 + log::error!("Error verifying gate code: {}", e); 562 + return json_error_response( 563 + StatusCode::INTERNAL_SERVER_ERROR, 564 + "InvalidToken", 565 + "Token could not be verified", 566 + ); 567 + } 568 + } 569 + } else { 570 + // No verification code provided but captcha is required 571 + log::warn!( 572 + "No verification code provided for account creation: {}", 573 + account_request.handle 574 + ); 575 + return json_error_response( 576 + StatusCode::BAD_REQUEST, 577 + "InvalidRequest", 578 + "Verification is now required on this server.", 579 + ); 580 + } 581 + } 582 + 583 + // Rebuild the request with the same body and headers 584 + let uri = format!( 585 + "{}{}", 586 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createAccount" 587 + ); 588 + 589 + let mut new_req = axum::http::Request::post(&uri); 590 + if let Some(req_headers) = new_req.headers_mut() { 591 + *req_headers = headers; 592 + } 593 + 594 + let new_req = new_req 595 + .body(Body::from(body_bytes)) 596 + .map_err(|_| StatusCode::BAD_REQUEST)?; 597 + 598 + let proxied = state 599 + .reverse_proxy_client 600 + .request(new_req) 601 + .await 602 + .map_err(|_| StatusCode::BAD_REQUEST)? 603 + .into_response(); 604 + 605 + Ok(proxied) 606 + }
+1 -1
src/xrpc/mod.rs
··· 1 - mod com_atproto_server; 1 + pub mod com_atproto_server;