learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: PDS publishing and Firehose integration

+1472 -116
+812 -30
Cargo.lock
··· 12 12 ] 13 13 14 14 [[package]] 15 + name = "allocator-api2" 16 + version = "0.2.21" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 + 20 + [[package]] 15 21 name = "android_system_properties" 16 22 version = "0.1.5" 17 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 71 77 ] 72 78 73 79 [[package]] 80 + name = "anyhow" 81 + version = "1.0.100" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 84 + 85 + [[package]] 74 86 name = "async-trait" 75 87 version = "0.1.89" 76 88 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 88 100 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 89 101 90 102 [[package]] 103 + name = "atproto-identity" 104 + version = "0.13.0" 105 + source = "registry+https://github.com/rust-lang/crates.io-index" 106 + checksum = "b956c07726fce812630be63c5cb31b1961cbb70f0a05614278523102d78c3a48" 107 + dependencies = [ 108 + "anyhow", 109 + "async-trait", 110 + "ecdsa", 111 + "elliptic-curve", 112 + "hickory-resolver", 113 + "k256", 114 + "lru", 115 + "multibase", 116 + "p256", 117 + "p384", 118 + "rand 0.8.5", 119 + "reqwest 0.12.28", 120 + "serde", 121 + "serde_ipld_dagcbor", 122 + "serde_json", 123 + "thiserror", 124 + "tokio", 125 + "tracing", 126 + "urlencoding", 127 + ] 128 + 129 + [[package]] 130 + name = "atproto-jetstream" 131 + version = "0.13.0" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "7b1897fb2f7c6d02d46f7b8d25d653c141cee4a68a10efd135d46201a95034db" 134 + dependencies = [ 135 + "anyhow", 136 + "async-trait", 137 + "atproto-identity", 138 + "futures", 139 + "http 1.4.0", 140 + "serde", 141 + "serde_json", 142 + "thiserror", 143 + "tokio", 144 + "tokio-util", 145 + "tokio-websockets", 146 + "tracing", 147 + "tracing-subscriber", 148 + "urlencoding", 149 + "zstd", 150 + ] 151 + 152 + [[package]] 91 153 name = "autocfg" 92 154 version = "1.5.0" 93 155 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 146 208 ] 147 209 148 210 [[package]] 211 + name = "base-x" 212 + version = "0.2.11" 213 + source = "registry+https://github.com/rust-lang/crates.io-index" 214 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 215 + 216 + [[package]] 217 + name = "base16ct" 218 + version = "0.2.0" 219 + source = "registry+https://github.com/rust-lang/crates.io-index" 220 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 221 + 222 + [[package]] 223 + name = "base256emoji" 224 + version = "1.0.2" 225 + source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 227 + dependencies = [ 228 + "const-str", 229 + "match-lookup", 230 + ] 231 + 232 + [[package]] 149 233 name = "base64" 150 234 version = "0.21.7" 151 235 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 203 287 checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 204 288 205 289 [[package]] 290 + name = "cbor4ii" 291 + version = "0.2.14" 292 + source = "registry+https://github.com/rust-lang/crates.io-index" 293 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 294 + dependencies = [ 295 + "serde", 296 + ] 297 + 298 + [[package]] 206 299 name = "cc" 207 300 version = "1.2.51" 208 301 source = "registry+https://github.com/rust-lang/crates.io-index" 209 302 checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" 210 303 dependencies = [ 211 304 "find-msvc-tools", 305 + "jobserver", 306 + "libc", 212 307 "shlex", 213 308 ] 214 309 ··· 219 314 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 220 315 221 316 [[package]] 317 + name = "cfg_aliases" 318 + version = "0.2.1" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 321 + 322 + [[package]] 222 323 name = "chrono" 223 324 version = "0.4.42" 224 325 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 233 334 ] 234 335 235 336 [[package]] 337 + name = "cid" 338 + version = "0.11.1" 339 + source = "registry+https://github.com/rust-lang/crates.io-index" 340 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 341 + dependencies = [ 342 + "core2", 343 + "multibase", 344 + "multihash", 345 + "serde", 346 + "serde_bytes", 347 + "unsigned-varint", 348 + ] 349 + 350 + [[package]] 236 351 name = "clap" 237 352 version = "4.5.53" 238 353 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 285 400 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 286 401 287 402 [[package]] 403 + name = "const-str" 404 + version = "0.4.3" 405 + source = "registry+https://github.com/rust-lang/crates.io-index" 406 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 407 + 408 + [[package]] 288 409 name = "cookie" 289 410 version = "0.18.1" 290 411 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 306 427 ] 307 428 308 429 [[package]] 430 + name = "core-foundation" 431 + version = "0.10.1" 432 + source = "registry+https://github.com/rust-lang/crates.io-index" 433 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 434 + dependencies = [ 435 + "core-foundation-sys", 436 + "libc", 437 + ] 438 + 439 + [[package]] 309 440 name = "core-foundation-sys" 310 441 version = "0.8.7" 311 442 source = "registry+https://github.com/rust-lang/crates.io-index" 312 443 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 313 444 314 445 [[package]] 446 + name = "core2" 447 + version = "0.4.0" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 450 + dependencies = [ 451 + "memchr", 452 + ] 453 + 454 + [[package]] 315 455 name = "cpufeatures" 316 456 version = "0.2.17" 317 457 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 321 461 ] 322 462 323 463 [[package]] 464 + name = "critical-section" 465 + version = "1.2.0" 466 + source = "registry+https://github.com/rust-lang/crates.io-index" 467 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 468 + 469 + [[package]] 470 + name = "crossbeam-channel" 471 + version = "0.5.15" 472 + source = "registry+https://github.com/rust-lang/crates.io-index" 473 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 474 + dependencies = [ 475 + "crossbeam-utils", 476 + ] 477 + 478 + [[package]] 479 + name = "crossbeam-epoch" 480 + version = "0.9.18" 481 + source = "registry+https://github.com/rust-lang/crates.io-index" 482 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 483 + dependencies = [ 484 + "crossbeam-utils", 485 + ] 486 + 487 + [[package]] 488 + name = "crossbeam-utils" 489 + version = "0.8.21" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 492 + 493 + [[package]] 494 + name = "crypto-bigint" 495 + version = "0.5.5" 496 + source = "registry+https://github.com/rust-lang/crates.io-index" 497 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 498 + dependencies = [ 499 + "generic-array", 500 + "rand_core 0.6.4", 501 + "subtle", 502 + "zeroize", 503 + ] 504 + 505 + [[package]] 324 506 name = "crypto-common" 325 507 version = "0.1.7" 326 508 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 358 540 ] 359 541 360 542 [[package]] 543 + name = "data-encoding" 544 + version = "2.9.0" 545 + source = "registry+https://github.com/rust-lang/crates.io-index" 546 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 547 + 548 + [[package]] 549 + name = "data-encoding-macro" 550 + version = "0.1.18" 551 + source = "registry+https://github.com/rust-lang/crates.io-index" 552 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 553 + dependencies = [ 554 + "data-encoding", 555 + "data-encoding-macro-internal", 556 + ] 557 + 558 + [[package]] 559 + name = "data-encoding-macro-internal" 560 + version = "0.1.16" 561 + source = "registry+https://github.com/rust-lang/crates.io-index" 562 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 563 + dependencies = [ 564 + "data-encoding", 565 + "syn 2.0.111", 566 + ] 567 + 568 + [[package]] 361 569 name = "deadpool" 362 570 version = "0.12.3" 363 571 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 399 607 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 400 608 dependencies = [ 401 609 "const-oid", 610 + "pem-rfc7468", 402 611 "zeroize", 403 612 ] 404 613 ··· 418 627 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 419 628 dependencies = [ 420 629 "block-buffer", 630 + "const-oid", 421 631 "crypto-common", 422 632 "subtle", 423 633 ] ··· 440 650 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 441 651 442 652 [[package]] 653 + name = "ecdsa" 654 + version = "0.16.9" 655 + source = "registry+https://github.com/rust-lang/crates.io-index" 656 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 657 + dependencies = [ 658 + "der", 659 + "digest", 660 + "elliptic-curve", 661 + "rfc6979", 662 + "signature", 663 + "spki", 664 + ] 665 + 666 + [[package]] 443 667 name = "ed25519" 444 668 version = "2.2.3" 445 669 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 465 689 ] 466 690 467 691 [[package]] 692 + name = "elliptic-curve" 693 + version = "0.13.8" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 696 + dependencies = [ 697 + "base16ct", 698 + "base64ct", 699 + "crypto-bigint", 700 + "digest", 701 + "ff", 702 + "generic-array", 703 + "group", 704 + "hkdf", 705 + "pem-rfc7468", 706 + "pkcs8", 707 + "rand_core 0.6.4", 708 + "sec1", 709 + "serde_json", 710 + "serdect", 711 + "subtle", 712 + "zeroize", 713 + ] 714 + 715 + [[package]] 468 716 name = "encoding_rs" 469 717 version = "0.8.35" 470 718 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 474 722 ] 475 723 476 724 [[package]] 725 + name = "enum-as-inner" 726 + version = "0.6.1" 727 + source = "registry+https://github.com/rust-lang/crates.io-index" 728 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 729 + dependencies = [ 730 + "heck", 731 + "proc-macro2", 732 + "quote", 733 + "syn 2.0.111", 734 + ] 735 + 736 + [[package]] 477 737 name = "equivalent" 478 738 version = "1.0.2" 479 739 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 502 762 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 503 763 504 764 [[package]] 765 + name = "ff" 766 + version = "0.13.1" 767 + source = "registry+https://github.com/rust-lang/crates.io-index" 768 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 769 + dependencies = [ 770 + "rand_core 0.6.4", 771 + "subtle", 772 + ] 773 + 774 + [[package]] 505 775 name = "fiat-crypto" 506 776 version = "0.2.9" 507 777 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 518 788 version = "1.0.7" 519 789 source = "registry+https://github.com/rust-lang/crates.io-index" 520 790 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 791 + 792 + [[package]] 793 + name = "foldhash" 794 + version = "0.1.5" 795 + source = "registry+https://github.com/rust-lang/crates.io-index" 796 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 521 797 522 798 [[package]] 523 799 name = "foreign-types" ··· 554 830 ] 555 831 556 832 [[package]] 833 + name = "futures" 834 + version = "0.3.31" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 837 + dependencies = [ 838 + "futures-channel", 839 + "futures-core", 840 + "futures-executor", 841 + "futures-io", 842 + "futures-sink", 843 + "futures-task", 844 + "futures-util", 845 + ] 846 + 847 + [[package]] 557 848 name = "futures-channel" 558 849 version = "0.3.31" 559 850 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 568 859 version = "0.3.31" 569 860 source = "registry+https://github.com/rust-lang/crates.io-index" 570 861 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 862 + 863 + [[package]] 864 + name = "futures-executor" 865 + version = "0.3.31" 866 + source = "registry+https://github.com/rust-lang/crates.io-index" 867 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 868 + dependencies = [ 869 + "futures-core", 870 + "futures-task", 871 + "futures-util", 872 + ] 571 873 572 874 [[package]] 573 875 name = "futures-io" ··· 604 906 source = "registry+https://github.com/rust-lang/crates.io-index" 605 907 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 606 908 dependencies = [ 909 + "futures-channel", 607 910 "futures-core", 608 911 "futures-io", 609 912 "futures-macro", ··· 623 926 dependencies = [ 624 927 "typenum", 625 928 "version_check", 929 + "zeroize", 626 930 ] 627 931 628 932 [[package]] ··· 645 949 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 646 950 dependencies = [ 647 951 "cfg-if", 952 + "js-sys", 648 953 "libc", 649 954 "r-efi", 650 955 "wasip2", 956 + "wasm-bindgen", 957 + ] 958 + 959 + [[package]] 960 + name = "group" 961 + version = "0.13.0" 962 + source = "registry+https://github.com/rust-lang/crates.io-index" 963 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 964 + dependencies = [ 965 + "ff", 966 + "rand_core 0.6.4", 967 + "subtle", 651 968 ] 652 969 653 970 [[package]] ··· 690 1007 691 1008 [[package]] 692 1009 name = "hashbrown" 1010 + version = "0.15.5" 1011 + source = "registry+https://github.com/rust-lang/crates.io-index" 1012 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1013 + dependencies = [ 1014 + "allocator-api2", 1015 + "equivalent", 1016 + "foldhash", 1017 + ] 1018 + 1019 + [[package]] 1020 + name = "hashbrown" 693 1021 version = "0.16.1" 694 1022 source = "registry+https://github.com/rust-lang/crates.io-index" 695 1023 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" ··· 707 1035 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 708 1036 709 1037 [[package]] 1038 + name = "hickory-proto" 1039 + version = "0.25.2" 1040 + source = "registry+https://github.com/rust-lang/crates.io-index" 1041 + checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" 1042 + dependencies = [ 1043 + "async-trait", 1044 + "cfg-if", 1045 + "data-encoding", 1046 + "enum-as-inner", 1047 + "futures-channel", 1048 + "futures-io", 1049 + "futures-util", 1050 + "idna", 1051 + "ipnet", 1052 + "once_cell", 1053 + "rand 0.9.2", 1054 + "ring", 1055 + "thiserror", 1056 + "tinyvec", 1057 + "tokio", 1058 + "tracing", 1059 + "url", 1060 + ] 1061 + 1062 + [[package]] 1063 + name = "hickory-resolver" 1064 + version = "0.25.2" 1065 + source = "registry+https://github.com/rust-lang/crates.io-index" 1066 + checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" 1067 + dependencies = [ 1068 + "cfg-if", 1069 + "futures-util", 1070 + "hickory-proto", 1071 + "ipconfig", 1072 + "moka", 1073 + "once_cell", 1074 + "parking_lot", 1075 + "rand 0.9.2", 1076 + "resolv-conf", 1077 + "smallvec", 1078 + "thiserror", 1079 + "tokio", 1080 + "tracing", 1081 + ] 1082 + 1083 + [[package]] 1084 + name = "hkdf" 1085 + version = "0.12.4" 1086 + source = "registry+https://github.com/rust-lang/crates.io-index" 1087 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1088 + dependencies = [ 1089 + "hmac", 1090 + ] 1091 + 1092 + [[package]] 710 1093 name = "hmac" 711 1094 version = "0.12.1" 712 1095 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 857 1240 "tokio", 858 1241 "tokio-rustls", 859 1242 "tower-service", 1243 + "webpki-roots", 860 1244 ] 861 1245 862 1246 [[package]] ··· 1047 1431 checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 1048 1432 dependencies = [ 1049 1433 "equivalent", 1050 - "hashbrown", 1434 + "hashbrown 0.16.1", 1435 + ] 1436 + 1437 + [[package]] 1438 + name = "ipconfig" 1439 + version = "0.3.2" 1440 + source = "registry+https://github.com/rust-lang/crates.io-index" 1441 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1442 + dependencies = [ 1443 + "socket2 0.5.10", 1444 + "widestring", 1445 + "windows-sys 0.48.0", 1446 + "winreg", 1447 + ] 1448 + 1449 + [[package]] 1450 + name = "ipld-core" 1451 + version = "0.4.2" 1452 + source = "registry+https://github.com/rust-lang/crates.io-index" 1453 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 1454 + dependencies = [ 1455 + "cid", 1456 + "serde", 1457 + "serde_bytes", 1051 1458 ] 1052 1459 1053 1460 [[package]] ··· 1079 1486 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1080 1487 1081 1488 [[package]] 1489 + name = "jobserver" 1490 + version = "0.1.34" 1491 + source = "registry+https://github.com/rust-lang/crates.io-index" 1492 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1493 + dependencies = [ 1494 + "getrandom 0.3.4", 1495 + "libc", 1496 + ] 1497 + 1498 + [[package]] 1082 1499 name = "js-sys" 1083 1500 version = "0.3.83" 1084 1501 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1089 1506 ] 1090 1507 1091 1508 [[package]] 1509 + name = "k256" 1510 + version = "0.13.4" 1511 + source = "registry+https://github.com/rust-lang/crates.io-index" 1512 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 1513 + dependencies = [ 1514 + "cfg-if", 1515 + "ecdsa", 1516 + "elliptic-curve", 1517 + "once_cell", 1518 + "sha2", 1519 + "signature", 1520 + ] 1521 + 1522 + [[package]] 1092 1523 name = "lazy_static" 1093 1524 version = "1.5.0" 1094 1525 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1137 1568 version = "0.4.29" 1138 1569 source = "registry+https://github.com/rust-lang/crates.io-index" 1139 1570 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 1571 + 1572 + [[package]] 1573 + name = "lru" 1574 + version = "0.12.5" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 1577 + dependencies = [ 1578 + "hashbrown 0.15.5", 1579 + ] 1580 + 1581 + [[package]] 1582 + name = "lru-slab" 1583 + version = "0.1.2" 1584 + source = "registry+https://github.com/rust-lang/crates.io-index" 1585 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1140 1586 1141 1587 [[package]] 1142 1588 name = "mac" ··· 1162 1608 dependencies = [ 1163 1609 "serde", 1164 1610 "serde_json", 1165 - "thiserror 2.0.17", 1611 + "thiserror", 1166 1612 ] 1167 1613 1168 1614 [[package]] 1169 1615 name = "malfestio-server" 1170 1616 version = "0.1.0" 1171 1617 dependencies = [ 1618 + "anyhow", 1172 1619 "async-trait", 1620 + "atproto-jetstream", 1173 1621 "axum", 1174 1622 "base64 0.22.1", 1175 1623 "chrono", ··· 1187 1635 "sha2", 1188 1636 "tokio", 1189 1637 "tokio-postgres", 1638 + "tokio-util", 1190 1639 "tower", 1191 1640 "tower-cookies", 1192 1641 "tower-http", ··· 1223 1672 ] 1224 1673 1225 1674 [[package]] 1675 + name = "match-lookup" 1676 + version = "0.1.1" 1677 + source = "registry+https://github.com/rust-lang/crates.io-index" 1678 + checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" 1679 + dependencies = [ 1680 + "proc-macro2", 1681 + "quote", 1682 + "syn 1.0.109", 1683 + ] 1684 + 1685 + [[package]] 1226 1686 name = "matchers" 1227 1687 version = "0.2.0" 1228 1688 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1271 1731 ] 1272 1732 1273 1733 [[package]] 1734 + name = "moka" 1735 + version = "0.12.12" 1736 + source = "registry+https://github.com/rust-lang/crates.io-index" 1737 + checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" 1738 + dependencies = [ 1739 + "crossbeam-channel", 1740 + "crossbeam-epoch", 1741 + "crossbeam-utils", 1742 + "equivalent", 1743 + "parking_lot", 1744 + "portable-atomic", 1745 + "smallvec", 1746 + "tagptr", 1747 + "uuid", 1748 + ] 1749 + 1750 + [[package]] 1751 + name = "multibase" 1752 + version = "0.9.2" 1753 + source = "registry+https://github.com/rust-lang/crates.io-index" 1754 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1755 + dependencies = [ 1756 + "base-x", 1757 + "base256emoji", 1758 + "data-encoding", 1759 + "data-encoding-macro", 1760 + ] 1761 + 1762 + [[package]] 1763 + name = "multihash" 1764 + version = "0.19.3" 1765 + source = "registry+https://github.com/rust-lang/crates.io-index" 1766 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1767 + dependencies = [ 1768 + "core2", 1769 + "serde", 1770 + "unsigned-varint", 1771 + ] 1772 + 1773 + [[package]] 1274 1774 name = "native-tls" 1275 1775 version = "0.2.14" 1276 1776 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1282 1782 "openssl-probe", 1283 1783 "openssl-sys", 1284 1784 "schannel", 1285 - "security-framework", 1785 + "security-framework 2.11.1", 1286 1786 "security-framework-sys", 1287 1787 "tempfile", 1288 1788 ] ··· 1332 1832 version = "1.21.3" 1333 1833 source = "registry+https://github.com/rust-lang/crates.io-index" 1334 1834 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1835 + dependencies = [ 1836 + "critical-section", 1837 + "portable-atomic", 1838 + ] 1335 1839 1336 1840 [[package]] 1337 1841 name = "once_cell_polyfill" ··· 1384 1888 ] 1385 1889 1386 1890 [[package]] 1891 + name = "p256" 1892 + version = "0.13.2" 1893 + source = "registry+https://github.com/rust-lang/crates.io-index" 1894 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 1895 + dependencies = [ 1896 + "ecdsa", 1897 + "elliptic-curve", 1898 + "primeorder", 1899 + "sha2", 1900 + ] 1901 + 1902 + [[package]] 1903 + name = "p384" 1904 + version = "0.13.1" 1905 + source = "registry+https://github.com/rust-lang/crates.io-index" 1906 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 1907 + dependencies = [ 1908 + "ecdsa", 1909 + "elliptic-curve", 1910 + "primeorder", 1911 + "sha2", 1912 + ] 1913 + 1914 + [[package]] 1387 1915 name = "parking_lot" 1388 1916 version = "0.12.5" 1389 1917 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1404 1932 "redox_syscall 0.5.18", 1405 1933 "smallvec", 1406 1934 "windows-link", 1935 + ] 1936 + 1937 + [[package]] 1938 + name = "pem-rfc7468" 1939 + version = "0.7.0" 1940 + source = "registry+https://github.com/rust-lang/crates.io-index" 1941 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1942 + dependencies = [ 1943 + "base64ct", 1407 1944 ] 1408 1945 1409 1946 [[package]] ··· 1517 2054 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1518 2055 1519 2056 [[package]] 2057 + name = "portable-atomic" 2058 + version = "1.13.0" 2059 + source = "registry+https://github.com/rust-lang/crates.io-index" 2060 + checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" 2061 + 2062 + [[package]] 1520 2063 name = "postgres-protocol" 1521 2064 version = "0.6.9" 1522 2065 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1580 2123 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1581 2124 1582 2125 [[package]] 2126 + name = "primeorder" 2127 + version = "0.13.6" 2128 + source = "registry+https://github.com/rust-lang/crates.io-index" 2129 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2130 + dependencies = [ 2131 + "elliptic-curve", 2132 + ] 2133 + 2134 + [[package]] 1583 2135 name = "proc-macro2" 1584 2136 version = "1.0.104" 1585 2137 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1589 2141 ] 1590 2142 1591 2143 [[package]] 2144 + name = "quinn" 2145 + version = "0.11.9" 2146 + source = "registry+https://github.com/rust-lang/crates.io-index" 2147 + checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" 2148 + dependencies = [ 2149 + "bytes", 2150 + "cfg_aliases", 2151 + "pin-project-lite", 2152 + "quinn-proto", 2153 + "quinn-udp", 2154 + "rustc-hash", 2155 + "rustls", 2156 + "socket2 0.6.1", 2157 + "thiserror", 2158 + "tokio", 2159 + "tracing", 2160 + "web-time", 2161 + ] 2162 + 2163 + [[package]] 2164 + name = "quinn-proto" 2165 + version = "0.11.13" 2166 + source = "registry+https://github.com/rust-lang/crates.io-index" 2167 + checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 2168 + dependencies = [ 2169 + "bytes", 2170 + "getrandom 0.3.4", 2171 + "lru-slab", 2172 + "rand 0.9.2", 2173 + "ring", 2174 + "rustc-hash", 2175 + "rustls", 2176 + "rustls-pki-types", 2177 + "slab", 2178 + "thiserror", 2179 + "tinyvec", 2180 + "tracing", 2181 + "web-time", 2182 + ] 2183 + 2184 + [[package]] 2185 + name = "quinn-udp" 2186 + version = "0.5.14" 2187 + source = "registry+https://github.com/rust-lang/crates.io-index" 2188 + checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" 2189 + dependencies = [ 2190 + "cfg_aliases", 2191 + "libc", 2192 + "once_cell", 2193 + "socket2 0.6.1", 2194 + "tracing", 2195 + "windows-sys 0.60.2", 2196 + ] 2197 + 2198 + [[package]] 1592 2199 name = "quote" 1593 2200 version = "1.0.42" 1594 2201 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1787 2394 "native-tls", 1788 2395 "percent-encoding", 1789 2396 "pin-project-lite", 2397 + "quinn", 2398 + "rustls", 1790 2399 "rustls-pki-types", 1791 2400 "serde", 1792 2401 "serde_json", ··· 1794 2403 "sync_wrapper 1.0.2", 1795 2404 "tokio", 1796 2405 "tokio-native-tls", 2406 + "tokio-rustls", 1797 2407 "tower", 1798 2408 "tower-http", 1799 2409 "tower-service", ··· 1801 2411 "wasm-bindgen", 1802 2412 "wasm-bindgen-futures", 1803 2413 "web-sys", 2414 + "webpki-roots", 2415 + ] 2416 + 2417 + [[package]] 2418 + name = "resolv-conf" 2419 + version = "0.7.6" 2420 + source = "registry+https://github.com/rust-lang/crates.io-index" 2421 + checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" 2422 + 2423 + [[package]] 2424 + name = "rfc6979" 2425 + version = "0.4.0" 2426 + source = "registry+https://github.com/rust-lang/crates.io-index" 2427 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 2428 + dependencies = [ 2429 + "hmac", 2430 + "subtle", 1804 2431 ] 1805 2432 1806 2433 [[package]] ··· 1816 2443 "untrusted", 1817 2444 "windows-sys 0.52.0", 1818 2445 ] 2446 + 2447 + [[package]] 2448 + name = "rustc-hash" 2449 + version = "2.1.1" 2450 + source = "registry+https://github.com/rust-lang/crates.io-index" 2451 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1819 2452 1820 2453 [[package]] 1821 2454 name = "rustc_version" ··· 1846 2479 checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 1847 2480 dependencies = [ 1848 2481 "once_cell", 2482 + "ring", 1849 2483 "rustls-pki-types", 1850 2484 "rustls-webpki", 1851 2485 "subtle", ··· 1853 2487 ] 1854 2488 1855 2489 [[package]] 2490 + name = "rustls-native-certs" 2491 + version = "0.8.2" 2492 + source = "registry+https://github.com/rust-lang/crates.io-index" 2493 + checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" 2494 + dependencies = [ 2495 + "openssl-probe", 2496 + "rustls-pki-types", 2497 + "schannel", 2498 + "security-framework 3.5.1", 2499 + ] 2500 + 2501 + [[package]] 1856 2502 name = "rustls-pemfile" 1857 2503 version = "1.0.4" 1858 2504 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1867 2513 source = "registry+https://github.com/rust-lang/crates.io-index" 1868 2514 checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" 1869 2515 dependencies = [ 2516 + "web-time", 1870 2517 "zeroize", 1871 2518 ] 1872 2519 ··· 1909 2556 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1910 2557 1911 2558 [[package]] 2559 + name = "sec1" 2560 + version = "0.7.3" 2561 + source = "registry+https://github.com/rust-lang/crates.io-index" 2562 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2563 + dependencies = [ 2564 + "base16ct", 2565 + "der", 2566 + "generic-array", 2567 + "pkcs8", 2568 + "serdect", 2569 + "subtle", 2570 + "zeroize", 2571 + ] 2572 + 2573 + [[package]] 1912 2574 name = "security-framework" 1913 2575 version = "2.11.1" 1914 2576 source = "registry+https://github.com/rust-lang/crates.io-index" 1915 2577 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1916 2578 dependencies = [ 1917 2579 "bitflags 2.10.0", 1918 - "core-foundation", 2580 + "core-foundation 0.9.4", 2581 + "core-foundation-sys", 2582 + "libc", 2583 + "security-framework-sys", 2584 + ] 2585 + 2586 + [[package]] 2587 + name = "security-framework" 2588 + version = "3.5.1" 2589 + source = "registry+https://github.com/rust-lang/crates.io-index" 2590 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 2591 + dependencies = [ 2592 + "bitflags 2.10.0", 2593 + "core-foundation 0.10.1", 1919 2594 "core-foundation-sys", 1920 2595 "libc", 1921 2596 "security-framework-sys", ··· 1948 2623 ] 1949 2624 1950 2625 [[package]] 2626 + name = "serde_bytes" 2627 + version = "0.11.19" 2628 + source = "registry+https://github.com/rust-lang/crates.io-index" 2629 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2630 + dependencies = [ 2631 + "serde", 2632 + "serde_core", 2633 + ] 2634 + 2635 + [[package]] 1951 2636 name = "serde_core" 1952 2637 version = "1.0.228" 1953 2638 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1968 2653 ] 1969 2654 1970 2655 [[package]] 2656 + name = "serde_ipld_dagcbor" 2657 + version = "0.6.4" 2658 + source = "registry+https://github.com/rust-lang/crates.io-index" 2659 + checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" 2660 + dependencies = [ 2661 + "cbor4ii", 2662 + "ipld-core", 2663 + "scopeguard", 2664 + "serde", 2665 + ] 2666 + 2667 + [[package]] 1971 2668 name = "serde_json" 1972 2669 version = "1.0.148" 1973 2670 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1993 2690 1994 2691 [[package]] 1995 2692 name = "serde_qs" 1996 - version = "0.13.0" 2693 + version = "0.15.0" 1997 2694 source = "registry+https://github.com/rust-lang/crates.io-index" 1998 - checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" 2695 + checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" 1999 2696 dependencies = [ 2000 2697 "percent-encoding", 2001 2698 "serde", 2002 - "thiserror 1.0.69", 2699 + "thiserror", 2003 2700 ] 2004 2701 2005 2702 [[package]] ··· 2015 2712 ] 2016 2713 2017 2714 [[package]] 2715 + name = "serdect" 2716 + version = "0.2.0" 2717 + source = "registry+https://github.com/rust-lang/crates.io-index" 2718 + checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" 2719 + dependencies = [ 2720 + "base16ct", 2721 + "serde", 2722 + ] 2723 + 2724 + [[package]] 2018 2725 name = "sha2" 2019 2726 version = "0.10.9" 2020 2727 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2056 2763 source = "registry+https://github.com/rust-lang/crates.io-index" 2057 2764 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 2058 2765 dependencies = [ 2766 + "digest", 2059 2767 "rand_core 0.6.4", 2060 2768 ] 2769 + 2770 + [[package]] 2771 + name = "simdutf8" 2772 + version = "0.1.5" 2773 + source = "registry+https://github.com/rust-lang/crates.io-index" 2774 + checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 2061 2775 2062 2776 [[package]] 2063 2777 name = "siphasher" ··· 2222 2936 checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 2223 2937 dependencies = [ 2224 2938 "bitflags 1.3.2", 2225 - "core-foundation", 2939 + "core-foundation 0.9.4", 2226 2940 "system-configuration-sys 0.5.0", 2227 2941 ] 2228 2942 ··· 2233 2947 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2234 2948 dependencies = [ 2235 2949 "bitflags 2.10.0", 2236 - "core-foundation", 2950 + "core-foundation 0.9.4", 2237 2951 "system-configuration-sys 0.6.0", 2238 2952 ] 2239 2953 ··· 2258 2972 ] 2259 2973 2260 2974 [[package]] 2975 + name = "tagptr" 2976 + version = "0.2.0" 2977 + source = "registry+https://github.com/rust-lang/crates.io-index" 2978 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 2979 + 2980 + [[package]] 2261 2981 name = "tempfile" 2262 2982 version = "3.24.0" 2263 2983 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2283 3003 2284 3004 [[package]] 2285 3005 name = "thiserror" 2286 - version = "1.0.69" 2287 - source = "registry+https://github.com/rust-lang/crates.io-index" 2288 - checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2289 - dependencies = [ 2290 - "thiserror-impl 1.0.69", 2291 - ] 2292 - 2293 - [[package]] 2294 - name = "thiserror" 2295 3006 version = "2.0.17" 2296 3007 source = "registry+https://github.com/rust-lang/crates.io-index" 2297 3008 checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 2298 3009 dependencies = [ 2299 - "thiserror-impl 2.0.17", 2300 - ] 2301 - 2302 - [[package]] 2303 - name = "thiserror-impl" 2304 - version = "1.0.69" 2305 - source = "registry+https://github.com/rust-lang/crates.io-index" 2306 - checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2307 - dependencies = [ 2308 - "proc-macro2", 2309 - "quote", 2310 - "syn 2.0.111", 3010 + "thiserror-impl", 2311 3011 ] 2312 3012 2313 3013 [[package]] ··· 2469 3169 "bytes", 2470 3170 "futures-core", 2471 3171 "futures-sink", 3172 + "futures-util", 2472 3173 "pin-project-lite", 2473 3174 "tokio", 2474 3175 ] 2475 3176 2476 3177 [[package]] 3178 + name = "tokio-websockets" 3179 + version = "0.11.4" 3180 + source = "registry+https://github.com/rust-lang/crates.io-index" 3181 + checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" 3182 + dependencies = [ 3183 + "base64 0.22.1", 3184 + "bytes", 3185 + "futures-core", 3186 + "futures-sink", 3187 + "http 1.4.0", 3188 + "httparse", 3189 + "rand 0.9.2", 3190 + "ring", 3191 + "rustls-native-certs", 3192 + "rustls-pki-types", 3193 + "simdutf8", 3194 + "tokio", 3195 + "tokio-rustls", 3196 + "tokio-util", 3197 + ] 3198 + 3199 + [[package]] 2477 3200 name = "tower" 2478 3201 version = "0.5.2" 2479 3202 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2638 3361 checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 2639 3362 2640 3363 [[package]] 3364 + name = "unsigned-varint" 3365 + version = "0.8.0" 3366 + source = "registry+https://github.com/rust-lang/crates.io-index" 3367 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 3368 + 3369 + [[package]] 2641 3370 name = "untrusted" 2642 3371 version = "0.9.0" 2643 3372 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2808 3537 ] 2809 3538 2810 3539 [[package]] 3540 + name = "web-time" 3541 + version = "1.1.0" 3542 + source = "registry+https://github.com/rust-lang/crates.io-index" 3543 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 3544 + dependencies = [ 3545 + "js-sys", 3546 + "wasm-bindgen", 3547 + ] 3548 + 3549 + [[package]] 3550 + name = "webpki-roots" 3551 + version = "1.0.4" 3552 + source = "registry+https://github.com/rust-lang/crates.io-index" 3553 + checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" 3554 + dependencies = [ 3555 + "rustls-pki-types", 3556 + ] 3557 + 3558 + [[package]] 2811 3559 name = "whoami" 2812 3560 version = "1.6.1" 2813 3561 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2817 3565 "wasite", 2818 3566 "web-sys", 2819 3567 ] 3568 + 3569 + [[package]] 3570 + name = "widestring" 3571 + version = "1.2.1" 3572 + source = "registry+https://github.com/rust-lang/crates.io-index" 3573 + checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" 2820 3574 2821 3575 [[package]] 2822 3576 name = "windows-core" ··· 3251 4005 version = "1.0.0" 3252 4006 source = "registry+https://github.com/rust-lang/crates.io-index" 3253 4007 checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" 4008 + 4009 + [[package]] 4010 + name = "zstd" 4011 + version = "0.13.3" 4012 + source = "registry+https://github.com/rust-lang/crates.io-index" 4013 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 4014 + dependencies = [ 4015 + "zstd-safe", 4016 + ] 4017 + 4018 + [[package]] 4019 + name = "zstd-safe" 4020 + version = "7.2.4" 4021 + source = "registry+https://github.com/rust-lang/crates.io-index" 4022 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 4023 + dependencies = [ 4024 + "zstd-sys", 4025 + ] 4026 + 4027 + [[package]] 4028 + name = "zstd-sys" 4029 + version = "2.0.16+zstd.1.5.7" 4030 + source = "registry+https://github.com/rust-lang/crates.io-index" 4031 + checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 4032 + dependencies = [ 4033 + "cc", 4034 + "pkg-config", 4035 + ]
+4 -1
crates/server/Cargo.toml
··· 4 4 edition = "2024" 5 5 6 6 [dependencies] 7 + anyhow = "1.0" 7 8 async-trait = "0.1.83" 9 + atproto-jetstream = "0.13" 8 10 axum = "0.8.8" 9 11 base64 = "0.22" 10 12 chrono = { version = "0.4.42", features = ["serde"] } ··· 18 20 reqwest = { version = "0.12.28", features = ["json"] } 19 21 serde = "1.0.228" 20 22 serde_json = "1.0.148" 21 - serde_qs = "0.13" 23 + serde_qs = "0.15" 22 24 sha2 = "0.10" 23 25 tokio = { version = "1.48.0", features = ["full"] } 26 + tokio-util = { version = "0.7", features = ["rt"] } 24 27 urlencoding = "2.1" 25 28 tokio-postgres = { version = "0.7.13", features = [ 26 29 "with-serde_json-1",
+8 -4
crates/server/src/api/card.rs
··· 87 87 use std::sync::Arc; 88 88 89 89 fn create_test_state() -> SharedState { 90 - let pool = crate::db::create_pool().unwrap_or_else(|_| panic!("For testing without DB, use mock pool")); 90 + let pool = crate::db::create_mock_pool(); 91 91 let card_repo = Arc::new(MockCardRepository::new()) as Arc<dyn crate::repository::card::CardRepository>; 92 92 let note_repo = Arc::new(crate::repository::note::mock::MockNoteRepository::new()) 93 93 as Arc<dyn crate::repository::note::NoteRepository>; 94 - AppState::new_with_repos(pool, card_repo, note_repo) 94 + let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 95 + as Arc<dyn crate::repository::oauth::OAuthRepository>; 96 + AppState::new_with_repos(pool, card_repo, note_repo, oauth_repo) 95 97 } 96 98 97 99 #[tokio::test] ··· 133 135 134 136 #[tokio::test] 135 137 async fn test_list_cards_success() { 136 - let pool = crate::db::create_pool().unwrap_or_else(|_| panic!("For testing without DB, use mock pool")); 138 + let pool = crate::db::create_mock_pool(); 137 139 138 140 let test_deck_id = "550e8400-e29b-41d4-a716-446655440000".to_string(); 139 141 let test_cards = vec![ ··· 159 161 Arc::new(MockCardRepository::with_cards(test_cards)) as Arc<dyn crate::repository::card::CardRepository>; 160 162 let note_repo = Arc::new(crate::repository::note::mock::MockNoteRepository::new()) 161 163 as Arc<dyn crate::repository::note::NoteRepository>; 164 + let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 165 + as Arc<dyn crate::repository::oauth::OAuthRepository>; 162 166 163 - let state = AppState::new_with_repos(pool, card_repo, note_repo); 167 + let state = AppState::new_with_repos(pool, card_repo, note_repo, oauth_repo); 164 168 165 169 let response = list_cards(axum::extract::State(state), None, Path(test_deck_id)) 166 170 .await
+132 -53
crates/server/src/api/deck.rs
··· 294 294 }; 295 295 296 296 let deck_row = match client 297 - .query_opt("SELECT owner_did FROM decks WHERE id = $1", &[&deck_id]) 297 + .query_opt( 298 + "SELECT id, owner_did, title, description, tags, visibility, published_at, fork_of 299 + FROM decks WHERE id = $1", 300 + &[&deck_id], 301 + ) 298 302 .await 299 303 { 300 304 Ok(Some(row)) => row, ··· 314 318 return (StatusCode::FORBIDDEN, Json(json!({"error": "Only owner can publish"}))).into_response(); 315 319 } 316 320 317 - let (new_visibility, published_at) = if payload.published { 318 - ( 319 - serde_json::to_value(&Visibility::Public).unwrap(), 320 - Some(chrono::Utc::now()), 321 - ) 322 - } else { 323 - (serde_json::to_value(&Visibility::Private).unwrap(), None) 324 - }; 325 - 326 - match client 327 - .execute( 328 - "UPDATE decks SET visibility = $1, published_at = $2 WHERE id = $3", 329 - &[&new_visibility, &published_at, &deck_id], 330 - ) 331 - .await 332 - { 333 - Ok(_) => (), 321 + let visibility_json: serde_json::Value = deck_row.get("visibility"); 322 + let visibility: Visibility = match serde_json::from_value(visibility_json) { 323 + Ok(v) => v, 334 324 Err(e) => { 335 - tracing::error!("Failed to update deck: {}", e); 325 + tracing::error!("Failed to parse visibility: {}", e); 336 326 return ( 337 327 StatusCode::INTERNAL_SERVER_ERROR, 338 - Json(json!({"error": "Failed to update deck"})), 328 + Json(json!({"error": "Invalid deck data"})), 339 329 ) 340 330 .into_response(); 341 331 } 342 - } 332 + }; 333 + 334 + let fork_of: Option<uuid::Uuid> = deck_row.get("fork_of"); 335 + let mut deck = Deck { 336 + id: deck_id.to_string(), 337 + owner_did: owner_did.clone(), 338 + title: deck_row.get("title"), 339 + description: deck_row.get("description"), 340 + tags: deck_row.get("tags"), 341 + visibility: visibility.clone(), 342 + published_at: deck_row 343 + .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 344 + .map(|dt| dt.to_rfc3339()), 345 + fork_of: fork_of.map(|u| u.to_string()), 346 + }; 347 + 348 + let mut deck_at_uri: Option<String> = None; 349 + 350 + if payload.published { 351 + let card_rows = match client 352 + .query( 353 + "SELECT id, owner_did, deck_id, front, back, media_url 354 + FROM cards WHERE deck_id = $1 ORDER BY created_at ASC", 355 + &[&deck_id], 356 + ) 357 + .await 358 + { 359 + Ok(rows) => rows, 360 + Err(e) => { 361 + tracing::error!("Failed to fetch cards: {}", e); 362 + return ( 363 + StatusCode::INTERNAL_SERVER_ERROR, 364 + Json(json!({"error": "Failed to fetch cards"})), 365 + ) 366 + .into_response(); 367 + } 368 + }; 369 + 370 + let cards: Vec<malfestio_core::model::Card> = card_rows 371 + .iter() 372 + .map(|row| { 373 + let card_id: uuid::Uuid = row.get("id"); 374 + let card_deck_id: uuid::Uuid = row.get("deck_id"); 375 + malfestio_core::model::Card { 376 + id: card_id.to_string(), 377 + owner_did: row.get("owner_did"), 378 + deck_id: card_deck_id.to_string(), 379 + front: row.get("front"), 380 + back: row.get("back"), 381 + media_url: row.get("media_url"), 382 + } 383 + }) 384 + .collect(); 385 + 386 + match crate::pds::publish::publish_deck_to_pds(state.oauth_repo.clone(), &user.did, &deck, &cards).await { 387 + Ok(result) => { 388 + deck_at_uri = Some(result.deck_at_uri.clone()); 389 + 390 + if let Err(e) = client 391 + .execute( 392 + "UPDATE decks SET at_uri = $1, visibility = $2, published_at = $3 WHERE id = $4", 393 + &[ 394 + &result.deck_at_uri, 395 + &serde_json::to_value(&Visibility::Public).unwrap(), 396 + &Some(chrono::Utc::now()), 397 + &deck_id, 398 + ], 399 + ) 400 + .await 401 + { 402 + tracing::error!("Failed to store deck AT-URI: {}", e); 403 + } 343 404 344 - let row = match client 345 - .query_one( 346 - "SELECT id, owner_did, title, description, tags, visibility, published_at, fork_of 347 - FROM decks WHERE id = $1", 348 - &[&deck_id], 349 - ) 350 - .await 351 - { 352 - Ok(row) => row, 353 - Err(e) => { 354 - tracing::error!("Failed to fetch updated deck: {}", e); 405 + for (i, at_uri) in result.card_at_uris.iter().enumerate() { 406 + if i < cards.len() 407 + && let Ok(card_uuid) = uuid::Uuid::parse_str(&cards[i].id) 408 + && let Err(e) = client 409 + .execute("UPDATE cards SET at_uri = $1 WHERE id = $2", &[at_uri, &card_uuid]) 410 + .await 411 + { 412 + tracing::warn!("Failed to store card AT-URI: {}", e); 413 + } 414 + } 415 + 416 + deck.visibility = Visibility::Public; 417 + deck.published_at = Some(chrono::Utc::now().to_rfc3339()); 418 + } 419 + Err(e) => { 420 + tracing::error!("Failed to publish to PDS: {}", e); 421 + return ( 422 + StatusCode::SERVICE_UNAVAILABLE, 423 + Json(json!({"error": format!("Failed to publish to PDS: {}", e)})), 424 + ) 425 + .into_response(); 426 + } 427 + } 428 + } else { 429 + // Unpublish - just update local visibility 430 + let (new_visibility, published_at) = ( 431 + serde_json::to_value(&Visibility::Private).unwrap(), 432 + None::<chrono::DateTime<chrono::Utc>>, 433 + ); 434 + if let Err(e) = client 435 + .execute( 436 + "UPDATE decks SET visibility = $1, published_at = $2 WHERE id = $3", 437 + &[&new_visibility, &published_at, &deck_id], 438 + ) 439 + .await 440 + { 441 + tracing::error!("Failed to update deck: {}", e); 355 442 return ( 356 443 StatusCode::INTERNAL_SERVER_ERROR, 357 - Json(json!({"error": "Failed to retrieve updated deck"})), 444 + Json(json!({"error": "Failed to update deck"})), 358 445 ) 359 446 .into_response(); 360 447 } 361 - }; 362 - 363 - let visibility_json: serde_json::Value = row.get("visibility"); 364 - let visibility: Visibility = serde_json::from_value(visibility_json).unwrap(); 365 - let uuid_id: uuid::Uuid = row.get("id"); 366 - let fork_of: Option<uuid::Uuid> = row.get("fork_of"); 367 - 368 - let deck = Deck { 369 - id: uuid_id.to_string(), 370 - owner_did: row.get("owner_did"), 371 - title: row.get("title"), 372 - description: row.get("description"), 373 - tags: row.get("tags"), 374 - visibility, 375 - published_at: row 376 - .get::<_, Option<chrono::DateTime<chrono::Utc>>>("published_at") 377 - .map(|dt| dt.to_rfc3339()), 378 - fork_of: fork_of.map(|u| u.to_string()), 379 - }; 448 + deck.visibility = Visibility::Private; 449 + deck.published_at = None; 450 + } 380 451 381 - Json(deck).into_response() 452 + if let Some(at_uri) = deck_at_uri { 453 + Json(json!({ 454 + "deck": deck, 455 + "at_uri": at_uri 456 + })) 457 + .into_response() 458 + } else { 459 + Json(deck).into_response() 460 + } 382 461 }
+12 -6
crates/server/src/api/note.rs
··· 140 140 use std::sync::Arc; 141 141 142 142 fn create_test_state() -> SharedState { 143 - let pool = crate::db::create_pool().unwrap_or_else(|_| panic!("For testing without DB, use mock pool")); 143 + let pool = crate::db::create_mock_pool(); 144 144 let card_repo = Arc::new(crate::repository::card::mock::MockCardRepository::new()) 145 145 as Arc<dyn crate::repository::card::CardRepository>; 146 146 let note_repo = Arc::new(MockNoteRepository::new()) as Arc<dyn crate::repository::note::NoteRepository>; 147 - AppState::new_with_repos(pool, card_repo, note_repo) 147 + let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 148 + as Arc<dyn crate::repository::oauth::OAuthRepository>; 149 + AppState::new_with_repos(pool, card_repo, note_repo, oauth_repo) 148 150 } 149 151 150 152 #[tokio::test] ··· 186 188 187 189 #[tokio::test] 188 190 async fn test_list_notes_with_visibility_filtering() { 189 - let pool = crate::db::create_pool().unwrap_or_else(|_| panic!("For testing without DB, use mock pool")); 191 + let pool = crate::db::create_mock_pool(); 190 192 191 193 let test_notes = vec![ 192 194 Note { ··· 215 217 Arc::new(MockNoteRepository::with_notes(test_notes)) as Arc<dyn crate::repository::note::NoteRepository>; 216 218 let card_repo = Arc::new(crate::repository::card::mock::MockCardRepository::new()) 217 219 as Arc<dyn crate::repository::card::CardRepository>; 220 + let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 221 + as Arc<dyn crate::repository::oauth::OAuthRepository>; 218 222 219 - let state = AppState::new_with_repos(pool, card_repo, note_repo); 223 + let state = AppState::new_with_repos(pool, card_repo, note_repo, oauth_repo); 220 224 221 225 let response = list_notes(axum::extract::State(state.clone()), None) 222 226 .await ··· 227 231 228 232 #[tokio::test] 229 233 async fn test_get_note_access_control() { 230 - let pool = crate::db::create_pool().unwrap_or_else(|_| panic!("For testing without DB, use mock pool")); 234 + let pool = crate::db::create_mock_pool(); 231 235 232 236 let note_id = "test-note-id".to_string(); 233 237 let test_notes = vec![Note { ··· 245 249 Arc::new(MockNoteRepository::with_notes(test_notes)) as Arc<dyn crate::repository::note::NoteRepository>; 246 250 let card_repo = Arc::new(crate::repository::card::mock::MockCardRepository::new()) 247 251 as Arc<dyn crate::repository::card::CardRepository>; 252 + let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 253 + as Arc<dyn crate::repository::oauth::OAuthRepository>; 248 254 249 - let state = AppState::new_with_repos(pool, card_repo, note_repo); 255 + let state = AppState::new_with_repos(pool, card_repo, note_repo, oauth_repo); 250 256 251 257 let owner = UserContext { did: "did:plc:owner".to_string(), handle: "owner.handle".to_string() }; 252 258
+11
crates/server/src/db.rs
··· 27 27 Ok(Pool::builder(mgr).max_size(16).build()?) 28 28 } 29 29 30 + /// Create a mock pool for testing that won't actually connect 31 + #[cfg(test)] 32 + pub fn create_mock_pool() -> DbPool { 33 + let config = "host=localhost user=test dbname=test" 34 + .parse::<tokio_postgres::Config>() 35 + .unwrap(); 36 + let mgr_config = ManagerConfig { recycling_method: RecyclingMethod::Fast }; 37 + let mgr = Manager::from_config(config, NoTls, mgr_config); 38 + Pool::builder(mgr).max_size(1).build().unwrap() 39 + } 40 + 30 41 /// Retry wrapper for getting database connections with exponential backoff 31 42 pub async fn get_connection_with_retry( 32 43 pool: &DbPool, max_retries: u32,
+196
crates/server/src/firehose.rs
··· 1 + //! Firehose consumption via AT Protocol Jetstream. 2 + //! 3 + //! Provides WebSocket subscription to Jetstream for indexing public records. 4 + //! Filters for `app.malfestio.*` collections and indexes them locally. 5 + 6 + use crate::db::DbPool; 7 + use async_trait::async_trait; 8 + use atproto_jetstream::{Consumer, ConsumerTaskConfig, EventHandler, JetstreamEvent}; 9 + use tokio_util::sync::CancellationToken; 10 + 11 + /// Default Jetstream endpoint (Bluesky's public instance). 12 + pub const DEFAULT_JETSTREAM_URL: &str = "wss://jetstream2.us-west.bsky.network/subscribe"; 13 + 14 + /// Collections we're interested in indexing. 15 + pub const MALFESTIO_COLLECTIONS: &[&str] = &["app.malfestio.deck", "app.malfestio.card", "app.malfestio.note"]; 16 + 17 + /// Event handler for Malfestio records from Jetstream. 18 + pub struct MalfestioEventHandler { 19 + pool: DbPool, 20 + handler_id: String, 21 + } 22 + 23 + impl MalfestioEventHandler { 24 + /// Create a new event handler with database connection. 25 + pub fn new(pool: DbPool) -> Self { 26 + Self { pool, handler_id: "malfestio-indexer".to_string() } 27 + } 28 + 29 + /// Index a record into the database. 30 + async fn index_record( 31 + &self, did: &str, collection: &str, rkey: &str, 32 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 33 + let at_uri = format!("at://{}/{}/{}", did, collection, rkey); 34 + 35 + let client = self.pool.get().await?; 36 + 37 + // Upsert into indexed_records table 38 + client 39 + .execute( 40 + "INSERT INTO indexed_records (at_uri, did, collection, rkey, indexed_at) 41 + VALUES ($1, $2, $3, $4, NOW()) 42 + ON CONFLICT (at_uri) DO UPDATE SET indexed_at = NOW()", 43 + &[&at_uri, &did, &collection, &rkey], 44 + ) 45 + .await?; 46 + 47 + tracing::debug!("Indexed record: {}", at_uri); 48 + Ok(()) 49 + } 50 + 51 + /// Update cursor position in database for reconnection. 52 + #[allow(dead_code)] 53 + pub async fn save_cursor(&self, cursor_us: i64) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 54 + let client = self.pool.get().await?; 55 + client 56 + .execute( 57 + "INSERT INTO firehose_cursors (endpoint, cursor_us) 58 + VALUES ($1, $2) 59 + ON CONFLICT (endpoint) DO UPDATE SET cursor_us = $2, updated_at = NOW()", 60 + &[&DEFAULT_JETSTREAM_URL, &cursor_us], 61 + ) 62 + .await?; 63 + Ok(()) 64 + } 65 + 66 + /// Get saved cursor position for reconnection. 67 + pub async fn get_cursor(&self) -> Option<i64> { 68 + let client = self.pool.get().await.ok()?; 69 + let row = client 70 + .query_opt( 71 + "SELECT cursor_us FROM firehose_cursors WHERE endpoint = $1", 72 + &[&DEFAULT_JETSTREAM_URL], 73 + ) 74 + .await 75 + .ok()??; 76 + row.get("cursor_us") 77 + } 78 + } 79 + 80 + #[async_trait] 81 + impl EventHandler for MalfestioEventHandler { 82 + fn handler_id(&self) -> String { 83 + self.handler_id.clone() 84 + } 85 + 86 + async fn handle_event(&self, event: JetstreamEvent) -> Result<(), anyhow::Error> { 87 + match event { 88 + JetstreamEvent::Commit { did, commit, .. } => { 89 + let collection = &commit.collection; 90 + 91 + // Only process our collections 92 + if !MALFESTIO_COLLECTIONS.iter().any(|c| collection == *c) { 93 + return Ok(()); 94 + } 95 + 96 + let rkey = &commit.rkey; 97 + 98 + tracing::info!("Received {} event: did={}, rkey={}", collection, did, rkey); 99 + 100 + // Index the record 101 + if let Err(e) = self.index_record(&did, collection, rkey).await { 102 + tracing::warn!("Failed to index record: {}", e); 103 + } 104 + } 105 + JetstreamEvent::Identity { .. } | JetstreamEvent::Account { .. } | JetstreamEvent::Delete { .. } => { 106 + // Ignore identity, account, and delete events 107 + } 108 + } 109 + Ok(()) 110 + } 111 + } 112 + 113 + /// Configuration for the firehose consumer. 114 + pub struct FirehoseConfig { 115 + /// Jetstream WebSocket URL 116 + pub jetstream_url: String, 117 + /// Collections to filter for 118 + pub collections: Vec<String>, 119 + /// Enable zstd compression 120 + pub compress: bool, 121 + } 122 + 123 + impl Default for FirehoseConfig { 124 + fn default() -> Self { 125 + Self { 126 + jetstream_url: DEFAULT_JETSTREAM_URL.to_string(), 127 + collections: MALFESTIO_COLLECTIONS.iter().map(|s| s.to_string()).collect(), 128 + compress: true, 129 + } 130 + } 131 + } 132 + 133 + /// Start the firehose consumer as a background task. 134 + /// 135 + /// Returns a `CancellationToken` that can be used to stop the consumer. 136 + pub async fn start_firehose(pool: DbPool, config: FirehoseConfig) -> CancellationToken { 137 + let cancel = CancellationToken::new(); 138 + let cancel_clone = cancel.clone(); 139 + 140 + let handler = MalfestioEventHandler::new(pool); 141 + 142 + // Build consumer config 143 + let task_config = ConsumerTaskConfig { 144 + user_agent: "malfestio-indexer/0.1.0".to_string(), 145 + compression: config.compress, 146 + zstd_dictionary_location: String::new(), 147 + jetstream_hostname: config.jetstream_url.replace("wss://", "").replace("/subscribe", ""), 148 + collections: config.collections, 149 + dids: vec![], 150 + max_message_size_bytes: None, 151 + cursor: None, 152 + require_hello: false, 153 + }; 154 + 155 + tokio::spawn(async move { 156 + tracing::info!("Starting Jetstream firehose consumer..."); 157 + 158 + if let Some(cursor) = handler.get_cursor().await { 159 + tracing::info!("Resuming from cursor: {}", cursor); 160 + } 161 + 162 + let consumer = Consumer::new(task_config); 163 + if let Err(e) = consumer.register_handler(std::sync::Arc::new(handler)).await { 164 + tracing::error!("Failed to register handler: {}", e); 165 + return; 166 + } 167 + 168 + if let Err(e) = consumer.run_background(cancel_clone).await { 169 + tracing::error!("Firehose consumer error: {}", e); 170 + } 171 + 172 + tracing::info!("Firehose consumer stopped"); 173 + }); 174 + 175 + cancel 176 + } 177 + 178 + #[cfg(test)] 179 + mod tests { 180 + use super::*; 181 + 182 + #[test] 183 + fn test_default_firehose_config() { 184 + let config = FirehoseConfig::default(); 185 + assert_eq!(config.jetstream_url, DEFAULT_JETSTREAM_URL); 186 + assert_eq!(config.collections.len(), 3); 187 + assert!(config.compress); 188 + } 189 + 190 + #[test] 191 + fn test_malfestio_collections() { 192 + assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.deck")); 193 + assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.card")); 194 + assert!(MALFESTIO_COLLECTIONS.contains(&"app.malfestio.note")); 195 + } 196 + }
+4
crates/server/src/lib.rs
··· 1 1 pub mod api; 2 2 pub mod db; 3 + pub mod firehose; 3 4 pub mod middleware; 4 5 pub mod oauth; 5 6 pub mod pds; 6 7 pub mod repository; 7 8 pub mod state; 9 + pub mod well_known; 8 10 9 11 use axum::http::Method; 10 12 use axum::{ ··· 45 47 let auth_routes = Router::new() 46 48 .route("/me", get(api::auth::me)) 47 49 .route("/decks", post(api::deck::create_deck)) 50 + .route("/decks/{id}/publish", post(api::deck::publish_deck)) 48 51 .route("/notes", post(api::note::create_note)) 49 52 .route("/cards", post(api::card::create_card)) 50 53 .layer(axum_middleware::from_fn(middleware::auth::auth_middleware)); ··· 69 72 "/.well-known/oauth-client-metadata", 70 73 get(oauth::client_metadata::client_metadata_handler), 71 74 ) 75 + .route("/.well-known/atproto-did", get(well_known::atproto_did_handler)) 72 76 .route("/api/auth/login", post(api::auth::login)) 73 77 .route("/api/import/article", post(api::importer::import_article)) 74 78 .nest("/api/oauth", oauth_routes)
+1
crates/server/src/pds/mod.rs
··· 6 6 //! - uploadBlob - Upload media attachments 7 7 8 8 pub mod client; 9 + pub mod publish; 9 10 pub mod records;
+122
crates/server/src/pds/publish.rs
··· 1 + //! Publishing service for AT Protocol PDS operations. 2 + //! 3 + //! Encapsulates the logic for publishing records to a user's PDS. 4 + 5 + use crate::pds::client::{PdsClient, PdsError}; 6 + use crate::pds::records::{prepare_card_record, prepare_deck_record}; 7 + use crate::repository::oauth::{OAuthRepoError, OAuthRepository, StoredToken}; 8 + use malfestio_core::model::{Card, Deck}; 9 + use std::sync::Arc; 10 + 11 + /// Error type for publishing operations. 12 + #[derive(Debug)] 13 + pub enum PublishError { 14 + /// User has no stored OAuth tokens 15 + NoTokens(String), 16 + /// OAuth token retrieval failed 17 + TokenError(String), 18 + /// Invalid DPoP keypair 19 + InvalidKeypair, 20 + /// PDS operation failed 21 + PdsError(PdsError), 22 + /// Database error storing AT-URI 23 + DatabaseError(String), 24 + } 25 + 26 + impl std::fmt::Display for PublishError { 27 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 + match self { 29 + PublishError::NoTokens(did) => write!(f, "No OAuth tokens for DID: {}", did), 30 + PublishError::TokenError(e) => write!(f, "Token error: {}", e), 31 + PublishError::InvalidKeypair => write!(f, "Invalid DPoP keypair"), 32 + PublishError::PdsError(e) => write!(f, "PDS error: {}", e), 33 + PublishError::DatabaseError(e) => write!(f, "Database error: {}", e), 34 + } 35 + } 36 + } 37 + 38 + impl std::error::Error for PublishError {} 39 + 40 + impl From<OAuthRepoError> for PublishError { 41 + fn from(e: OAuthRepoError) -> Self { 42 + match e { 43 + OAuthRepoError::NotFound(did) => PublishError::NoTokens(did), 44 + _ => PublishError::TokenError(e.to_string()), 45 + } 46 + } 47 + } 48 + 49 + impl From<PdsError> for PublishError { 50 + fn from(e: PdsError) -> Self { 51 + PublishError::PdsError(e) 52 + } 53 + } 54 + 55 + /// Result of publishing a deck to PDS. 56 + pub struct PublishDeckResult { 57 + /// The AT-URI of the published deck 58 + pub deck_at_uri: String, 59 + /// The AT-URIs of the published cards 60 + pub card_at_uris: Vec<String>, 61 + } 62 + 63 + /// Publish a deck and its cards to the user's PDS. 64 + /// 65 + /// This function: 66 + /// 1. Retrieves OAuth tokens for the user 67 + /// 2. Creates a PDS client 68 + /// 3. Publishes each card (with placeholder deck ref initially) 69 + /// 4. Publishes the deck with card AT-URIs 70 + /// 71 + /// Note: Cards are published with an empty deck_ref since we don't have the 72 + /// deck's AT-URI yet. This is acceptable per the Lexicon - the deck holds 73 + /// the authoritative list of card references. 74 + pub async fn publish_deck_to_pds( 75 + oauth_repo: Arc<dyn OAuthRepository>, did: &str, deck: &Deck, cards: &[Card], 76 + ) -> Result<PublishDeckResult, PublishError> { 77 + let stored_token: StoredToken = oauth_repo.get_tokens(did).await?; 78 + let dpop_keypair = stored_token.dpop_keypair().ok_or(PublishError::InvalidKeypair)?; 79 + 80 + let pds_client = PdsClient::new( 81 + stored_token.pds_url.clone(), 82 + stored_token.access_token.clone(), 83 + dpop_keypair, 84 + ); 85 + 86 + let mut card_at_uris = Vec::with_capacity(cards.len()); 87 + for card in cards { 88 + let prepared = prepare_card_record(card, ""); 89 + let at_uri = pds_client 90 + .put_record(did, &prepared.collection, &prepared.rkey, prepared.record) 91 + .await?; 92 + card_at_uris.push(at_uri.to_string()); 93 + } 94 + 95 + let prepared = prepare_deck_record(deck, card_at_uris.clone()); 96 + let deck_at_uri = pds_client 97 + .put_record(did, &prepared.collection, &prepared.rkey, prepared.record) 98 + .await?; 99 + 100 + Ok(PublishDeckResult { deck_at_uri: deck_at_uri.to_string(), card_at_uris }) 101 + } 102 + 103 + #[cfg(test)] 104 + mod tests { 105 + use super::*; 106 + 107 + #[test] 108 + fn test_publish_error_display() { 109 + let err = PublishError::NoTokens("did:plc:test".to_string()); 110 + assert!(err.to_string().contains("did:plc:test")); 111 + 112 + let err = PublishError::InvalidKeypair; 113 + assert!(err.to_string().contains("Invalid DPoP keypair")); 114 + } 115 + 116 + #[test] 117 + fn test_publish_error_from_oauth_error() { 118 + let oauth_err = OAuthRepoError::NotFound("did:plc:test".to_string()); 119 + let publish_err: PublishError = oauth_err.into(); 120 + assert!(matches!(publish_err, PublishError::NoTokens(_))); 121 + } 122 + }
+98
crates/server/src/repository/oauth.rs
··· 211 211 assert!(err.to_string().contains("connection failed")); 212 212 } 213 213 } 214 + 215 + #[cfg(test)] 216 + pub mod mock { 217 + use super::*; 218 + use std::sync::{Arc, Mutex}; 219 + 220 + #[derive(Clone)] 221 + pub struct MockOAuthRepository { 222 + pub tokens: Arc<Mutex<Vec<StoredToken>>>, 223 + pub should_fail: Arc<Mutex<bool>>, 224 + } 225 + 226 + impl MockOAuthRepository { 227 + pub fn new() -> Self { 228 + Self { tokens: Arc::new(Mutex::new(Vec::new())), should_fail: Arc::new(Mutex::new(false)) } 229 + } 230 + 231 + pub fn with_tokens(tokens: Vec<StoredToken>) -> Self { 232 + Self { tokens: Arc::new(Mutex::new(tokens)), should_fail: Arc::new(Mutex::new(false)) } 233 + } 234 + 235 + pub fn set_should_fail(&self, should_fail: bool) { 236 + *self.should_fail.lock().unwrap() = should_fail; 237 + } 238 + } 239 + 240 + impl Default for MockOAuthRepository { 241 + fn default() -> Self { 242 + Self::new() 243 + } 244 + } 245 + 246 + #[async_trait] 247 + impl OAuthRepository for MockOAuthRepository { 248 + async fn store_tokens(&self, req: StoreTokensRequest<'_>) -> Result<(), OAuthRepoError> { 249 + if *self.should_fail.lock().unwrap() { 250 + return Err(OAuthRepoError::DatabaseError("Mock failure".to_string())); 251 + } 252 + 253 + let token = StoredToken { 254 + did: req.did.to_string(), 255 + pds_url: req.pds_url.to_string(), 256 + access_token: req.access_token.to_string(), 257 + refresh_token: req.refresh_token.map(String::from), 258 + token_type: req.token_type.to_string(), 259 + expires_at: req.expires_at, 260 + dpop_private_key: req.dpop_keypair.private_key_bytes(), 261 + created_at: Utc::now(), 262 + updated_at: Utc::now(), 263 + }; 264 + 265 + self.tokens.lock().unwrap().push(token); 266 + Ok(()) 267 + } 268 + 269 + async fn get_tokens(&self, did: &str) -> Result<StoredToken, OAuthRepoError> { 270 + if *self.should_fail.lock().unwrap() { 271 + return Err(OAuthRepoError::DatabaseError("Mock failure".to_string())); 272 + } 273 + 274 + let tokens = self.tokens.lock().unwrap(); 275 + tokens 276 + .iter() 277 + .find(|t| t.did == did) 278 + .cloned() 279 + .ok_or_else(|| OAuthRepoError::NotFound(format!("No tokens for DID: {}", did))) 280 + } 281 + 282 + async fn update_tokens( 283 + &self, did: &str, access_token: &str, refresh_token: Option<&str>, expires_at: Option<DateTime<Utc>>, 284 + ) -> Result<(), OAuthRepoError> { 285 + if *self.should_fail.lock().unwrap() { 286 + return Err(OAuthRepoError::DatabaseError("Mock failure".to_string())); 287 + } 288 + 289 + let mut tokens = self.tokens.lock().unwrap(); 290 + if let Some(token) = tokens.iter_mut().find(|t| t.did == did) { 291 + token.access_token = access_token.to_string(); 292 + token.refresh_token = refresh_token.map(String::from); 293 + token.expires_at = expires_at; 294 + token.updated_at = Utc::now(); 295 + Ok(()) 296 + } else { 297 + Err(OAuthRepoError::NotFound(format!("No tokens for DID: {}", did))) 298 + } 299 + } 300 + 301 + async fn delete_tokens(&self, did: &str) -> Result<(), OAuthRepoError> { 302 + if *self.should_fail.lock().unwrap() { 303 + return Err(OAuthRepoError::DatabaseError("Mock failure".to_string())); 304 + } 305 + 306 + let mut tokens = self.tokens.lock().unwrap(); 307 + tokens.retain(|t| t.did != did); 308 + Ok(()) 309 + } 310 + } 311 + }
+6 -2
crates/server/src/state.rs
··· 1 1 use crate::db::DbPool; 2 2 use crate::repository::card::{CardRepository, DbCardRepository}; 3 3 use crate::repository::note::{DbNoteRepository, NoteRepository}; 4 + use crate::repository::oauth::{DbOAuthRepository, OAuthRepository}; 4 5 use std::sync::Arc; 5 6 6 7 pub type SharedState = Arc<AppState>; ··· 9 10 pub pool: DbPool, 10 11 pub card_repo: Arc<dyn CardRepository>, 11 12 pub note_repo: Arc<dyn NoteRepository>, 13 + pub oauth_repo: Arc<dyn OAuthRepository>, 12 14 } 13 15 14 16 impl AppState { 15 17 pub fn new(pool: DbPool) -> SharedState { 16 18 let card_repo = Arc::new(DbCardRepository::new(pool.clone())) as Arc<dyn CardRepository>; 17 19 let note_repo = Arc::new(DbNoteRepository::new(pool.clone())) as Arc<dyn NoteRepository>; 20 + let oauth_repo = Arc::new(DbOAuthRepository::new(pool.clone())) as Arc<dyn OAuthRepository>; 18 21 19 - Arc::new(Self { pool, card_repo, note_repo }) 22 + Arc::new(Self { pool, card_repo, note_repo, oauth_repo }) 20 23 } 21 24 22 25 #[cfg(test)] 23 26 pub fn new_with_repos( 24 27 pool: DbPool, card_repo: Arc<dyn CardRepository>, note_repo: Arc<dyn NoteRepository>, 28 + oauth_repo: Arc<dyn OAuthRepository>, 25 29 ) -> SharedState { 26 - Arc::new(Self { pool, card_repo, note_repo }) 30 + Arc::new(Self { pool, card_repo, note_repo, oauth_repo }) 27 31 } 28 32 }
+36
crates/server/src/well_known.rs
··· 1 + //! Well-known endpoints for AT Protocol. 2 + //! 3 + //! Provides: 4 + //! - `/.well-known/atproto-did` - Returns the server's DID for domain verification 5 + 6 + use axum::response::IntoResponse; 7 + 8 + /// Handler for `/.well-known/atproto-did`. 9 + /// 10 + /// Returns the server's DID from the `ATPROTO_SERVER_DID` environment variable. 11 + /// Used for domain verification in AT Protocol. 12 + pub async fn atproto_did_handler() -> impl IntoResponse { 13 + std::env::var("ATPROTO_SERVER_DID").unwrap_or_default() 14 + } 15 + 16 + #[cfg(test)] 17 + mod tests { 18 + use super::*; 19 + 20 + #[tokio::test] 21 + async fn test_atproto_did_handler_empty_when_not_set() { 22 + let original = std::env::var("ATPROTO_SERVER_DID").ok(); 23 + unsafe { 24 + std::env::remove_var("ATPROTO_SERVER_DID"); 25 + } 26 + 27 + let result = atproto_did_handler().await.into_response(); 28 + assert_eq!(result.status(), axum::http::StatusCode::OK); 29 + 30 + if let Some(val) = original { 31 + unsafe { 32 + std::env::set_var("ATPROTO_SERVER_DID", val); 33 + } 34 + } 35 + } 36 + }
+5 -20
docs/todo.md
··· 38 38 ## Roadmap Milestones 39 39 40 40 - **(Done) Milestone A**: Defined core user journeys, information architecture, and privacy rules for the platform. 41 - 42 41 - **(Done) Milestone B**: Designed AT Protocol Lexicons for all core types and documented data model mapping + publishing pipeline. 43 - 44 42 - **(Done) Milestone C**: Foundations: Repo, CI, Axum API Skeleton, Solid Shell. 45 43 - Monorepo layout, CI, Axum/Solid skeletons implemented. 46 44 - Backend running on 8080, Frontend on 3000. 47 - 48 45 - **(Done) Milestone D**: Identity + Permissions + Publishing Model. 49 46 - Auth MVP, Permission model (Private/Public/SharedWith), and basic Publishing flow implemented. 50 47 - Backend API and Frontend Editor updated with tests covering permissions and publishing. 48 + - **(Done) Milestone F**: OAuth + PDS Record Publishing. 49 + - OAuth 2.1 client flow (PKCE, DPoP, handle/DID resolution, token refresh). 50 + - PDS client for `putRecord`, `deleteRecord`, `uploadBlob`. 51 + - TID generation and AT-URI builder in core crate. 52 + - Database migration for token storage and AT-URI columns. 51 53 52 54 ### Milestone E - Content Authoring (Notes + Cards + Deck Builder) 53 55 ··· 66 68 #### Acceptance 67 69 68 70 - A creator can build a deck from an article and publish it. 69 - 70 - ### Milestone F - OAuth + PDS Record Publishing 71 - 72 - #### Deliverables 73 - 74 - - OAuth 2.1 client flow (PKCE + DPoP) 75 - - client_metadata.json endpoint 76 - - handle/DID resolution 77 - - token refresh 78 - - Record publishing to user's PDS 79 - - putRecord for decks, cards, notes 80 - - blob uploads for media attachments 81 - - AT-URI generation for cross-references 82 - 83 - #### Acceptance 84 - 85 - - A user can authenticate via OAuth, create a deck, and see it in their PDS repository. 86 71 87 72 ### Milestone G - Study Engine (SRS) + Daily Review UX 88 73
+25
migrations/003_2025_12_28_firehose_cursor.sql
··· 1 + -- Firehose cursor and indexed records for AT Protocol Jetstream consumption 2 + -- Tracks cursor position for reconnection and indexes discovered records 3 + 4 + -- Table for tracking Jetstream cursor position 5 + CREATE TABLE firehose_cursors ( 6 + id SERIAL PRIMARY KEY, 7 + endpoint TEXT NOT NULL UNIQUE, 8 + cursor_us BIGINT NOT NULL, -- Unix microseconds timestamp 9 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 10 + ); 11 + 12 + CREATE INDEX idx_firehose_cursors_endpoint ON firehose_cursors(endpoint); 13 + 14 + CREATE TABLE indexed_records ( 15 + id SERIAL PRIMARY KEY, 16 + at_uri TEXT NOT NULL UNIQUE, 17 + did TEXT NOT NULL, 18 + collection TEXT NOT NULL, 19 + rkey TEXT NOT NULL, 20 + indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 21 + ); 22 + 23 + CREATE INDEX idx_indexed_records_did ON indexed_records(did); 24 + CREATE INDEX idx_indexed_records_collection ON indexed_records(collection); 25 + CREATE INDEX idx_indexed_records_at_uri ON indexed_records(at_uri);