Microservice to bring 2FA to self hosted PDSes

Compare changes

Choose any two refs to compare.

+1402 -88
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" ··· 39 58 ] 40 59 41 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]] 42 67 name = "allocator-api2" 43 68 version = "0.2.21" 44 69 source = "registry+https://github.com/rust-lang/crates.io-index" 45 70 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 46 71 47 72 [[package]] 48 - name = "android-tzdata" 49 - version = "0.1.1" 50 - source = "registry+https://github.com/rust-lang/crates.io-index" 51 - checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 - 53 - [[package]] 54 73 name = "android_system_properties" 55 74 version = "0.1.5" 56 75 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 71 90 source = "registry+https://github.com/rust-lang/crates.io-index" 72 91 checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" 73 92 dependencies = [ 93 + "flate2", 74 94 "futures-core", 75 95 "memchr", 76 96 "pin-project-lite", ··· 87 107 dependencies = [ 88 108 "proc-macro2", 89 109 "quote", 90 - "syn", 110 + "syn 2.0.105", 91 111 ] 92 112 93 113 [[package]] ··· 198 218 dependencies = [ 199 219 "proc-macro2", 200 220 "quote", 201 - "syn", 221 + "syn 2.0.105", 202 222 ] 203 223 204 224 [[package]] ··· 229 249 ] 230 250 231 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]] 232 274 name = "base64" 233 275 version = "0.22.1" 234 276 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 257 299 "proc-macro2", 258 300 "quote", 259 301 "regex", 260 - "rustc-hash", 302 + "rustc-hash 1.1.0", 261 303 "shlex", 262 - "syn", 304 + "syn 2.0.105", 263 305 "which", 264 306 ] 265 307 ··· 282 324 ] 283 325 284 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]] 285 361 name = "bstr" 286 362 version = "1.12.0" 287 363 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 292 368 ] 293 369 294 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]] 295 395 name = "bumpalo" 296 396 version = "3.19.0" 297 397 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 308 408 version = "1.10.1" 309 409 source = "registry+https://github.com/rust-lang/crates.io-index" 310 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 + ] 311 423 312 424 [[package]] 313 425 name = "cc" ··· 318 430 "jobserver", 319 431 "libc", 320 432 "shlex", 433 + ] 434 + 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", 321 442 ] 322 443 323 444 [[package]] ··· 336 457 checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 337 458 338 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]] 339 466 name = "chrono" 340 - version = "0.4.41" 467 + version = "0.4.42" 341 468 source = "registry+https://github.com/rust-lang/crates.io-index" 342 - checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 469 + checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 343 470 dependencies = [ 344 - "android-tzdata", 345 471 "iana-time-zone", 346 472 "js-sys", 347 473 "num-traits", 474 + "serde", 348 475 "wasm-bindgen", 349 - "windows-link", 476 + "windows-link 0.2.1", 350 477 ] 351 478 352 479 [[package]] ··· 387 514 ] 388 515 389 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]] 390 531 name = "cipher" 391 532 version = "0.4.4" 392 533 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 432 573 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 433 574 434 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]] 435 592 name = "core-foundation-sys" 436 593 version = "0.8.7" 437 594 source = "registry+https://github.com/rust-lang/crates.io-index" 438 595 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 439 596 440 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 + ] 605 + 606 + [[package]] 441 607 name = "cpufeatures" 442 608 version = "0.2.17" 443 609 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 462 628 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 463 629 464 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]] 465 640 name = "crossbeam-queue" 466 641 version = "0.3.12" 467 642 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 483 658 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 484 659 485 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]] 486 673 name = "crypto-common" 487 674 version = "0.1.6" 488 675 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 498 685 source = "registry+https://github.com/rust-lang/crates.io-index" 499 686 checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 500 687 dependencies = [ 501 - "darling_core", 502 - "darling_macro", 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", 503 700 ] 504 701 505 702 [[package]] ··· 513 710 "proc-macro2", 514 711 "quote", 515 712 "strsim", 516 - "syn", 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", 517 728 ] 518 729 519 730 [[package]] ··· 522 733 source = "registry+https://github.com/rust-lang/crates.io-index" 523 734 checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 524 735 dependencies = [ 525 - "darling_core", 736 + "darling_core 0.20.11", 526 737 "quote", 527 - "syn", 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", 528 750 ] 529 751 530 752 [[package]] ··· 542 764 ] 543 765 544 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", 790 + ] 791 + 792 + [[package]] 545 793 name = "der" 546 794 version = "0.7.10" 547 795 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 553 801 ] 554 802 555 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]] 556 814 name = "derive_builder" 557 815 version = "0.20.2" 558 816 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 567 825 source = "registry+https://github.com/rust-lang/crates.io-index" 568 826 checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 569 827 dependencies = [ 570 - "darling", 828 + "darling 0.20.11", 571 829 "proc-macro2", 572 830 "quote", 573 - "syn", 831 + "syn 2.0.105", 574 832 ] 575 833 576 834 [[package]] ··· 580 838 checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 581 839 dependencies = [ 582 840 "derive_builder_core", 583 - "syn", 841 + "syn 2.0.105", 584 842 ] 585 843 586 844 [[package]] ··· 603 861 dependencies = [ 604 862 "proc-macro2", 605 863 "quote", 606 - "syn", 864 + "syn 2.0.105", 607 865 ] 608 866 609 867 [[package]] ··· 619 877 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 620 878 621 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]] 622 900 name = "either" 623 901 version = "1.15.0" 624 902 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 628 906 ] 629 907 630 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]] 631 929 name = "email-encoding" 632 930 version = "0.4.1" 633 931 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 644 942 checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 645 943 646 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]] 647 954 name = "equivalent" 648 955 version = "1.0.2" 649 956 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 656 963 checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 657 964 dependencies = [ 658 965 "libc", 659 - "windows-sys 0.52.0", 966 + "windows-sys 0.59.0", 660 967 ] 661 968 662 969 [[package]] ··· 688 995 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 689 996 690 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]] 691 1018 name = "flume" 692 1019 version = "0.11.1" 693 1020 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 709 1036 version = "0.1.5" 710 1037 source = "registry+https://github.com/rust-lang/crates.io-index" 711 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" 712 1054 713 1055 [[package]] 714 1056 name = "form_urlencoded" ··· 780 1122 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 781 1123 782 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]] 783 1136 name = "futures-sink" 784 1137 version = "0.3.31" 785 1138 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 805 1158 dependencies = [ 806 1159 "futures-core", 807 1160 "futures-io", 1161 + "futures-macro", 808 1162 "futures-sink", 809 1163 "futures-task", 810 1164 "memchr", ··· 821 1175 dependencies = [ 822 1176 "typenum", 823 1177 "version_check", 1178 + "zeroize", 824 1179 ] 825 1180 826 1181 [[package]] ··· 830 1185 checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 831 1186 dependencies = [ 832 1187 "cfg-if", 1188 + "js-sys", 833 1189 "libc", 834 - "wasi 0.11.1+wasi-snapshot-preview1", 1190 + "wasi", 1191 + "wasm-bindgen", 835 1192 ] 836 1193 837 1194 [[package]] 838 1195 name = "getrandom" 839 - version = "0.3.3" 1196 + version = "0.3.4" 840 1197 source = "registry+https://github.com/rust-lang/crates.io-index" 841 - checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1198 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 842 1199 dependencies = [ 843 1200 "cfg-if", 844 1201 "js-sys", 845 1202 "libc", 846 1203 "r-efi", 847 - "wasi 0.14.2+wasi-0.2.4", 1204 + "wasip2", 848 1205 "wasm-bindgen", 849 1206 ] 850 1207 ··· 869 1226 "aho-corasick", 870 1227 "bstr", 871 1228 "log", 872 - "regex-automata 0.4.9", 1229 + "regex-automata 0.4.13", 873 1230 "regex-syntax 0.8.5", 874 1231 ] 875 1232 ··· 884 1241 "futures-sink", 885 1242 "futures-timer", 886 1243 "futures-util", 887 - "getrandom 0.3.3", 1244 + "getrandom 0.3.4", 888 1245 "hashbrown 0.15.5", 889 1246 "nonzero_ext", 890 1247 "parking_lot", ··· 897 1254 ] 898 1255 899 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]] 900 1268 name = "h2" 901 1269 version = "0.4.12" 902 1270 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 908 1276 "futures-core", 909 1277 "futures-sink", 910 1278 "http", 911 - "indexmap", 1279 + "indexmap 2.10.0", 912 1280 "slab", 913 1281 "tokio", 914 1282 "tokio-util", ··· 944 1312 945 1313 [[package]] 946 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]] 1320 + name = "hashbrown" 947 1321 version = "0.14.5" 948 1322 source = "registry+https://github.com/rust-lang/crates.io-index" 949 1323 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" ··· 974 1348 975 1349 [[package]] 976 1350 name = "heck" 1351 + version = "0.4.1" 1352 + source = "registry+https://github.com/rust-lang/crates.io-index" 1353 + checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 1354 + 1355 + [[package]] 1356 + name = "heck" 977 1357 version = "0.5.0" 978 1358 source = "registry+https://github.com/rust-lang/crates.io-index" 979 1359 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" ··· 985 1365 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 986 1366 987 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" 1372 + 1373 + [[package]] 988 1374 name = "hkdf" 989 1375 version = "0.12.4" 990 1376 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1009 1395 checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 1010 1396 dependencies = [ 1011 1397 "windows-sys 0.59.0", 1398 + ] 1399 + 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", 1012 1407 ] 1013 1408 1014 1409 [[package]] ··· 1079 1474 ] 1080 1475 1081 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]] 1082 1494 name = "hyper-timeout" 1083 1495 version = "0.5.2" 1084 1496 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1097 1509 source = "registry+https://github.com/rust-lang/crates.io-index" 1098 1510 checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 1099 1511 dependencies = [ 1512 + "base64", 1100 1513 "bytes", 1101 1514 "futures-channel", 1102 1515 "futures-core", ··· 1104 1517 "http", 1105 1518 "http-body", 1106 1519 "hyper", 1520 + "ipnet", 1107 1521 "libc", 1522 + "percent-encoding", 1108 1523 "pin-project-lite", 1109 1524 "socket2", 1525 + "system-configuration", 1110 1526 "tokio", 1111 1527 "tower-service", 1112 1528 "tracing", 1529 + "windows-registry", 1113 1530 ] 1114 1531 1115 1532 [[package]] ··· 1251 1668 1252 1669 [[package]] 1253 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" 1254 1682 version = "2.10.0" 1255 1683 source = "registry+https://github.com/rust-lang/crates.io-index" 1256 1684 checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 1257 1685 dependencies = [ 1258 1686 "equivalent", 1259 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", 1260 1698 ] 1261 1699 1262 1700 [[package]] ··· 1269 1707 ] 1270 1708 1271 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", 1716 + ] 1717 + 1718 + [[package]] 1272 1719 name = "io-uring" 1273 1720 version = "0.7.9" 1274 1721 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1280 1727 ] 1281 1728 1282 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]] 1283 1757 name = "itertools" 1284 1758 version = "0.12.1" 1285 1759 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1295 1769 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1296 1770 1297 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]] 1298 1895 name = "jobserver" 1299 1896 version = "0.1.33" 1300 1897 source = "registry+https://github.com/rust-lang/crates.io-index" 1301 1898 checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1302 1899 dependencies = [ 1303 - "getrandom 0.3.3", 1900 + "getrandom 0.3.4", 1304 1901 "libc", 1305 1902 ] 1306 1903 1307 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]] 1308 1922 name = "js-sys" 1309 1923 version = "0.3.77" 1310 1924 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1337 1951 ] 1338 1952 1339 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]] 1340 1977 name = "lazy_static" 1341 1978 version = "1.5.0" 1342 1979 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1392 2029 checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 1393 2030 dependencies = [ 1394 2031 "cfg-if", 1395 - "windows-targets 0.48.5", 2032 + "windows-targets 0.52.6", 1396 2033 ] 1397 2034 1398 2035 [[package]] ··· 1452 2089 checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1453 2090 1454 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]] 1455 2109 name = "matchers" 1456 2110 version = "0.1.0" 1457 2111 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1483 2137 checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1484 2138 1485 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]] 1486 2162 name = "mime" 1487 2163 version = "0.3.17" 1488 2164 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1501 2177 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1502 2178 dependencies = [ 1503 2179 "adler2", 2180 + "simd-adler32", 1504 2181 ] 1505 2182 1506 2183 [[package]] ··· 1510 2187 checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1511 2188 dependencies = [ 1512 2189 "libc", 1513 - "wasi 0.11.1+wasi-snapshot-preview1", 2190 + "wasi", 1514 2191 "windows-sys 0.59.0", 1515 2192 ] 1516 2193 1517 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]] 1518 2218 name = "nom" 1519 2219 version = "7.1.3" 1520 2220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1573 2273 ] 1574 2274 1575 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]] 1576 2282 name = "num-integer" 1577 2283 version = "0.1.46" 1578 2284 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1633 2339 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1634 2340 1635 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]] 1636 2404 name = "overload" 1637 2405 version = "0.1.1" 1638 2406 source = "registry+https://github.com/rust-lang/crates.io-index" 1639 2407 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1640 2408 1641 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]] 1642 2422 name = "parking" 1643 2423 version = "2.2.1" 1644 2424 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1690 2470 1691 2471 [[package]] 1692 2472 name = "pds_gatekeeper" 1693 - version = "0.1.0" 2473 + version = "0.1.2" 1694 2474 dependencies = [ 1695 2475 "anyhow", 1696 2476 "aws-lc-rs", ··· 1700 2480 "dotenvy", 1701 2481 "handlebars", 1702 2482 "hex", 2483 + "html-escape", 1703 2484 "hyper-util", 2485 + "jacquard-common", 2486 + "jacquard-identity", 2487 + "josekit", 1704 2488 "jwt-compact", 1705 2489 "lettre", 2490 + "multibase", 1706 2491 "rand 0.9.2", 2492 + "reqwest", 1707 2493 "rust-embed", 1708 2494 "rustls", 1709 2495 "scrypt", ··· 1716 2502 "tower_governor", 1717 2503 "tracing", 1718 2504 "tracing-subscriber", 2505 + "urlencoding", 1719 2506 ] 1720 2507 1721 2508 [[package]] ··· 1764 2551 "pest_meta", 1765 2552 "proc-macro2", 1766 2553 "quote", 1767 - "syn", 2554 + "syn 2.0.105", 1768 2555 ] 1769 2556 1770 2557 [[package]] ··· 1794 2581 dependencies = [ 1795 2582 "proc-macro2", 1796 2583 "quote", 1797 - "syn", 2584 + "syn 2.0.105", 1798 2585 ] 1799 2586 1800 2587 [[package]] ··· 1852 2639 ] 1853 2640 1854 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]] 1855 2648 name = "ppv-lite86" 1856 2649 version = "0.2.21" 1857 2650 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1867 2660 checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" 1868 2661 dependencies = [ 1869 2662 "proc-macro2", 1870 - "syn", 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", 1871 2697 ] 1872 2698 1873 2699 [[package]] ··· 1880 2706 ] 1881 2707 1882 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]] 1883 2722 name = "psm" 1884 2723 version = "0.1.26" 1885 2724 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1898 2737 "libc", 1899 2738 "once_cell", 1900 2739 "raw-cpuid", 1901 - "wasi 0.11.1+wasi-snapshot-preview1", 2740 + "wasi", 1902 2741 "web-sys", 1903 2742 "winapi", 1904 2743 ] 1905 2744 1906 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]] 1907 2801 name = "quote" 1908 2802 version = "1.0.40" 1909 2803 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1980 2874 source = "registry+https://github.com/rust-lang/crates.io-index" 1981 2875 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1982 2876 dependencies = [ 1983 - "getrandom 0.3.3", 2877 + "getrandom 0.3.4", 1984 2878 ] 1985 2879 1986 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]] 1987 2887 name = "raw-cpuid" 1988 2888 version = "11.5.0" 1989 2889 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2002 2902 ] 2003 2903 2004 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]] 2005 2925 name = "regex" 2006 - version = "1.11.1" 2926 + version = "1.12.2" 2007 2927 source = "registry+https://github.com/rust-lang/crates.io-index" 2008 - checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 2928 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 2009 2929 dependencies = [ 2010 2930 "aho-corasick", 2011 2931 "memchr", 2012 - "regex-automata 0.4.9", 2932 + "regex-automata 0.4.13", 2013 2933 "regex-syntax 0.8.5", 2014 2934 ] 2015 2935 ··· 2024 2944 2025 2945 [[package]] 2026 2946 name = "regex-automata" 2027 - version = "0.4.9" 2947 + version = "0.4.13" 2028 2948 source = "registry+https://github.com/rust-lang/crates.io-index" 2029 - checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 2949 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 2030 2950 dependencies = [ 2031 2951 "aho-corasick", 2032 2952 "memchr", ··· 2034 2954 ] 2035 2955 2036 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]] 2037 2963 name = "regex-syntax" 2038 2964 version = "0.6.29" 2039 2965 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2046 2972 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 2047 2973 2048 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]] 2049 3030 name = "ring" 2050 3031 version = "0.17.14" 2051 3032 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2099 3080 "proc-macro2", 2100 3081 "quote", 2101 3082 "rust-embed-utils", 2102 - "syn", 3083 + "syn 2.0.105", 2103 3084 "walkdir", 2104 3085 ] 2105 3086 ··· 2127 3108 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 2128 3109 2129 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]] 2130 3117 name = "rustix" 2131 3118 version = "0.38.44" 2132 3119 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2136 3123 "errno", 2137 3124 "libc", 2138 3125 "linux-raw-sys", 2139 - "windows-sys 0.52.0", 3126 + "windows-sys 0.59.0", 2140 3127 ] 2141 3128 2142 3129 [[package]] ··· 2161 3148 source = "registry+https://github.com/rust-lang/crates.io-index" 2162 3149 checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 2163 3150 dependencies = [ 3151 + "web-time", 2164 3152 "zeroize", 2165 3153 ] 2166 3154 ··· 2207 3195 ] 2208 3196 2209 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]] 2210 3222 name = "scopeguard" 2211 3223 version = "1.2.0" 2212 3224 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2225 3237 ] 2226 3238 2227 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]] 2228 3254 name = "secp256k1" 2229 3255 version = "0.28.2" 2230 3256 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2244 3270 2245 3271 [[package]] 2246 3272 name = "serde" 2247 - 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" 3284 + source = "registry+https://github.com/rust-lang/crates.io-index" 3285 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 3286 + dependencies = [ 3287 + "serde", 3288 + "serde_core", 3289 + ] 3290 + 3291 + [[package]] 3292 + name = "serde_core" 3293 + version = "1.0.228" 2248 3294 source = "registry+https://github.com/rust-lang/crates.io-index" 2249 - checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 3295 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2250 3296 dependencies = [ 2251 3297 "serde_derive", 2252 3298 ] 2253 3299 2254 3300 [[package]] 2255 3301 name = "serde_derive" 2256 - version = "1.0.219" 3302 + version = "1.0.228" 2257 3303 source = "registry+https://github.com/rust-lang/crates.io-index" 2258 - checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 3304 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2259 3305 dependencies = [ 2260 3306 "proc-macro2", 2261 3307 "quote", 2262 - "syn", 3308 + "syn 2.0.105", 3309 + ] 3310 + 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", 2263 3334 ] 2264 3335 2265 3336 [[package]] 2266 3337 name = "serde_json" 2267 - version = "1.0.142" 3338 + version = "1.0.145" 2268 3339 source = "registry+https://github.com/rust-lang/crates.io-index" 2269 - checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" 3340 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 2270 3341 dependencies = [ 3342 + "indexmap 2.10.0", 2271 3343 "itoa", 2272 3344 "memchr", 2273 3345 "ryu", 2274 3346 "serde", 3347 + "serde_core", 2275 3348 ] 2276 3349 2277 3350 [[package]] ··· 2285 3358 ] 2286 3359 2287 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]] 2288 3372 name = "serde_urlencoded" 2289 3373 version = "0.7.1" 2290 3374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2297 3381 ] 2298 3382 2299 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]] 2300 3415 name = "sha1" 2301 3416 version = "0.10.6" 2302 3417 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2353 3468 ] 2354 3469 2355 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" 3475 + 3476 + [[package]] 2356 3477 name = "slab" 2357 3478 version = "0.4.11" 2358 3479 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2368 3489 ] 2369 3490 2370 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 + ] 3500 + 3501 + [[package]] 2371 3502 name = "socket2" 2372 3503 version = "0.6.0" 2373 3504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2437 3568 "futures-util", 2438 3569 "hashbrown 0.15.5", 2439 3570 "hashlink", 2440 - "indexmap", 3571 + "indexmap 2.10.0", 2441 3572 "log", 2442 3573 "memchr", 2443 3574 "once_cell", ··· 2465 3596 "quote", 2466 3597 "sqlx-core", 2467 3598 "sqlx-macros-core", 2468 - "syn", 3599 + "syn 2.0.105", 2469 3600 ] 2470 3601 2471 3602 [[package]] ··· 2476 3607 dependencies = [ 2477 3608 "dotenvy", 2478 3609 "either", 2479 - "heck", 3610 + "heck 0.5.0", 2480 3611 "hex", 2481 3612 "once_cell", 2482 3613 "proc-macro2", ··· 2488 3619 "sqlx-mysql", 2489 3620 "sqlx-postgres", 2490 3621 "sqlx-sqlite", 2491 - "syn", 3622 + "syn 2.0.105", 2492 3623 "tokio", 2493 3624 "url", 2494 3625 ] ··· 2619 3750 ] 2620 3751 2621 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]] 2622 3779 name = "stringprep" 2623 3780 version = "0.1.5" 2624 3781 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2643 3800 2644 3801 [[package]] 2645 3802 name = "syn" 3803 + version = "1.0.109" 3804 + source = "registry+https://github.com/rust-lang/crates.io-index" 3805 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 3806 + dependencies = [ 3807 + "proc-macro2", 3808 + "quote", 3809 + "unicode-ident", 3810 + ] 3811 + 3812 + [[package]] 3813 + name = "syn" 2646 3814 version = "2.0.105" 2647 3815 source = "registry+https://github.com/rust-lang/crates.io-index" 2648 3816 checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" ··· 2657 3825 version = "1.0.2" 2658 3826 source = "registry+https://github.com/rust-lang/crates.io-index" 2659 3827 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 3828 + dependencies = [ 3829 + "futures-core", 3830 + ] 2660 3831 2661 3832 [[package]] 2662 3833 name = "synstructure" ··· 2666 3837 dependencies = [ 2667 3838 "proc-macro2", 2668 3839 "quote", 2669 - "syn", 3840 + "syn 2.0.105", 3841 + ] 3842 + 3843 + [[package]] 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" 3857 + source = "registry+https://github.com/rust-lang/crates.io-index" 3858 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 3859 + dependencies = [ 3860 + "core-foundation-sys", 3861 + "libc", 2670 3862 ] 2671 3863 2672 3864 [[package]] ··· 2695 3887 dependencies = [ 2696 3888 "proc-macro2", 2697 3889 "quote", 2698 - "syn", 3890 + "syn 2.0.105", 2699 3891 ] 2700 3892 2701 3893 [[package]] ··· 2706 3898 dependencies = [ 2707 3899 "proc-macro2", 2708 3900 "quote", 2709 - "syn", 3901 + "syn 2.0.105", 2710 3902 ] 2711 3903 2712 3904 [[package]] ··· 2719 3911 ] 2720 3912 2721 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]] 2722 3945 name = "tinystr" 2723 3946 version = "0.8.1" 2724 3947 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2770 3993 dependencies = [ 2771 3994 "proc-macro2", 2772 3995 "quote", 2773 - "syn", 3996 + "syn 2.0.105", 2774 3997 ] 2775 3998 2776 3999 [[package]] ··· 2796 4019 2797 4020 [[package]] 2798 4021 name = "tokio-util" 2799 - version = "0.7.15" 4022 + version = "0.7.17" 2800 4023 source = "registry+https://github.com/rust-lang/crates.io-index" 2801 - checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 4024 + checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 2802 4025 dependencies = [ 2803 4026 "bytes", 2804 4027 "futures-core", ··· 2844 4067 dependencies = [ 2845 4068 "futures-core", 2846 4069 "futures-util", 2847 - "indexmap", 4070 + "indexmap 2.10.0", 2848 4071 "pin-project-lite", 2849 4072 "slab", 2850 4073 "sync_wrapper", ··· 2865 4088 "bitflags", 2866 4089 "bytes", 2867 4090 "futures-core", 4091 + "futures-util", 2868 4092 "http", 2869 4093 "http-body", 4094 + "iri-string", 2870 4095 "pin-project-lite", 2871 4096 "tokio", 2872 4097 "tokio-util", 4098 + "tower", 2873 4099 "tower-layer", 2874 4100 "tower-service", 2875 4101 ] ··· 2923 4149 dependencies = [ 2924 4150 "proc-macro2", 2925 4151 "quote", 2926 - "syn", 4152 + "syn 2.0.105", 2927 4153 ] 2928 4154 2929 4155 [[package]] ··· 2966 4192 ] 2967 4193 2968 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]] 2969 4206 name = "try-lock" 2970 4207 version = "0.2.5" 2971 4208 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3011 4248 checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 3012 4249 3013 4250 [[package]] 4251 + name = "unicode-segmentation" 4252 + version = "1.12.0" 4253 + source = "registry+https://github.com/rust-lang/crates.io-index" 4254 + checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 4255 + 4256 + [[package]] 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]] 3014 4269 name = "untrusted" 3015 4270 version = "0.7.1" 3016 4271 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3031 4286 "form_urlencoded", 3032 4287 "idna", 3033 4288 "percent-encoding", 4289 + "serde", 3034 4290 ] 3035 4291 3036 4292 [[package]] 4293 + name = "urlencoding" 4294 + version = "2.1.3" 4295 + source = "registry+https://github.com/rust-lang/crates.io-index" 4296 + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 4297 + 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]] 3037 4305 name = "utf8_iter" 3038 4306 version = "1.0.4" 3039 4307 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3083 4351 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 3084 4352 3085 4353 [[package]] 3086 - name = "wasi" 3087 - version = "0.14.2+wasi-0.2.4" 4354 + name = "wasip2" 4355 + version = "1.0.1+wasi-0.2.4" 3088 4356 source = "registry+https://github.com/rust-lang/crates.io-index" 3089 - checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 4357 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 3090 4358 dependencies = [ 3091 - "wit-bindgen-rt", 4359 + "wit-bindgen", 3092 4360 ] 3093 4361 3094 4362 [[package]] ··· 3119 4387 "log", 3120 4388 "proc-macro2", 3121 4389 "quote", 3122 - "syn", 4390 + "syn 2.0.105", 3123 4391 "wasm-bindgen-shared", 3124 4392 ] 3125 4393 3126 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]] 3127 4408 name = "wasm-bindgen-macro" 3128 4409 version = "0.2.100" 3129 4410 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3141 4422 dependencies = [ 3142 4423 "proc-macro2", 3143 4424 "quote", 3144 - "syn", 4425 + "syn 2.0.105", 3145 4426 "wasm-bindgen-backend", 3146 4427 "wasm-bindgen-shared", 3147 4428 ] ··· 3153 4434 checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 3154 4435 dependencies = [ 3155 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", 3156 4450 ] 3157 4451 3158 4452 [[package]] ··· 3254 4548 dependencies = [ 3255 4549 "windows-implement", 3256 4550 "windows-interface", 3257 - "windows-link", 4551 + "windows-link 0.1.3", 3258 4552 "windows-result", 3259 4553 "windows-strings", 3260 4554 ] ··· 3267 4561 dependencies = [ 3268 4562 "proc-macro2", 3269 4563 "quote", 3270 - "syn", 4564 + "syn 2.0.105", 3271 4565 ] 3272 4566 3273 4567 [[package]] ··· 3278 4572 dependencies = [ 3279 4573 "proc-macro2", 3280 4574 "quote", 3281 - "syn", 4575 + "syn 2.0.105", 3282 4576 ] 3283 4577 3284 4578 [[package]] ··· 3288 4582 checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3289 4583 3290 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]] 3291 4602 name = "windows-result" 3292 4603 version = "0.3.4" 3293 4604 source = "registry+https://github.com/rust-lang/crates.io-index" 3294 4605 checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 3295 4606 dependencies = [ 3296 - "windows-link", 4607 + "windows-link 0.1.3", 3297 4608 ] 3298 4609 3299 4610 [[package]] ··· 3302 4613 source = "registry+https://github.com/rust-lang/crates.io-index" 3303 4614 checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3304 4615 dependencies = [ 3305 - "windows-link", 4616 + "windows-link 0.1.3", 3306 4617 ] 3307 4618 3308 4619 [[package]] ··· 3454 4765 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3455 4766 3456 4767 [[package]] 3457 - name = "wit-bindgen-rt" 3458 - version = "0.39.0" 4768 + name = "wit-bindgen" 4769 + version = "0.46.0" 3459 4770 source = "registry+https://github.com/rust-lang/crates.io-index" 3460 - checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 3461 - dependencies = [ 3462 - "bitflags", 3463 - ] 4771 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 3464 4772 3465 4773 [[package]] 3466 4774 name = "writeable" ··· 3469 4777 checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 3470 4778 3471 4779 [[package]] 4780 + name = "yansi" 4781 + version = "1.0.1" 4782 + source = "registry+https://github.com/rust-lang/crates.io-index" 4783 + checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 4784 + 4785 + [[package]] 3472 4786 name = "yoke" 3473 4787 version = "0.8.0" 3474 4788 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3488 4802 dependencies = [ 3489 4803 "proc-macro2", 3490 4804 "quote", 3491 - "syn", 4805 + "syn 2.0.105", 3492 4806 "synstructure", 3493 4807 ] 3494 4808 ··· 3509 4823 dependencies = [ 3510 4824 "proc-macro2", 3511 4825 "quote", 3512 - "syn", 4826 + "syn 2.0.105", 3513 4827 ] 3514 4828 3515 4829 [[package]] ··· 3529 4843 dependencies = [ 3530 4844 "proc-macro2", 3531 4845 "quote", 3532 - "syn", 4846 + "syn 2.0.105", 3533 4847 "synstructure", 3534 4848 ] 3535 4849 ··· 3550 4864 dependencies = [ 3551 4865 "proc-macro2", 3552 4866 "quote", 3553 - "syn", 4867 + "syn 2.0.105", 3554 4868 ] 3555 4869 3556 4870 [[package]] ··· 3583 4897 dependencies = [ 3584 4898 "proc-macro2", 3585 4899 "quote", 3586 - "syn", 4900 + "syn 2.0.105", 3587 4901 ] 3588 4902 3589 4903 [[package]]
+13 -6
Cargo.toml
··· 1 1 [package] 2 2 name = "pds_gatekeeper" 3 - version = "0.1.0" 3 + version = "0.1.2" 4 4 edition = "2024" 5 + license = "MIT" 5 6 6 7 [dependencies] 7 8 axum = { version = "0.8.4", features = ["macros", "json"] } ··· 14 15 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 15 16 hyper-util = { version = "0.1.16", features = ["client", "client-legacy"] } 16 17 tower-http = { version = "0.6", features = ["cors", "compression-zstd"] } 17 - tower_governor = "0.8.0" 18 + tower_governor = { version = "0.8.0", features = ["axum", "tracing"] } 18 19 hex = "0.4" 19 20 jwt-compact = { version = "0.8.0", features = ["es256k"] } 20 21 scrypt = "0.11" 21 - #lettre = { version = "0.11.18", default-features = false, features = ["pool", "tokio1-rustls", "smtp-transport", "hostname", "builder"] } 22 - #lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 22 + #Leaveing these two cause I think it is needed by the email crate for ssl 23 23 aws-lc-rs = "1.13.0" 24 + rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 24 25 lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 25 - rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 26 26 handlebars = { version = "6.3.2", features = ["rust-embed"] } 27 27 rust-embed = "8.7.2" 28 28 axum-template = { version = "3.0.0", features = ["handlebars"] } 29 29 rand = "0.9.2" 30 30 anyhow = "1.0.99" 31 - chrono = "0.4.41" 31 + chrono = { version = "0.4.42", features = ["default", "serde"] } 32 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"
+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.
+118 -23
README.md
··· 15 15 - Overrides The login endpoint to add 2FA for both Bluesky client logged in and OAuth logins 16 16 - Overrides the settings endpoints as well. As long as you have a confirmed email you can turn on 2FA 17 17 18 - ## Captcha on Create Account 18 + ## Captcha on account creation 19 19 20 - Future feature? 20 + Require a `verificationCode` set on the `createAccount` request. This is gotten from completing a captcha challenge 21 + hosted on the 22 + PDS mimicking what the Bluesky Entryway does. Migration tools will need to support this, but social-apps will support 23 + and redirect to `GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT`. This is how the clients know to get the code to prove a captcha 24 + was successful. 25 + 26 + - Requires `GATEKEEPER_CREATE_ACCOUNT_CAPTCHA` to be set to true. 27 + - Requires `PDS_HCAPTCHA_SITE_KEY` and `PDS_HCAPTCHA_SECRET_KEY` to be set. Can sign up at https://www.hcaptcha.com/ 28 + - Requires proxying `/xrpc/com.atproto.server.describeServer`, `/xrpc/com.atproto.server.createAccount` and `/gate/*` to 29 + PDS 30 + Gatekeeper 31 + - Optional `GATEKEEPER_JWE_KEY` key to encrypt the captcha verification code. Defaults to a random 32 byte key. Not 32 + strictly needed unless you're scaling 33 + - Optional`GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT` default redirect on captcha success. Defaults to `https://bsky.app`. 34 + - Optional `GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS` allowed redirect urls for captcha success. You want these to match the 35 + url showing the captcha. Defaults are: 36 + - https://bsky.app 37 + - https://pdsmoover.com 38 + - https://blacksky.community 39 + - https://tektite.cc 40 + 41 + ## Block account creation unless it's a migration 42 + 43 + You can set `GATEKEEPER_ALLOW_ONLY_MIGRATIONS` to block createAccount unless it's via a migration. This does not require 44 + a change for migration tools, but social-apps create a new account will no longer work and to create a brand new account 45 + users will need to do this via the Oauth account create screen on the PDS. We recommend setting `PDS_HCAPTCHA_SITE_KEY` 46 + and `PDS_HCAPTCHA_SECRET_KEY` so the OAuth screen is protected by a captcha if you use this with invite codes turned 47 + off. 21 48 22 49 # Setup 23 50 ··· 37 64 ```yml 38 65 gatekeeper: 39 66 container_name: gatekeeper 40 - image: fatfingers23/pds_gatekeeper:arm-latest 67 + image: fatfingers23/pds_gatekeeper:latest 41 68 network_mode: host 42 69 restart: unless-stopped 43 70 #This gives the container to the access to the PDS folder. Source is the location on your server of that directory ··· 49 76 - pds 50 77 ``` 51 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 + 52 124 ## Caddy setup 53 125 54 126 For the reverse proxy I use caddy. This part is what overwrites the endpoints and proxies them to PDS gatekeeper to add 55 127 in extra functionality. The main part is below, for a full example see [./examples/Caddyfile](./examples/Caddyfile). 56 128 This is usually found at `/pds/caddy/etc/caddy/Caddyfile` on your PDS. 57 129 58 - ```caddyfile 130 + ``` 59 131 @gatekeeper { 60 - path /xrpc/com.atproto.server.getSession 61 - path /xrpc/com.atproto.server.updateEmail 62 - path /xrpc/com.atproto.server.createSession 63 - path /@atproto/oauth-provider/~api/sign-in 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/* 64 139 } 65 140 66 141 handle @gatekeeper { 67 - reverse_proxy http://localhost:8080 68 - } 142 + reverse_proxy http://localhost:8080 143 + } 69 144 70 - reverse_proxy http://localhost:3000 145 + reverse_proxy http://localhost:3000 71 146 ``` 72 147 73 148 If you use a cloudflare tunnel then your caddyfile would look a bit more like below with your tunnel proxying to 74 149 `localhost:8081` (or w/e port you want). 75 150 76 - ```caddyfile 151 + ``` 77 152 http://*.localhost:8082, http://localhost:8082 { 78 - @gatekeeper { 79 - path /xrpc/com.atproto.server.getSession 80 - path /xrpc/com.atproto.server.updateEmail 81 - path /xrpc/com.atproto.server.createSession 82 - path /@atproto/oauth-provider/~api/sign-in 83 - } 84 - 85 - handle @gatekeeper { 86 - reverse_proxy http://localhost:8080 87 - } 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 + } 88 162 89 - reverse_proxy http://localhost:3000 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 90 170 } 91 171 92 172 ``` ··· 105 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 106 186 same. 107 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 + 108 191 `PDS_BASE_URL` - Base url of the PDS. You most likely want `https://localhost:3000` which is also the default 109 192 110 193 `GATEKEEPER_HOST` - Host for pds gatekeeper. Defaults to `127.0.0.1` 111 194 112 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 +
+22 -21
examples/Caddyfile
··· 1 1 { 2 - email youremail@myemail.com 3 - on_demand_tls { 4 - ask http://localhost:3000/tls-check 5 - } 2 + email youremail@myemail.com 3 + on_demand_tls { 4 + ask http://localhost:3000/tls-check 5 + } 6 6 } 7 7 8 8 *.yourpds.com, yourpds.com { 9 - tls { 10 - on_demand 9 + tls { 10 + on_demand 11 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.updateEmail 16 - path /xrpc/com.atproto.server.createSession 17 - path /@atproto/oauth-provider/~api/sign-in 18 - } 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 + } 19 22 20 - handle @gatekeeper { 21 - #This is the address for PDS gatekeeper, default is 8080 22 - reverse_proxy http://localhost:8080 23 - } 23 + handle @gatekeeper { 24 + #This is the address for PDS gatekeeper, default is 8080 25 + reverse_proxy http://localhost:8080 26 + } 24 27 25 - reverse_proxy http://localhost:3000 26 - #..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line 28 + reverse_proxy http://localhost:3000 29 + #..here. Copy and paste this replacing the reverse_proxy http://localhost:3000 line 27 30 } 28 - 29 -
+1 -1
examples/compose.yml
··· 39 39 WATCHTOWER_SCHEDULE: "@midnight" 40 40 gatekeeper: 41 41 container_name: gatekeeper 42 - image: fatfingers23/pds_gatekeeper:arm-latest 42 + image: fatfingers23/pds_gatekeeper:latest 43 43 network_mode: host 44 44 restart: unless-stopped 45 45 #This gives the container to the access to the PDS folder. Source is the location on your server of that directory
+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>
+1 -1
justfile
··· 2 2 docker buildx build \ 3 3 --platform linux/arm64,linux/amd64 \ 4 4 --tag fatfingers23/pds_gatekeeper:latest \ 5 - --tag fatfingers23/pds_gatekeeper:0.1 \ 5 + --tag fatfingers23/pds_gatekeeper:0.1.0.5 \ 6 6 --push .
+10
migrations/20251126000000_gate_codes.sql
··· 1 + -- Add migration script here 2 + CREATE TABLE IF NOT EXISTS gate_codes 3 + ( 4 + code VARCHAR PRIMARY KEY, 5 + handle VARCHAR NOT NULL, 6 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 7 + ); 8 + 9 + -- Index on created_at for efficient cleanup of expired codes 10 + CREATE INDEX IF NOT EXISTS idx_gate_codes_created_at ON gate_codes(created_at);
+247
src/gate.rs
··· 1 + use crate::AppState; 2 + use crate::helpers::{generate_gate_token, json_error_response}; 3 + use axum::Form; 4 + use axum::extract::{Query, State}; 5 + use axum::http::StatusCode; 6 + use axum::response::{IntoResponse, Redirect, Response}; 7 + use axum_template::RenderHtml; 8 + use chrono::{DateTime, Utc}; 9 + use serde::{Deserialize, Serialize}; 10 + use std::env; 11 + use tracing::log; 12 + 13 + #[derive(Deserialize)] 14 + pub struct GateQuery { 15 + handle: String, 16 + state: String, 17 + #[serde(default)] 18 + error: Option<String>, 19 + #[serde(default)] 20 + redirect_url: Option<String>, 21 + } 22 + 23 + #[derive(Deserialize, Serialize)] 24 + pub struct CaptchaPage { 25 + handle: String, 26 + state: String, 27 + captcha_site_key: String, 28 + error_message: Option<String>, 29 + pds: String, 30 + redirect_url: Option<String>, 31 + } 32 + 33 + #[derive(Deserialize)] 34 + pub struct CaptchaForm { 35 + #[serde(rename = "h-captcha-response")] 36 + h_captcha_response: String, 37 + #[serde(default)] 38 + redirect_url: Option<String>, 39 + } 40 + 41 + /// GET /gate - Display the captcha page 42 + pub async fn get_gate( 43 + Query(params): Query<GateQuery>, 44 + State(state): State<AppState>, 45 + ) -> impl IntoResponse { 46 + let hcaptcha_site_key = match env::var("PDS_HCAPTCHA_SITE_KEY") { 47 + Ok(key) => key, 48 + Err(_) => { 49 + return json_error_response( 50 + StatusCode::INTERNAL_SERVER_ERROR, 51 + "ServerError", 52 + "hCaptcha is not configured", 53 + ) 54 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 55 + } 56 + }; 57 + 58 + let error_message = match params.error { 59 + None => None, 60 + Some(error) => Some(html_escape::encode_safe(&error).to_string()), 61 + }; 62 + 63 + RenderHtml( 64 + "captcha.hbs", 65 + state.template_engine, 66 + CaptchaPage { 67 + handle: params.handle, 68 + state: params.state, 69 + captcha_site_key: hcaptcha_site_key, 70 + error_message, 71 + pds: state.app_config.pds_service_did.replace("did:web:", ""), 72 + redirect_url: params.redirect_url, 73 + }, 74 + ) 75 + .into_response() 76 + } 77 + 78 + /// POST /gate - Verify captcha and redirect 79 + pub async fn post_gate( 80 + State(state): State<AppState>, 81 + Query(params): Query<GateQuery>, 82 + Form(form): Form<CaptchaForm>, 83 + ) -> Response { 84 + // Verify hCaptcha response 85 + let hcaptcha_secret = match env::var("PDS_HCAPTCHA_SECRET_KEY") { 86 + Ok(secret) => secret, 87 + Err(_) => { 88 + return json_error_response( 89 + StatusCode::INTERNAL_SERVER_ERROR, 90 + "ServerError", 91 + "hCaptcha is not configured", 92 + ) 93 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 94 + } 95 + }; 96 + 97 + let client = match reqwest::Client::builder() 98 + .timeout(std::time::Duration::from_secs(10)) 99 + .build() 100 + { 101 + Ok(c) => c, 102 + Err(e) => { 103 + log::error!("Failed to create HTTP client: {}", e); 104 + return json_error_response( 105 + StatusCode::INTERNAL_SERVER_ERROR, 106 + "ServerError", 107 + "Failed to verify captcha", 108 + ) 109 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 110 + } 111 + }; 112 + 113 + #[derive(Deserialize, Serialize)] 114 + struct HCaptchaResponse { 115 + success: bool, 116 + challenge_ts: DateTime<Utc>, 117 + hostname: String, 118 + #[serde(rename = "error-codes", default)] 119 + error_codes: Vec<String>, 120 + } 121 + 122 + let verification_result = client 123 + .post("https://api.hcaptcha.com/siteverify") 124 + .form(&[ 125 + ("secret", hcaptcha_secret.as_str()), 126 + ("response", form.h_captcha_response.as_str()), 127 + ]) 128 + .send() 129 + .await; 130 + 131 + let verification_response = match verification_result { 132 + Ok(resp) => resp, 133 + Err(e) => { 134 + log::error!("Failed to verify hCaptcha: {}", e); 135 + 136 + return Redirect::to(&format!( 137 + "/gate?handle={}&state={}&error={}", 138 + url_encode(&params.handle), 139 + url_encode(&params.state), 140 + url_encode("Verification failed. Please try again.") 141 + )) 142 + .into_response(); 143 + } 144 + }; 145 + 146 + let captcha_result: HCaptchaResponse = match verification_response.json().await { 147 + Ok(result) => result, 148 + Err(e) => { 149 + log::error!("Failed to parse hCaptcha response: {}", e); 150 + 151 + return Redirect::to(&format!( 152 + "/gate?handle={}&state={}&error={}", 153 + url_encode(&params.handle), 154 + url_encode(&params.state), 155 + url_encode("Verification failed. Please try again.") 156 + )) 157 + .into_response(); 158 + } 159 + }; 160 + 161 + if !captcha_result.success { 162 + log::warn!( 163 + "hCaptcha verification failed for handle {}: {:?}", 164 + params.handle, 165 + captcha_result.error_codes 166 + ); 167 + return Redirect::to(&format!( 168 + "/gate?handle={}&state={}&error={}", 169 + url_encode(&params.handle), 170 + url_encode(&params.state), 171 + url_encode("Verification failed. Please try again.") 172 + )) 173 + .into_response(); 174 + } 175 + 176 + // Generate secure JWE verification token 177 + let code = match generate_gate_token(&params.handle, &state.app_config.gate_jwe_key) { 178 + Ok(token) => token, 179 + Err(e) => { 180 + log::error!("Failed to generate gate token: {}", e); 181 + return json_error_response( 182 + StatusCode::INTERNAL_SERVER_ERROR, 183 + "ServerError", 184 + "Failed to create verification code", 185 + ) 186 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 187 + } 188 + }; 189 + 190 + let now = Utc::now(); 191 + 192 + // Store the encrypted token in the database 193 + let result = sqlx::query( 194 + "INSERT INTO gate_codes (code, handle, created_at) 195 + VALUES (?, ?, ?)", 196 + ) 197 + .bind(&code) 198 + .bind(&params.handle) 199 + .bind(now) 200 + .execute(&state.pds_gatekeeper_pool) 201 + .await; 202 + 203 + if let Err(e) = result { 204 + log::error!("Failed to store gate code: {}", e); 205 + return json_error_response( 206 + StatusCode::INTERNAL_SERVER_ERROR, 207 + "ServerError", 208 + "Failed to create verification code", 209 + ) 210 + .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()); 211 + } 212 + 213 + // Redirects by origin if it's found. If not redirect to the configured URL. 214 + let mut base_redirect = state.app_config.default_successful_redirect_url.clone(); 215 + if let Some(ref redirect_url) = form.redirect_url { 216 + let trimmed = redirect_url.trim(); 217 + if !trimmed.is_empty() 218 + && (trimmed.starts_with("https://") || trimmed.starts_with("http://")) 219 + { 220 + base_redirect = trimmed.trim_end_matches('/').to_string(); 221 + } 222 + } 223 + 224 + let base_redirect = match state 225 + .app_config 226 + .captcha_success_redirects 227 + .contains(&base_redirect) 228 + { 229 + true => base_redirect, 230 + false => state.app_config.default_successful_redirect_url.clone(), 231 + }; 232 + 233 + // Redirect to client app with code and state 234 + let redirect_url = format!( 235 + "{}/?code={}&state={}", 236 + base_redirect, 237 + url_encode(&code), 238 + url_encode(&params.state) 239 + ); 240 + 241 + Redirect::to(&redirect_url).into_response() 242 + } 243 + 244 + /// Simple URL encode function 245 + fn url_encode(s: &str) -> String { 246 + urlencoding::encode(s).to_string() 247 + }
+177 -14
src/helpers.rs
··· 1 1 use crate::AppState; 2 2 use crate::helpers::TokenCheckError::InvalidToken; 3 3 use anyhow::anyhow; 4 - use axum::body::{Body, to_bytes}; 5 - use axum::extract::Request; 6 - use axum::http::header::CONTENT_TYPE; 7 - use axum::http::{HeaderMap, StatusCode, Uri}; 8 - use axum::response::{IntoResponse, Response}; 4 + use axum::{ 5 + body::{Body, to_bytes}, 6 + extract::Request, 7 + http::header::CONTENT_TYPE, 8 + http::{HeaderMap, StatusCode, Uri}, 9 + response::{IntoResponse, Response}, 10 + }; 9 11 use axum_template::TemplateEngine; 10 12 use chrono::Utc; 11 - use lettre::message::{MultiPart, SinglePart, header}; 12 - use lettre::{AsyncTransport, Message}; 13 + use jacquard_common::{ 14 + service_auth, service_auth::PublicKey, types::did::Did, types::did_doc::VerificationMethod, 15 + types::nsid::Nsid, 16 + }; 17 + use jacquard_identity::{PublicResolver, resolver::IdentityResolver}; 18 + use josekit::jwe::alg::direct::DirectJweAlgorithm; 19 + use lettre::{ 20 + AsyncTransport, Message, 21 + message::{MultiPart, SinglePart, header}, 22 + }; 13 23 use rand::Rng; 14 24 use serde::de::DeserializeOwned; 15 25 use serde_json::{Map, Value}; 16 26 use sha2::{Digest, Sha256}; 17 27 use sqlx::SqlitePool; 28 + use std::sync::Arc; 18 29 use tracing::{error, log}; 19 30 20 31 ///Used to generate the email 2fa code ··· 39 50 where 40 51 T: DeserializeOwned, 41 52 { 42 - let uri = format!("{}{}", state.pds_base_url, path); 53 + let uri = format!("{}{}", state.app_config.pds_base_url, path); 43 54 *req.uri_mut() = Uri::try_from(uri).map_err(|_| StatusCode::BAD_REQUEST)?; 44 55 45 56 let result = state ··· 134 145 full_code.push(UPPERCASE_BASE32_CHARS[idx] as char); 135 146 } 136 147 137 - //The PDS implementation creates in lowercase, then converts to uppercase. 138 - //Just going a head and doing uppercase here. 139 - let slice_one = &full_code[0..5].to_ascii_uppercase(); 140 - let slice_two = &full_code[5..10].to_ascii_uppercase(); 148 + let slice_one = &full_code[0..5]; 149 + let slice_two = &full_code[5..10]; 141 150 format!("{slice_one}-{slice_two}") 142 151 } 143 152 ··· 337 346 338 347 let email_message = Message::builder() 339 348 //TODO prob get the proper type in the state 340 - .from(state.mailer_from.parse()?) 349 + .from(state.app_config.mailer_from.parse()?) 341 350 .to(email.parse()?) 342 - .subject("Sign in to Bluesky") 351 + .subject(&state.app_config.email_subject) 343 352 .multipart( 344 353 MultiPart::alternative() // This is composed of two parts. 345 354 .singlepart( ··· 522 531 523 532 format!("{masked_local}@{masked_domain}") 524 533 } 534 + 535 + pub enum VerifyServiceAuthError { 536 + AuthFailed, 537 + Error(anyhow::Error), 538 + } 539 + 540 + /// Verifies the service auth token that is appended to an XRPC proxy request 541 + pub async fn verify_service_auth( 542 + jwt: &str, 543 + lxm: &Nsid<'static>, 544 + public_resolver: Arc<PublicResolver>, 545 + service_did: &Did<'static>, 546 + //The did of the user wanting to create an account 547 + requested_did: &Did<'static>, 548 + ) -> Result<(), VerifyServiceAuthError> { 549 + let parsed = 550 + service_auth::parse_jwt(jwt).map_err(|e| VerifyServiceAuthError::Error(e.into()))?; 551 + 552 + let claims = parsed.claims(); 553 + 554 + let did_doc = public_resolver 555 + .resolve_did_doc(&requested_did) 556 + .await 557 + .map_err(|err| { 558 + log::error!("Error resolving the service auth for: {}", claims.iss); 559 + return VerifyServiceAuthError::Error(err.into()); 560 + })?; 561 + 562 + // Parse the DID document response to get verification methods 563 + let doc = did_doc.parse().map_err(|err| { 564 + log::error!("Error parsing the service auth did doc: {}", claims.iss); 565 + VerifyServiceAuthError::Error(anyhow::anyhow!(err)) 566 + })?; 567 + 568 + let verification_methods = doc.verification_method.as_deref().ok_or_else(|| { 569 + VerifyServiceAuthError::Error(anyhow::anyhow!( 570 + "No verification methods in did doc: {}", 571 + &claims.iss 572 + )) 573 + })?; 574 + 575 + let signing_key = extract_signing_key(verification_methods).ok_or_else(|| { 576 + VerifyServiceAuthError::Error(anyhow::anyhow!( 577 + "No signing key found in did doc: {}", 578 + &claims.iss 579 + )) 580 + })?; 581 + 582 + service_auth::verify_signature(&parsed, &signing_key).map_err(|err| { 583 + log::error!("Error verifying service auth signature: {}", err); 584 + VerifyServiceAuthError::AuthFailed 585 + })?; 586 + 587 + // Now validate claims (audience, expiration, etc.) 588 + claims.validate(service_did).map_err(|e| { 589 + log::error!("Error validating service auth claims: {}", e); 590 + VerifyServiceAuthError::AuthFailed 591 + })?; 592 + 593 + if claims.aud != *service_did { 594 + log::error!("Invalid audience (did:web): {}", claims.aud); 595 + return Err(VerifyServiceAuthError::AuthFailed); 596 + } 597 + 598 + let lxm_from_claims = claims.lxm.as_ref().ok_or_else(|| { 599 + VerifyServiceAuthError::Error(anyhow::anyhow!("No lxm claim in service auth JWT")) 600 + })?; 601 + 602 + if lxm_from_claims != lxm { 603 + return Err(VerifyServiceAuthError::Error(anyhow::anyhow!( 604 + "Invalid XRPC endpoint requested" 605 + ))); 606 + } 607 + Ok(()) 608 + } 609 + 610 + /// Ripped from Jacquard 611 + /// 612 + /// Extract the signing key from a DID document's verification methods. 613 + /// 614 + /// This looks for a key with type "atproto" or the first available key 615 + /// if no atproto-specific key is found. 616 + fn extract_signing_key(methods: &[VerificationMethod]) -> Option<PublicKey> { 617 + // First try to find an atproto-specific key 618 + let atproto_method = methods 619 + .iter() 620 + .find(|m| m.r#type.as_ref() == "Multikey" || m.r#type.as_ref() == "atproto"); 621 + 622 + let method = atproto_method.or_else(|| methods.first())?; 623 + 624 + // Parse the multikey 625 + let public_key_multibase = method.public_key_multibase.as_ref()?; 626 + 627 + // Decode multibase 628 + let (_, key_bytes) = multibase::decode(public_key_multibase.as_ref()).ok()?; 629 + 630 + // First two bytes are the multicodec prefix 631 + if key_bytes.len() < 2 { 632 + return None; 633 + } 634 + 635 + let codec = &key_bytes[..2]; 636 + let key_material = &key_bytes[2..]; 637 + 638 + match codec { 639 + // p256-pub (0x1200) 640 + [0x80, 0x24] => PublicKey::from_p256_bytes(key_material).ok(), 641 + // secp256k1-pub (0xe7) 642 + [0xe7, 0x01] => PublicKey::from_k256_bytes(key_material).ok(), 643 + _ => None, 644 + } 645 + } 646 + 647 + /// Payload for gate JWE tokens 648 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 649 + pub struct GateTokenPayload { 650 + pub handle: String, 651 + pub created_at: String, 652 + } 653 + 654 + /// Generate a secure JWE token for gate verification 655 + pub fn generate_gate_token(handle: &str, encryption_key: &[u8]) -> Result<String, anyhow::Error> { 656 + use josekit::jwe::{JweHeader, alg::direct::DirectJweAlgorithm}; 657 + 658 + let payload = GateTokenPayload { 659 + handle: handle.to_string(), 660 + created_at: Utc::now().to_rfc3339(), 661 + }; 662 + 663 + let payload_json = serde_json::to_string(&payload)?; 664 + 665 + let mut header = JweHeader::new(); 666 + header.set_token_type("JWT"); 667 + header.set_content_encryption("A128CBC-HS256"); 668 + 669 + let encrypter = DirectJweAlgorithm::Dir.encrypter_from_bytes(encryption_key)?; 670 + 671 + // Encrypt 672 + let jwe = josekit::jwe::serialize_compact(payload_json.as_bytes(), &header, &encrypter)?; 673 + 674 + Ok(jwe) 675 + } 676 + 677 + /// Verify and decrypt a gate JWE token, returning the payload if valid 678 + pub fn verify_gate_token( 679 + token: &str, 680 + encryption_key: &[u8], 681 + ) -> Result<GateTokenPayload, anyhow::Error> { 682 + let decrypter = DirectJweAlgorithm::Dir.decrypter_from_bytes(encryption_key)?; 683 + let (payload_bytes, _header) = josekit::jwe::deserialize_compact(token, &decrypter)?; 684 + let payload: GateTokenPayload = serde_json::from_slice(&payload_bytes)?; 685 + 686 + Ok(payload) 687 + }
+206 -33
src/main.rs
··· 1 1 #![warn(clippy::unwrap_used)] 2 + use crate::gate::{get_gate, post_gate}; 2 3 use crate::oauth_provider::sign_in; 3 - use crate::xrpc::com_atproto_server::{create_session, get_session, update_email}; 4 - use axum::body::Body; 5 - use axum::handler::Handler; 6 - use axum::http::{Method, header}; 7 - use axum::middleware as ax_middleware; 8 - use axum::routing::post; 9 - use axum::{Router, routing::get}; 4 + use crate::xrpc::com_atproto_server::{ 5 + create_account, create_session, describe_server, get_session, update_email, 6 + }; 7 + use axum::{ 8 + Router, 9 + body::Body, 10 + handler::Handler, 11 + http::{Method, header}, 12 + middleware as ax_middleware, 13 + routing::get, 14 + routing::post, 15 + }; 10 16 use axum_template::engine::Engine; 11 17 use handlebars::Handlebars; 12 - use hyper_util::client::legacy::connect::HttpConnector; 13 - use hyper_util::rt::TokioExecutor; 18 + use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 19 + use jacquard_common::types::did::Did; 20 + use jacquard_identity::{PublicResolver, resolver::PlcSource}; 14 21 use lettre::{AsyncSmtpTransport, Tokio1Executor}; 22 + use rand::Rng; 15 23 use rust_embed::RustEmbed; 16 24 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; 17 25 use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; 18 26 use std::path::Path; 27 + use std::sync::Arc; 19 28 use std::time::Duration; 20 29 use std::{env, net::SocketAddr}; 21 - use tower_governor::GovernorLayer; 22 - use tower_governor::governor::GovernorConfigBuilder; 23 - use tower_http::compression::CompressionLayer; 24 - use tower_http::cors::{Any, CorsLayer}; 30 + use tower_governor::{ 31 + GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, 32 + }; 33 + use tower_http::{ 34 + compression::CompressionLayer, 35 + cors::{Any, CorsLayer}, 36 + }; 25 37 use tracing::log; 26 38 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 27 39 40 + mod gate; 28 41 pub mod helpers; 29 42 mod middleware; 30 43 mod oauth_provider; ··· 37 50 #[include = "*.hbs"] 38 51 struct EmailTemplates; 39 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 + 40 148 #[derive(Clone)] 41 149 pub struct AppState { 42 150 account_pool: SqlitePool, 43 151 pds_gatekeeper_pool: SqlitePool, 44 152 reverse_proxy_client: HyperUtilClient, 45 - pds_base_url: String, 46 153 mailer: AsyncSmtpTransport<Tokio1Executor>, 47 - mailer_from: String, 48 154 template_engine: Engine<Handlebars<'static>>, 155 + resolver: Arc<PublicResolver>, 156 + app_config: AppConfig, 49 157 } 50 158 51 159 async fn root_handler() -> impl axum::response::IntoResponse { ··· 91 199 let pds_env_location = 92 200 env::var("PDS_ENV_LOCATION").unwrap_or_else(|_| "/pds/pds.env".to_string()); 93 201 94 - dotenvy::from_path(Path::new(&pds_env_location))?; 95 - let pds_root = env::var("PDS_DATA_DIRECTORY")?; 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"); 96 211 let account_db_url = format!("{pds_root}/account.sqlite"); 97 212 98 213 let account_options = SqliteConnectOptions::new() ··· 129 244 //Emailer set up 130 245 let smtp_url = 131 246 env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 132 - let sent_from = env::var("PDS_EMAIL_FROM_ADDRESS") 133 - .expect("PDS_EMAIL_FROM_ADDRESS is not set in your pds.env file"); 134 247 135 248 let mailer: AsyncSmtpTransport<Tokio1Executor> = 136 249 AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); ··· 147 260 let _ = hbs.register_embed_templates::<EmailTemplates>(); 148 261 } 149 262 150 - let pds_base_url = 151 - env::var("PDS_BASE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string()); 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()); 152 273 153 274 let state = AppState { 154 275 account_pool, 155 276 pds_gatekeeper_pool, 156 277 reverse_proxy_client: client, 157 - pds_base_url, 158 278 mailer, 159 - mailer_from: sent_from, 160 279 template_engine: Engine::from(hbs), 280 + resolver: Arc::new(resolver), 281 + app_config: AppConfig::new(), 161 282 }; 162 283 163 284 // Rate limiting 164 285 //Allows 5 within 60 seconds, and after 60 should drop one off? So hit 5, then goes to 4 after 60 seconds. 165 - let create_session_governor_conf = GovernorConfigBuilder::default() 286 + let captcha_governor_conf = GovernorConfigBuilder::default() 166 287 .per_second(60) 167 288 .burst_size(5) 289 + .key_extractor(SmartIpKeyExtractor) 168 290 .finish() 169 - .expect("failed to create governor config. this should not happen and is a bug"); 291 + .expect("failed to create governor config for create session. this should not happen and is a bug"); 170 292 171 293 // Create a second config with the same settings for the other endpoint 172 294 let sign_in_governor_conf = GovernorConfigBuilder::default() 173 295 .per_second(60) 174 296 .burst_size(5) 297 + .key_extractor(SmartIpKeyExtractor) 175 298 .finish() 176 - .expect("failed to create governor config. this should not happen and is a bug"); 299 + .expect( 300 + "failed to create governor config for sign in. this should not happen and is a bug", 301 + ); 302 + 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(); 307 + 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); 316 + } 177 317 178 - let create_session_governor_limiter = create_session_governor_conf.limiter().clone(); 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(); 179 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 + 180 338 let interval = Duration::from_secs(60); 181 339 // a separate background task to clean up 182 340 std::thread::spawn(move || { 183 341 loop { 184 342 std::thread::sleep(interval); 185 - create_session_governor_limiter.retain_recent(); 343 + captcha_governor_limiter.retain_recent(); 186 344 sign_in_governor_limiter.retain_recent(); 345 + create_account_governor_limiter.retain_recent(); 187 346 } 188 347 }); 189 348 ··· 192 351 .allow_methods([Method::GET, Method::OPTIONS, Method::POST]) 193 352 .allow_headers(Any); 194 353 195 - let app = Router::new() 354 + let mut app = Router::new() 196 355 .route("/", get(root_handler)) 356 + .route("/xrpc/com.atproto.server.getSession", get(get_session)) 197 357 .route( 198 - "/xrpc/com.atproto.server.getSession", 199 - get(get_session).layer(ax_middleware::from_fn(middleware::extract_did)), 358 + "/xrpc/com.atproto.server.describeServer", 359 + get(describe_server), 200 360 ) 201 361 .route( 202 362 "/xrpc/com.atproto.server.updateEmail", ··· 204 364 ) 205 365 .route( 206 366 "/@atproto/oauth-provider/~api/sign-in", 207 - post(sign_in).layer(GovernorLayer::new(sign_in_governor_conf)), 367 + post(sign_in).layer(sign_in_governor_layer.clone()), 208 368 ) 209 369 .route( 210 370 "/xrpc/com.atproto.server.createSession", 211 - post(create_session.layer(GovernorLayer::new(create_session_governor_conf))), 371 + post(create_session.layer(sign_in_governor_layer)), 212 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 213 386 .layer(CompressionLayer::new()) 214 387 .layer(cors) 215 388 .with_state(state);
+73 -39
src/middleware.rs
··· 12 12 #[derive(Clone, Debug)] 13 13 pub struct Did(pub Option<String>); 14 14 15 + #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 + pub enum AuthScheme { 17 + Bearer, 18 + DPoP, 19 + } 20 + 15 21 #[derive(Serialize, Deserialize)] 16 22 pub struct TokenClaims { 17 23 pub sub: String, 18 24 } 19 25 20 26 pub async fn extract_did(mut req: Request, next: Next) -> impl IntoResponse { 21 - let token = extract_bearer(req.headers()); 27 + let auth = extract_auth(req.headers()); 22 28 23 - match token { 24 - Ok(token) => { 25 - match token { 29 + match auth { 30 + Ok(auth_opt) => { 31 + match auth_opt { 26 32 None => json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 27 33 .expect("Error creating an error response"), 28 - Some(token) => { 29 - let token = UntrustedToken::new(&token); 30 - if token.is_err() { 31 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 32 - .expect("Error creating an error response"); 33 - } 34 - let parsed_token = token.expect("Already checked for error"); 35 - let claims: Result<Claims<TokenClaims>, ValidationError> = 36 - parsed_token.deserialize_claims_unchecked(); 37 - if claims.is_err() { 38 - return json_error_response(StatusCode::BAD_REQUEST, "TokenRequired", "") 39 - .expect("Error creating an error response"); 40 - } 34 + Some((scheme, token_str)) => { 35 + // For Bearer, validate JWT and extract DID from `sub`. 36 + // For DPoP, we currently only pass through and do not validate here; insert None DID. 37 + match scheme { 38 + AuthScheme::Bearer => { 39 + let token = UntrustedToken::new(&token_str); 40 + if token.is_err() { 41 + return json_error_response( 42 + StatusCode::BAD_REQUEST, 43 + "TokenRequired", 44 + "", 45 + ) 46 + .expect("Error creating an error response"); 47 + } 48 + let parsed_token = token.expect("Already checked for error"); 49 + let claims: Result<Claims<TokenClaims>, ValidationError> = 50 + parsed_token.deserialize_claims_unchecked(); 51 + if claims.is_err() { 52 + return json_error_response( 53 + StatusCode::BAD_REQUEST, 54 + "TokenRequired", 55 + "", 56 + ) 57 + .expect("Error creating an error response"); 58 + } 41 59 42 - let key = Hs256Key::new( 43 - env::var("PDS_JWT_SECRET").expect("PDS_JWT_SECRET not set in the pds.env"), 44 - ); 45 - let token: Result<Token<TokenClaims>, ValidationError> = 46 - Hs256.validator(&key).validate(&parsed_token); 47 - if token.is_err() { 48 - return json_error_response(StatusCode::BAD_REQUEST, "InvalidToken", "") 49 - .expect("Error creating an error response"); 60 + let key = Hs256Key::new( 61 + env::var("PDS_JWT_SECRET") 62 + .expect("PDS_JWT_SECRET not set in the pds.env"), 63 + ); 64 + let token: Result<Token<TokenClaims>, ValidationError> = 65 + Hs256.validator(&key).validate(&parsed_token); 66 + if token.is_err() { 67 + return json_error_response( 68 + StatusCode::BAD_REQUEST, 69 + "InvalidToken", 70 + "", 71 + ) 72 + .expect("Error creating an error response"); 73 + } 74 + let token = token.expect("Already checked for error,"); 75 + req.extensions_mut() 76 + .insert(Did(Some(token.claims().custom.sub.clone()))); 77 + } 78 + AuthScheme::DPoP => { 79 + //Not going to worry about oauth email update for now, just always forward to the PDS 80 + req.extensions_mut().insert(Did(None)); 81 + } 50 82 } 51 - let token = token.expect("Already checked for error,"); 52 - //Not going to worry about expiration since it still goes to the PDS 53 - req.extensions_mut() 54 - .insert(Did(Some(token.claims().custom.sub.clone()))); 83 + 55 84 next.run(req).await 56 85 } 57 86 } ··· 64 93 } 65 94 } 66 95 67 - fn extract_bearer(headers: &HeaderMap) -> Result<Option<String>, String> { 96 + fn extract_auth(headers: &HeaderMap) -> Result<Option<(AuthScheme, String)>, String> { 68 97 match headers.get(axum::http::header::AUTHORIZATION) { 69 98 None => Ok(None), 70 - Some(hv) => match hv.to_str() { 71 - Err(_) => Err("Authorization header is not valid".into()), 72 - Ok(s) => { 73 - // Accept forms like: "Bearer <token>" (case-sensitive for the scheme here) 74 - let mut parts = s.splitn(2, ' '); 75 - match (parts.next(), parts.next()) { 76 - (Some("Bearer"), Some(tok)) if !tok.is_empty() => Ok(Some(tok.to_string())), 77 - _ => Err("Authorization header must be in format 'Bearer <token>'".into()), 99 + Some(hv) => { 100 + match hv.to_str() { 101 + Err(_) => Err("Authorization header is not valid".into()), 102 + Ok(s) => { 103 + // Accept forms like: "Bearer <token>" or "DPoP <token>" (case-sensitive for the scheme here) 104 + let mut parts = s.splitn(2, ' '); 105 + match (parts.next(), parts.next()) { 106 + (Some("Bearer"), Some(tok)) if !tok.is_empty() => 107 + Ok(Some((AuthScheme::Bearer, tok.to_string()))), 108 + (Some("DPoP"), Some(tok)) if !tok.is_empty() => 109 + Ok(Some((AuthScheme::DPoP, tok.to_string()))), 110 + _ => Err("Authorization header must be in format 'Bearer <token>' or 'DPoP <token>'".into()), 111 + } 78 112 } 79 113 } 80 - }, 114 + } 81 115 } 82 116 }
+3 -2
src/oauth_provider.rs
··· 13 13 pub struct SignInRequest { 14 14 pub username: String, 15 15 pub password: String, 16 - pub remember: bool, 16 + #[serde(skip_serializing_if = "Option::is_none")] 17 + pub remember: Option<bool>, 17 18 pub locale: String, 18 19 #[serde(skip_serializing_if = "Option::is_none", rename = "emailOtp")] 19 20 pub email_otp: Option<String>, ··· 56 57 //No 2FA or already passed 57 58 let uri = format!( 58 59 "{}{}", 59 - state.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" 60 + state.app_config.pds_base_url, "/@atproto/oauth-provider/~api/sign-in" 60 61 ); 61 62 62 63 let mut req = axum::http::Request::post(uri);
+394 -53
src/xrpc/com_atproto_server.rs
··· 1 1 use crate::AppState; 2 2 use crate::helpers::{ 3 - AuthResult, ProxiedResult, TokenCheckError, json_error_response, preauth_check, proxy_get_json, 3 + AuthResult, ProxiedResult, TokenCheckError, VerifyServiceAuthError, json_error_response, 4 + preauth_check, proxy_get_json, verify_gate_token, verify_service_auth, 4 5 }; 5 6 use crate::middleware::Did; 6 - use axum::body::Body; 7 + use axum::body::{Body, to_bytes}; 7 8 use axum::extract::State; 8 - use axum::http::{HeaderMap, StatusCode}; 9 + use axum::http::{HeaderMap, StatusCode, header}; 9 10 use axum::response::{IntoResponse, Response}; 10 11 use axum::{Extension, Json, debug_handler, extract, extract::Request}; 12 + use chrono::{Duration, Utc}; 13 + use jacquard_common::types::did::Did as JacquardDid; 11 14 use serde::{Deserialize, Serialize}; 12 15 use serde_json; 13 16 use tracing::log; ··· 61 64 allow_takendown: Option<bool>, 62 65 } 63 66 67 + #[derive(Deserialize, Serialize, Debug)] 68 + #[serde(rename_all = "camelCase")] 69 + pub struct CreateAccountRequest { 70 + handle: String, 71 + #[serde(skip_serializing_if = "Option::is_none")] 72 + email: Option<String>, 73 + #[serde(skip_serializing_if = "Option::is_none")] 74 + password: Option<String>, 75 + #[serde(skip_serializing_if = "Option::is_none")] 76 + did: Option<String>, 77 + #[serde(skip_serializing_if = "Option::is_none")] 78 + invite_code: Option<String>, 79 + #[serde(skip_serializing_if = "Option::is_none")] 80 + verification_code: Option<String>, 81 + #[serde(skip_serializing_if = "Option::is_none")] 82 + plc_op: Option<serde_json::Value>, 83 + } 84 + 85 + #[derive(Deserialize, Serialize, Debug, Clone)] 86 + #[serde(rename_all = "camelCase")] 87 + pub struct DescribeServerContact { 88 + #[serde(skip_serializing_if = "Option::is_none")] 89 + email: Option<String>, 90 + } 91 + 92 + #[derive(Deserialize, Serialize, Debug, Clone)] 93 + #[serde(rename_all = "camelCase")] 94 + pub struct DescribeServerLinks { 95 + #[serde(skip_serializing_if = "Option::is_none")] 96 + privacy_policy: Option<String>, 97 + #[serde(skip_serializing_if = "Option::is_none")] 98 + terms_of_service: Option<String>, 99 + } 100 + 101 + #[derive(Deserialize, Serialize, Debug, Clone)] 102 + #[serde(rename_all = "camelCase")] 103 + pub struct DescribeServerResponse { 104 + #[serde(skip_serializing_if = "Option::is_none")] 105 + invite_code_required: Option<bool>, 106 + #[serde(skip_serializing_if = "Option::is_none")] 107 + phone_verification_required: Option<bool>, 108 + #[serde(skip_serializing_if = "Option::is_none")] 109 + available_user_domains: Option<Vec<String>>, 110 + #[serde(skip_serializing_if = "Option::is_none")] 111 + links: Option<DescribeServerLinks>, 112 + #[serde(skip_serializing_if = "Option::is_none")] 113 + contact: Option<DescribeServerContact>, 114 + #[serde(skip_serializing_if = "Option::is_none")] 115 + did: Option<String>, 116 + } 117 + 64 118 pub async fn create_session( 65 119 State(state): State<AppState>, 66 120 headers: HeaderMap, ··· 87 141 ) 88 142 } 89 143 AuthResult::ProxyThrough => { 90 - log::info!("Proxying through"); 91 144 //No 2FA or already passed 92 145 let uri = format!( 93 146 "{}{}", 94 - state.pds_base_url, "/xrpc/com.atproto.server.createSession" 147 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createSession" 95 148 ); 96 149 97 150 let mut req = axum::http::Request::post(uri); ··· 148 201 //If email auth is set it is to either turn on or off 2fa 149 202 let email_auth_update = payload.email_auth_factor.unwrap_or(false); 150 203 151 - // Email update asked for 152 - if email_auth_update { 153 - let email = payload.email.clone(); 154 - let email_confirmed = sqlx::query_as::<_, (String,)>( 155 - "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 156 - ) 157 - .bind(&email) 158 - .fetch_optional(&state.account_pool) 159 - .await 160 - .map_err(|_| StatusCode::BAD_REQUEST)?; 161 - 162 - //Since the email is already confirmed we can enable 2fa 163 - return match email_confirmed { 164 - None => Err(StatusCode::BAD_REQUEST), 165 - Some(did_row) => { 166 - let _ = sqlx::query( 167 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 168 - ) 169 - .bind(&did_row.0) 170 - .execute(&state.pds_gatekeeper_pool) 171 - .await 172 - .map_err(|_| StatusCode::BAD_REQUEST)?; 173 - 174 - Ok(StatusCode::OK.into_response()) 175 - } 176 - }; 177 - } 204 + //This means the middleware successfully extracted a did from the request, if not it just needs to be forward to the PDS 205 + //This is also empty if it is an oauth request, which is not supported by gatekeeper turning on 2fa since the dpop stuff needs to be implemented 206 + let did_is_not_empty = did.0.is_some(); 178 207 179 - // User wants auth turned off 180 - if !email_auth_update && !email_auth_not_set { 181 - //User wants auth turned off and has a token 182 - if let Some(token) = &payload.token { 183 - let token_found = sqlx::query_as::<_, (String,)>( 184 - "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 208 + if did_is_not_empty { 209 + // Email update asked for 210 + if email_auth_update { 211 + let email = payload.email.clone(); 212 + let email_confirmed = match sqlx::query_as::<_, (String,)>( 213 + "SELECT did FROM account WHERE emailConfirmedAt IS NOT NULL AND email = ?", 185 214 ) 186 - .bind(token) 187 - .bind(&did.0) 215 + .bind(&email) 188 216 .fetch_optional(&state.account_pool) 189 217 .await 190 - .map_err(|_| StatusCode::BAD_REQUEST)?; 218 + { 219 + Ok(row) => row, 220 + Err(err) => { 221 + log::error!("Error checking if email is confirmed: {err}"); 222 + return Err(StatusCode::BAD_REQUEST); 223 + } 224 + }; 191 225 192 - if token_found.is_some() { 193 - let _ = sqlx::query( 194 - "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 226 + //Since the email is already confirmed we can enable 2fa 227 + return match email_confirmed { 228 + None => Err(StatusCode::BAD_REQUEST), 229 + Some(did_row) => { 230 + let _ = sqlx::query( 231 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 1) ON CONFLICT(did) DO UPDATE SET required = 1", 232 + ) 233 + .bind(&did_row.0) 234 + .execute(&state.pds_gatekeeper_pool) 235 + .await 236 + .map_err(|_| StatusCode::BAD_REQUEST)?; 237 + 238 + Ok(StatusCode::OK.into_response()) 239 + } 240 + }; 241 + } 242 + 243 + // User wants auth turned off 244 + if !email_auth_update && !email_auth_not_set { 245 + //User wants auth turned off and has a token 246 + if let Some(token) = &payload.token { 247 + let token_found = match sqlx::query_as::<_, (String,)>( 248 + "SELECT token FROM email_token WHERE token = ? AND did = ? AND purpose = 'update_email'", 195 249 ) 196 - .bind(&did.0) 197 - .execute(&state.pds_gatekeeper_pool) 198 - .await 199 - .map_err(|_| StatusCode::BAD_REQUEST)?; 250 + .bind(token) 251 + .bind(&did.0) 252 + .fetch_optional(&state.account_pool) 253 + .await{ 254 + Ok(token) => token, 255 + Err(err) => { 256 + log::error!("Error checking if token is valid: {err}"); 257 + return Err(StatusCode::BAD_REQUEST); 258 + } 259 + }; 200 260 201 - return Ok(StatusCode::OK.into_response()); 202 - } else { 203 - return Err(StatusCode::BAD_REQUEST); 261 + return if token_found.is_some() { 262 + //TODO I think there may be a bug here and need to do some retry logic 263 + // First try was erroring, seconds was allowing 264 + match sqlx::query( 265 + "INSERT INTO two_factor_accounts (did, required) VALUES (?, 0) ON CONFLICT(did) DO UPDATE SET required = 0", 266 + ) 267 + .bind(&did.0) 268 + .execute(&state.pds_gatekeeper_pool) 269 + .await { 270 + Ok(_) => {} 271 + Err(err) => { 272 + log::error!("Error updating email auth: {err}"); 273 + return Err(StatusCode::BAD_REQUEST); 274 + } 275 + } 276 + 277 + Ok(StatusCode::OK.into_response()) 278 + } else { 279 + Err(StatusCode::BAD_REQUEST) 280 + }; 204 281 } 205 282 } 206 283 } 207 - 208 284 // Updating the actual email address by sending it on to the PDS 209 285 let uri = format!( 210 286 "{}{}", 211 - state.pds_base_url, "/xrpc/com.atproto.server.updateEmail" 287 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.updateEmail" 212 288 ); 213 289 let mut req = axum::http::Request::post(uri); 214 290 if let Some(req_headers) = req.headers_mut() { ··· 260 336 ProxiedResult::Passthrough(resp) => Ok(resp), 261 337 } 262 338 } 339 + 340 + pub async fn describe_server( 341 + State(state): State<AppState>, 342 + req: Request, 343 + ) -> Result<Response<Body>, StatusCode> { 344 + match proxy_get_json::<DescribeServerResponse>( 345 + &state, 346 + req, 347 + "/xrpc/com.atproto.server.describeServer", 348 + ) 349 + .await? 350 + { 351 + ProxiedResult::Parsed { 352 + value: mut server_info, 353 + .. 354 + } => { 355 + //This signifies the server is configured for captcha verification 356 + server_info.phone_verification_required = Some(state.app_config.use_captcha); 357 + Ok(Json(server_info).into_response()) 358 + } 359 + ProxiedResult::Passthrough(resp) => Ok(resp), 360 + } 361 + } 362 + 363 + /// Verify a gate code matches the handle and is not expired 364 + async fn verify_gate_code( 365 + state: &AppState, 366 + code: &str, 367 + handle: &str, 368 + ) -> Result<bool, anyhow::Error> { 369 + // First, decrypt and verify the JWE token 370 + let payload = match verify_gate_token(code, &state.app_config.gate_jwe_key) { 371 + Ok(p) => p, 372 + Err(e) => { 373 + log::warn!("Failed to decrypt gate token: {}", e); 374 + return Ok(false); 375 + } 376 + }; 377 + 378 + // Verify the handle matches 379 + if payload.handle != handle { 380 + log::warn!( 381 + "Gate code handle mismatch: expected {}, got {}", 382 + handle, 383 + payload.handle 384 + ); 385 + return Ok(false); 386 + } 387 + 388 + let created_at = chrono::DateTime::parse_from_rfc3339(&payload.created_at) 389 + .map_err(|e| anyhow::anyhow!("Failed to parse created_at from token: {}", e))? 390 + .with_timezone(&Utc); 391 + 392 + let now = Utc::now(); 393 + let age = now - created_at; 394 + 395 + // Check if the token is expired (5 minutes) 396 + if age > Duration::minutes(5) { 397 + log::warn!("Gate code expired for handle {}", handle); 398 + return Ok(false); 399 + } 400 + 401 + // Verify the token exists in the database (to prevent reuse) 402 + let row: Option<(String,)> = 403 + sqlx::query_as("SELECT code FROM gate_codes WHERE code = ? and handle = ? LIMIT 1") 404 + .bind(code) 405 + .bind(handle) 406 + .fetch_optional(&state.pds_gatekeeper_pool) 407 + .await?; 408 + 409 + if row.is_none() { 410 + log::warn!("Gate code not found in database or already used"); 411 + return Ok(false); 412 + } 413 + 414 + // Token is valid, delete it so it can't be reused 415 + //TODO probably also delete expired codes? Will need to do that at some point probably altho the where is on code and handle 416 + 417 + sqlx::query("DELETE FROM gate_codes WHERE code = ?") 418 + .bind(code) 419 + .execute(&state.pds_gatekeeper_pool) 420 + .await?; 421 + 422 + Ok(true) 423 + } 424 + 425 + pub async fn create_account( 426 + State(state): State<AppState>, 427 + req: Request, 428 + ) -> Result<Response<Body>, StatusCode> { 429 + let headers = req.headers().clone(); 430 + let body_bytes = to_bytes(req.into_body(), usize::MAX) 431 + .await 432 + .map_err(|_| StatusCode::BAD_REQUEST)?; 433 + 434 + // Parse the body to check for verification code 435 + let account_request: CreateAccountRequest = 436 + serde_json::from_slice(&body_bytes).map_err(|e| { 437 + log::error!("Failed to parse create account request: {}", e); 438 + StatusCode::BAD_REQUEST 439 + })?; 440 + 441 + // Check for service auth (migrations) if configured 442 + if state.app_config.allow_only_migrations { 443 + // Expect Authorization: Bearer <jwt> 444 + let auth_header = headers 445 + .get(header::AUTHORIZATION) 446 + .and_then(|v| v.to_str().ok()) 447 + .map(str::to_string); 448 + 449 + let Some(value) = auth_header else { 450 + log::error!("No Authorization header found in the request"); 451 + return json_error_response( 452 + StatusCode::UNAUTHORIZED, 453 + "InvalidAuth", 454 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 455 + ); 456 + }; 457 + 458 + // Ensure Bearer prefix 459 + let token = value.strip_prefix("Bearer ").unwrap_or("").trim(); 460 + if token.is_empty() { 461 + log::error!("No Service Auth token found in the Authorization header"); 462 + return json_error_response( 463 + StatusCode::UNAUTHORIZED, 464 + "InvalidAuth", 465 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 466 + ); 467 + } 468 + 469 + // Ensure a non-empty DID was provided when migrations are enabled 470 + let requested_did_str = match account_request.did.as_deref() { 471 + Some(s) if !s.trim().is_empty() => s, 472 + _ => { 473 + return json_error_response( 474 + StatusCode::BAD_REQUEST, 475 + "InvalidRequest", 476 + "The 'did' field is required when migrations are enforced.", 477 + ); 478 + } 479 + }; 480 + 481 + // Parse the DID into the expected type for verification 482 + let requested_did: JacquardDid<'static> = match requested_did_str.parse() { 483 + Ok(d) => d, 484 + Err(e) => { 485 + log::error!( 486 + "Invalid DID format provided in createAccount: {} | error: {}", 487 + requested_did_str, 488 + e 489 + ); 490 + return json_error_response( 491 + StatusCode::BAD_REQUEST, 492 + "InvalidRequest", 493 + "The 'did' field is not a valid DID.", 494 + ); 495 + } 496 + }; 497 + 498 + let nsid = "com.atproto.server.createAccount".parse().unwrap(); 499 + match verify_service_auth( 500 + token, 501 + &nsid, 502 + state.resolver.clone(), 503 + &state.app_config.pds_service_did, 504 + &requested_did, 505 + ) 506 + .await 507 + { 508 + //Just do nothing if it passes so it continues. 509 + Ok(_) => {} 510 + Err(err) => match err { 511 + VerifyServiceAuthError::AuthFailed => { 512 + return json_error_response( 513 + StatusCode::UNAUTHORIZED, 514 + "InvalidAuth", 515 + "This PDS is configured to only allow accounts created by migrations via this endpoint.", 516 + ); 517 + } 518 + VerifyServiceAuthError::Error(err) => { 519 + log::error!("Error verifying service auth token: {err}"); 520 + return json_error_response( 521 + StatusCode::BAD_REQUEST, 522 + "InvalidRequest", 523 + "There has been an error, please contact your PDS administrator for help and for them to review the server logs.", 524 + ); 525 + } 526 + }, 527 + } 528 + } 529 + 530 + // Check for captcha verification if configured 531 + if state.app_config.use_captcha { 532 + if let Some(ref verification_code) = account_request.verification_code { 533 + match verify_gate_code(&state, verification_code, &account_request.handle).await { 534 + //TODO has a few errors to support 535 + 536 + //expired token 537 + // { 538 + // "error": "ExpiredToken", 539 + // "message": "Token has expired" 540 + // } 541 + 542 + //TODO ALSO add rate limits on the /gate endpoints so they can't be abused 543 + Ok(true) => { 544 + log::info!("Gate code verified for handle: {}", account_request.handle); 545 + } 546 + Ok(false) => { 547 + log::warn!( 548 + "Invalid or expired gate code for handle: {}", 549 + account_request.handle 550 + ); 551 + return json_error_response( 552 + StatusCode::BAD_REQUEST, 553 + "InvalidToken", 554 + "Token could not be verified", 555 + ); 556 + } 557 + Err(e) => { 558 + log::error!("Error verifying gate code: {}", e); 559 + return json_error_response( 560 + StatusCode::INTERNAL_SERVER_ERROR, 561 + "InvalidToken", 562 + "Token could not be verified", 563 + ); 564 + } 565 + } 566 + } else { 567 + // No verification code provided but captcha is required 568 + log::warn!( 569 + "No verification code provided for account creation: {}", 570 + account_request.handle 571 + ); 572 + return json_error_response( 573 + StatusCode::BAD_REQUEST, 574 + "InvalidRequest", 575 + "Verification is now required on this server.", 576 + ); 577 + } 578 + } 579 + 580 + // Rebuild the request with the same body and headers 581 + let uri = format!( 582 + "{}{}", 583 + state.app_config.pds_base_url, "/xrpc/com.atproto.server.createAccount" 584 + ); 585 + 586 + let mut new_req = axum::http::Request::post(&uri); 587 + if let Some(req_headers) = new_req.headers_mut() { 588 + *req_headers = headers; 589 + } 590 + 591 + let new_req = new_req 592 + .body(Body::from(body_bytes)) 593 + .map_err(|_| StatusCode::BAD_REQUEST)?; 594 + 595 + let proxied = state 596 + .reverse_proxy_client 597 + .request(new_req) 598 + .await 599 + .map_err(|_| StatusCode::BAD_REQUEST)? 600 + .into_response(); 601 + 602 + Ok(proxied) 603 + }