[MIRROR ONLY] A correct and efficient ATProto blob proxy for secure content delivery. codeberg.org/Blooym/porxie
36
fork

Configure Feed

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

Merge pull request 'feat: use xrpc' (#3) from xrpcify into main

Reviewed-on: https://codeberg.org/Blooym/porxie/pulls/3

Blooym 55ce19b0 68db166e

+2668 -822
+1 -1
.gitignore
··· 1 1 target/ 2 2 .direnv/ 3 - lexicons/ 3 + __pycache__/ 4 4 5 5 test_server.py
+674 -288
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", 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", 22 - ] 23 - 24 - [[package]] 25 6 name = "adler2" 26 7 version = "2.0.1" 27 8 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 58 39 ] 59 40 60 41 [[package]] 42 + name = "allocator-api2" 43 + version = "0.2.21" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 46 + 47 + [[package]] 61 48 name = "android_system_properties" 62 49 version = "0.1.5" 63 50 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 186 173 dependencies = [ 187 174 "axum-core", 188 175 "bytes", 176 + "form_urlencoded", 189 177 "futures-util", 190 178 "http", 191 179 "http-body", ··· 201 189 "serde_core", 202 190 "serde_json", 203 191 "serde_path_to_error", 192 + "serde_urlencoded", 204 193 "sync_wrapper", 205 194 "tokio", 206 195 "tower", ··· 366 355 "proc-macro2", 367 356 "quote", 368 357 "rustversion", 369 - "syn 2.0.117", 358 + "syn", 370 359 ] 371 360 372 361 [[package]] 362 + name = "borrow-or-share" 363 + version = "0.2.4" 364 + source = "registry+https://github.com/rust-lang/crates.io-index" 365 + checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" 366 + 367 + [[package]] 373 368 name = "borsh" 374 369 version = "1.6.1" 375 370 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 401 396 ] 402 397 403 398 [[package]] 404 - name = "btree-range-map" 405 - version = "0.7.2" 406 - source = "registry+https://github.com/rust-lang/crates.io-index" 407 - checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" 408 - dependencies = [ 409 - "btree-slab", 410 - "cc-traits", 411 - "range-traits", 412 - "serde", 413 - "slab", 414 - ] 415 - 416 - [[package]] 417 - name = "btree-slab" 418 - version = "0.6.1" 419 - source = "registry+https://github.com/rust-lang/crates.io-index" 420 - checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" 421 - dependencies = [ 422 - "cc-traits", 423 - "slab", 424 - "smallvec", 425 - ] 426 - 427 - [[package]] 428 399 name = "bumpalo" 429 400 version = "3.20.2" 430 401 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 473 444 ] 474 445 475 446 [[package]] 476 - name = "cc-traits" 477 - version = "2.0.0" 478 - source = "registry+https://github.com/rust-lang/crates.io-index" 479 - checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" 480 - dependencies = [ 481 - "slab", 482 - ] 483 - 484 - [[package]] 485 447 name = "cfb" 486 448 version = "0.7.3" 487 449 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 560 522 561 523 [[package]] 562 524 name = "clap" 563 - version = "4.6.1" 525 + version = "4.5.61" 564 526 source = "registry+https://github.com/rust-lang/crates.io-index" 565 - checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" 527 + checksum = "52fa72306bb30daf11bc97773431628e5b4916e97aaa74b7d3f625d4d495da02" 566 528 dependencies = [ 567 529 "clap_builder", 568 530 "clap_derive", ··· 570 532 571 533 [[package]] 572 534 name = "clap_builder" 573 - version = "4.6.0" 535 + version = "4.5.61" 574 536 source = "registry+https://github.com/rust-lang/crates.io-index" 575 - checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" 537 + checksum = "2071365c5c56eae7d77414029dde2f4f4ba151cf68d5a3261c9a40de428ace93" 576 538 dependencies = [ 577 539 "anstream", 578 540 "anstyle", ··· 582 544 583 545 [[package]] 584 546 name = "clap_derive" 585 - version = "4.6.1" 547 + version = "4.5.61" 586 548 source = "registry+https://github.com/rust-lang/crates.io-index" 587 - checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" 549 + checksum = "dec5be1eea072311774b7b84ded287adbd9f293f9d23456817605c6042f4f5e0" 588 550 dependencies = [ 589 551 "heck 0.5.0", 590 552 "proc-macro2", 591 553 "quote", 592 - "syn 2.0.117", 554 + "syn", 593 555 ] 594 556 595 557 [[package]] ··· 781 743 ] 782 744 783 745 [[package]] 746 + name = "curve25519-dalek" 747 + version = "4.1.3" 748 + source = "registry+https://github.com/rust-lang/crates.io-index" 749 + checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 750 + dependencies = [ 751 + "cfg-if", 752 + "cpufeatures 0.2.17", 753 + "curve25519-dalek-derive", 754 + "digest 0.10.7", 755 + "fiat-crypto", 756 + "rustc_version", 757 + "subtle", 758 + "zeroize", 759 + ] 760 + 761 + [[package]] 762 + name = "curve25519-dalek-derive" 763 + version = "0.1.1" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 766 + dependencies = [ 767 + "proc-macro2", 768 + "quote", 769 + "syn", 770 + ] 771 + 772 + [[package]] 784 773 name = "darling" 785 774 version = "0.23.0" 786 775 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 800 789 "proc-macro2", 801 790 "quote", 802 791 "strsim", 803 - "syn 2.0.117", 792 + "syn", 804 793 ] 805 794 806 795 [[package]] ··· 811 800 dependencies = [ 812 801 "darling_core", 813 802 "quote", 814 - "syn 2.0.117", 803 + "syn", 815 804 ] 816 805 817 806 [[package]] ··· 851 840 checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" 852 841 dependencies = [ 853 842 "data-encoding", 854 - "syn 2.0.117", 843 + "syn", 855 844 ] 856 845 857 846 [[package]] ··· 872 861 checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" 873 862 dependencies = [ 874 863 "powerfmt", 875 - "serde_core", 876 864 ] 877 865 878 866 [[package]] ··· 892 880 dependencies = [ 893 881 "proc-macro2", 894 882 "quote", 895 - "syn 2.0.117", 883 + "syn", 896 884 "unicode-xid", 897 885 ] 898 886 ··· 932 920 dependencies = [ 933 921 "proc-macro2", 934 922 "quote", 935 - "syn 2.0.117", 923 + "syn", 936 924 ] 937 925 938 926 [[package]] ··· 942 930 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 943 931 944 932 [[package]] 945 - name = "dyn-clone" 946 - version = "1.0.20" 947 - source = "registry+https://github.com/rust-lang/crates.io-index" 948 - checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 949 - 950 - [[package]] 951 933 name = "ecdsa" 952 934 version = "0.16.9" 953 935 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 959 941 "rfc6979", 960 942 "signature", 961 943 "spki", 944 + ] 945 + 946 + [[package]] 947 + name = "ed25519" 948 + version = "2.2.3" 949 + source = "registry+https://github.com/rust-lang/crates.io-index" 950 + checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 951 + dependencies = [ 952 + "pkcs8", 953 + "signature", 954 + ] 955 + 956 + [[package]] 957 + name = "ed25519-dalek" 958 + version = "2.2.0" 959 + source = "registry+https://github.com/rust-lang/crates.io-index" 960 + checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" 961 + dependencies = [ 962 + "curve25519-dalek", 963 + "ed25519", 964 + "rand_core 0.6.4", 965 + "serde", 966 + "sha2 0.10.9", 967 + "subtle", 968 + "zeroize", 962 969 ] 963 970 964 971 [[package]] ··· 973 980 "ff", 974 981 "generic-array", 975 982 "group", 983 + "hkdf", 976 984 "pem-rfc7468", 977 985 "pkcs8", 978 986 "rand_core 0.6.4", ··· 1056 1064 ] 1057 1065 1058 1066 [[package]] 1067 + name = "fiat-crypto" 1068 + version = "0.2.9" 1069 + source = "registry+https://github.com/rust-lang/crates.io-index" 1070 + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 1071 + 1072 + [[package]] 1059 1073 name = "find-msvc-tools" 1060 1074 version = "0.1.9" 1061 1075 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1072 1086 ] 1073 1087 1074 1088 [[package]] 1089 + name = "fluent-uri" 1090 + version = "0.4.1" 1091 + source = "registry+https://github.com/rust-lang/crates.io-index" 1092 + checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" 1093 + dependencies = [ 1094 + "borrow-or-share", 1095 + "ref-cast", 1096 + "serde", 1097 + ] 1098 + 1099 + [[package]] 1075 1100 name = "fnv" 1076 1101 version = "1.0.7" 1077 1102 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1090 1115 checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 1091 1116 dependencies = [ 1092 1117 "percent-encoding", 1118 + ] 1119 + 1120 + [[package]] 1121 + name = "futf" 1122 + version = "0.1.5" 1123 + source = "registry+https://github.com/rust-lang/crates.io-index" 1124 + checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 1125 + dependencies = [ 1126 + "mac", 1127 + "new_debug_unreachable", 1093 1128 ] 1094 1129 1095 1130 [[package]] ··· 1147 1182 dependencies = [ 1148 1183 "proc-macro2", 1149 1184 "quote", 1150 - "syn 2.0.117", 1185 + "syn", 1151 1186 ] 1152 1187 1153 1188 [[package]] ··· 1245 1280 ] 1246 1281 1247 1282 [[package]] 1283 + name = "gloo-storage" 1284 + version = "0.3.0" 1285 + source = "registry+https://github.com/rust-lang/crates.io-index" 1286 + checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" 1287 + dependencies = [ 1288 + "gloo-utils", 1289 + "js-sys", 1290 + "serde", 1291 + "serde_json", 1292 + "thiserror 1.0.69", 1293 + "wasm-bindgen", 1294 + "web-sys", 1295 + ] 1296 + 1297 + [[package]] 1298 + name = "gloo-utils" 1299 + version = "0.2.0" 1300 + source = "registry+https://github.com/rust-lang/crates.io-index" 1301 + checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 1302 + dependencies = [ 1303 + "js-sys", 1304 + "serde", 1305 + "serde_json", 1306 + "wasm-bindgen", 1307 + "web-sys", 1308 + ] 1309 + 1310 + [[package]] 1248 1311 name = "group" 1249 1312 version = "0.13.0" 1250 1313 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1267 1330 "futures-core", 1268 1331 "futures-sink", 1269 1332 "http", 1270 - "indexmap 2.14.0", 1333 + "indexmap", 1271 1334 "slab", 1272 1335 "tokio", 1273 1336 "tokio-util", ··· 1296 1359 1297 1360 [[package]] 1298 1361 name = "hashbrown" 1299 - version = "0.12.3" 1300 - source = "registry+https://github.com/rust-lang/crates.io-index" 1301 - checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 1302 - 1303 - [[package]] 1304 - name = "hashbrown" 1305 1362 version = "0.14.5" 1306 1363 source = "registry+https://github.com/rust-lang/crates.io-index" 1307 1364 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" ··· 1312 1369 source = "registry+https://github.com/rust-lang/crates.io-index" 1313 1370 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 1314 1371 dependencies = [ 1372 + "allocator-api2", 1373 + "equivalent", 1315 1374 "foldhash", 1316 1375 ] 1317 1376 ··· 1378 1437 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1379 1438 1380 1439 [[package]] 1381 - name = "hex_fmt" 1382 - version = "0.3.0" 1440 + name = "hkdf" 1441 + version = "0.12.4" 1383 1442 source = "registry+https://github.com/rust-lang/crates.io-index" 1384 - checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" 1443 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1444 + dependencies = [ 1445 + "hmac", 1446 + ] 1385 1447 1386 1448 [[package]] 1387 1449 name = "hmac" ··· 1393 1455 ] 1394 1456 1395 1457 [[package]] 1458 + name = "html5ever" 1459 + version = "0.27.0" 1460 + source = "registry+https://github.com/rust-lang/crates.io-index" 1461 + checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" 1462 + dependencies = [ 1463 + "log", 1464 + "mac", 1465 + "markup5ever", 1466 + "proc-macro2", 1467 + "quote", 1468 + "syn", 1469 + ] 1470 + 1471 + [[package]] 1396 1472 name = "http" 1397 1473 version = "1.4.0" 1398 1474 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1656 1732 1657 1733 [[package]] 1658 1734 name = "indexmap" 1659 - version = "1.9.3" 1660 - source = "registry+https://github.com/rust-lang/crates.io-index" 1661 - checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 1662 - dependencies = [ 1663 - "autocfg", 1664 - "hashbrown 0.12.3", 1665 - "serde", 1666 - ] 1667 - 1668 - [[package]] 1669 - name = "indexmap" 1670 1735 version = "2.14.0" 1671 1736 source = "registry+https://github.com/rust-lang/crates.io-index" 1672 1737 checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" ··· 1675 1740 "hashbrown 0.17.0", 1676 1741 "serde", 1677 1742 "serde_core", 1678 - ] 1679 - 1680 - [[package]] 1681 - name = "indoc" 1682 - version = "2.0.7" 1683 - source = "registry+https://github.com/rust-lang/crates.io-index" 1684 - checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" 1685 - dependencies = [ 1686 - "rustversion", 1687 1743 ] 1688 1744 1689 1745 [[package]] ··· 1744 1800 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 1745 1801 1746 1802 [[package]] 1803 + name = "jacquard" 1804 + version = "0.11.0" 1805 + source = "registry+https://github.com/rust-lang/crates.io-index" 1806 + checksum = "1e4bb22329646c06eb07eb63ae6d33320e8f52ae8b5a5444257f27c3d68d44ae" 1807 + dependencies = [ 1808 + "bytes", 1809 + "getrandom 0.2.17", 1810 + "gloo-storage", 1811 + "http", 1812 + "jacquard-api", 1813 + "jacquard-common", 1814 + "jacquard-identity", 1815 + "jacquard-oauth", 1816 + "jose-jwk", 1817 + "miette", 1818 + "regex", 1819 + "regex-lite", 1820 + "reqwest", 1821 + "serde", 1822 + "serde_html_form", 1823 + "serde_json", 1824 + "smol_str", 1825 + "thiserror 2.0.18", 1826 + "tokio", 1827 + "trait-variant", 1828 + "webpage", 1829 + ] 1830 + 1831 + [[package]] 1747 1832 name = "jacquard-api" 1748 - version = "0.9.5" 1833 + version = "0.11.1" 1834 + source = "registry+https://github.com/rust-lang/crates.io-index" 1835 + checksum = "f4bba022e9c632f737de481d7c894b5736dd1a503362876ca4da49b9255a7e61" 1836 + dependencies = [ 1837 + "jacquard-common", 1838 + "jacquard-derive", 1839 + "jacquard-lexicon", 1840 + "miette", 1841 + "serde", 1842 + "thiserror 2.0.18", 1843 + ] 1844 + 1845 + [[package]] 1846 + name = "jacquard-axum" 1847 + version = "0.11.0" 1749 1848 source = "registry+https://github.com/rust-lang/crates.io-index" 1750 - checksum = "4979fb1848c1dd7ac8fd12745bc71f56f6da61374407d5f9b06005467a954e5a" 1849 + checksum = "707649f378c7aaac47a267e1eb95dbcf595fca3b0c949e0e7aa742dec7093cde" 1751 1850 dependencies = [ 1752 - "bon", 1851 + "axum", 1753 1852 "bytes", 1853 + "jacquard", 1754 1854 "jacquard-common", 1755 1855 "jacquard-derive", 1756 - "jacquard-lexicon", 1757 1856 "miette", 1758 - "rustversion", 1759 1857 "serde", 1760 - "serde_bytes", 1761 - "serde_ipld_dagcbor", 1858 + "serde_html_form", 1859 + "serde_json", 1762 1860 "thiserror 2.0.18", 1763 - "unicode-segmentation", 1861 + "tokio", 1862 + "tower-http", 1863 + "tracing", 1764 1864 ] 1765 1865 1766 1866 [[package]] 1767 1867 name = "jacquard-common" 1768 - version = "0.9.5" 1868 + version = "0.11.0" 1769 1869 source = "registry+https://github.com/rust-lang/crates.io-index" 1770 - checksum = "1751921e0bdae5e0077afade6161545e9ef7698306c868f800916e99ecbcaae9" 1870 + checksum = "9631f08f1e65d19e204bc6774d00b4e0b69fb649d7d7ac59ccf97797bd9a196e" 1771 1871 dependencies = [ 1772 1872 "base64", 1773 1873 "bon", 1774 1874 "bytes", 1775 1875 "chrono", 1876 + "ciborium", 1877 + "ciborium-io", 1776 1878 "cid", 1879 + "fluent-uri", 1777 1880 "getrandom 0.2.17", 1778 1881 "getrandom 0.3.4", 1882 + "hashbrown 0.15.5", 1779 1883 "http", 1780 1884 "ipld-core", 1781 1885 "k256", 1782 - "langtag", 1886 + "maitake-sync", 1783 1887 "miette", 1784 1888 "multibase", 1785 1889 "multihash", 1786 1890 "ouroboros", 1891 + "oxilangtag", 1787 1892 "p256", 1893 + "phf", 1788 1894 "postcard", 1789 - "rand", 1895 + "rand 0.9.4", 1790 1896 "regex", 1897 + "regex-automata", 1791 1898 "regex-lite", 1792 1899 "reqwest", 1900 + "rustversion", 1793 1901 "serde", 1794 1902 "serde_bytes", 1795 1903 "serde_html_form", ··· 1797 1905 "serde_json", 1798 1906 "signature", 1799 1907 "smol_str", 1908 + "spin 0.10.0", 1800 1909 "thiserror 2.0.18", 1801 1910 "tokio", 1802 1911 "tokio-util", 1803 1912 "trait-variant", 1804 - "url", 1913 + "unicode-segmentation", 1805 1914 ] 1806 1915 1807 1916 [[package]] 1808 1917 name = "jacquard-derive" 1809 - version = "0.9.5" 1918 + version = "0.11.0" 1810 1919 source = "registry+https://github.com/rust-lang/crates.io-index" 1811 - checksum = "9c8d73dfee07943fdab93569ed1c28b06c6921ed891c08b415c4a323ff67e593" 1920 + checksum = "22904bd0f9a959591e14ee9e1dab91a9110209cb8b292930e584134f96a83ece" 1812 1921 dependencies = [ 1813 1922 "heck 0.5.0", 1814 1923 "jacquard-lexicon", 1815 1924 "proc-macro2", 1816 1925 "quote", 1817 - "syn 2.0.117", 1926 + "syn", 1818 1927 ] 1819 1928 1820 1929 [[package]] 1821 1930 name = "jacquard-identity" 1822 - version = "0.9.5" 1931 + version = "0.11.0" 1823 1932 source = "registry+https://github.com/rust-lang/crates.io-index" 1824 - checksum = "e7aaefa819fa4213cf59f180dba932f018a7cd0599582fd38474ee2a38c16cf2" 1933 + checksum = "b84a9302ea9dd39c49d748a8eba21faf365570088571ff327ebb00117dbd65b8" 1825 1934 dependencies = [ 1826 1935 "bon", 1827 1936 "bytes", 1828 1937 "http", 1829 - "jacquard-api", 1830 1938 "jacquard-common", 1831 1939 "jacquard-lexicon", 1832 1940 "miette", 1941 + "mini-moka-wasm", 1833 1942 "n0-future", 1834 - "percent-encoding", 1835 1943 "reqwest", 1836 1944 "serde", 1837 1945 "serde_html_form", ··· 1840 1948 "tokio", 1841 1949 "tracing", 1842 1950 "trait-variant", 1843 - "url", 1844 - "urlencoding", 1845 1951 ] 1846 1952 1847 1953 [[package]] 1848 1954 name = "jacquard-lexicon" 1849 - version = "0.9.5" 1955 + version = "0.11.1" 1850 1956 source = "registry+https://github.com/rust-lang/crates.io-index" 1851 - checksum = "8411aff546569b0a1e0ef669bed2380cec1c00d48f02f3fcd57a71545321b3d8" 1957 + checksum = "fd7863d4f56a49f07391b5f775e82be12e6381156642ee83574f481ca73e8b0e" 1852 1958 dependencies = [ 1853 1959 "cid", 1854 1960 "dashmap", ··· 1863 1969 "serde", 1864 1970 "serde_ipld_dagcbor", 1865 1971 "serde_json", 1972 + "serde_path_to_error", 1866 1973 "serde_repr", 1867 1974 "serde_with", 1868 1975 "sha2 0.10.9", 1869 - "syn 2.0.117", 1976 + "syn", 1870 1977 "thiserror 2.0.18", 1871 1978 "unicode-segmentation", 1872 1979 ] 1873 1980 1874 1981 [[package]] 1982 + name = "jacquard-oauth" 1983 + version = "0.11.0" 1984 + source = "registry+https://github.com/rust-lang/crates.io-index" 1985 + checksum = "ca0b3a8b765b3d3f1890233f9f62dda1b1524d1dc9967c310e5b009b93443776" 1986 + dependencies = [ 1987 + "base64", 1988 + "bytes", 1989 + "chrono", 1990 + "dashmap", 1991 + "ed25519-dalek", 1992 + "elliptic-curve", 1993 + "http", 1994 + "jacquard-common", 1995 + "jacquard-identity", 1996 + "jose-jwa", 1997 + "jose-jwk", 1998 + "k256", 1999 + "miette", 2000 + "p256", 2001 + "p384", 2002 + "rand 0.8.6", 2003 + "serde", 2004 + "serde_html_form", 2005 + "serde_json", 2006 + "sha2 0.10.9", 2007 + "smol_str", 2008 + "thiserror 2.0.18", 2009 + "tokio", 2010 + "trait-variant", 2011 + ] 2012 + 2013 + [[package]] 1875 2014 name = "jemalloc-sys" 1876 2015 version = "0.5.4+5.3.0-patched" 1877 2016 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1902 2041 ] 1903 2042 1904 2043 [[package]] 2044 + name = "jose-b64" 2045 + version = "0.1.2" 2046 + source = "registry+https://github.com/rust-lang/crates.io-index" 2047 + checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" 2048 + dependencies = [ 2049 + "base64ct", 2050 + "serde", 2051 + "subtle", 2052 + "zeroize", 2053 + ] 2054 + 2055 + [[package]] 2056 + name = "jose-jwa" 2057 + version = "0.1.2" 2058 + source = "registry+https://github.com/rust-lang/crates.io-index" 2059 + checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" 2060 + dependencies = [ 2061 + "serde", 2062 + ] 2063 + 2064 + [[package]] 2065 + name = "jose-jwk" 2066 + version = "0.1.2" 2067 + source = "registry+https://github.com/rust-lang/crates.io-index" 2068 + checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" 2069 + dependencies = [ 2070 + "jose-b64", 2071 + "jose-jwa", 2072 + "p256", 2073 + "p384", 2074 + "rsa", 2075 + "serde", 2076 + "zeroize", 2077 + ] 2078 + 2079 + [[package]] 1905 2080 name = "js-sys" 1906 2081 version = "0.3.97" 1907 2082 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1938 2113 "cfg-if", 1939 2114 "ecdsa", 1940 2115 "elliptic-curve", 2116 + "once_cell", 1941 2117 "sha2 0.10.9", 2118 + "signature", 1942 2119 ] 1943 2120 1944 2121 [[package]] ··· 1952 2129 ] 1953 2130 1954 2131 [[package]] 1955 - name = "langtag" 1956 - version = "0.4.0" 1957 - source = "registry+https://github.com/rust-lang/crates.io-index" 1958 - checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" 1959 - dependencies = [ 1960 - "serde", 1961 - "static-regular-grammar", 1962 - "thiserror 1.0.69", 1963 - ] 1964 - 1965 - [[package]] 1966 2132 name = "lazy_static" 1967 2133 version = "1.5.0" 1968 2134 source = "registry+https://github.com/rust-lang/crates.io-index" 1969 2135 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2136 + dependencies = [ 2137 + "spin 0.9.8", 2138 + ] 1970 2139 1971 2140 [[package]] 1972 2141 name = "leb128fmt" ··· 1975 2144 checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 1976 2145 1977 2146 [[package]] 2147 + name = "lexgen" 2148 + version = "0.0.0" 2149 + dependencies = [ 2150 + "jacquard-common", 2151 + "jacquard-derive", 2152 + "jacquard-lexicon", 2153 + "miette", 2154 + "serde", 2155 + "thiserror 2.0.18", 2156 + ] 2157 + 2158 + [[package]] 1978 2159 name = "libc" 1979 2160 version = "0.2.186" 1980 2161 source = "registry+https://github.com/rust-lang/crates.io-index" 1981 2162 checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" 2163 + 2164 + [[package]] 2165 + name = "libm" 2166 + version = "0.2.16" 2167 + source = "registry+https://github.com/rust-lang/crates.io-index" 2168 + checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" 1982 2169 1983 2170 [[package]] 1984 2171 name = "litemap" ··· 2021 2208 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2022 2209 2023 2210 [[package]] 2211 + name = "mac" 2212 + version = "0.1.1" 2213 + source = "registry+https://github.com/rust-lang/crates.io-index" 2214 + checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 2215 + 2216 + [[package]] 2217 + name = "maitake-sync" 2218 + version = "0.1.2" 2219 + source = "registry+https://github.com/rust-lang/crates.io-index" 2220 + checksum = "6816ab14147f80234c675b80ed6dc4f440d8a1cefc158e766067aedb84c0bcd5" 2221 + dependencies = [ 2222 + "cordyceps", 2223 + "loom", 2224 + "mycelium-bitfield", 2225 + "pin-project", 2226 + "portable-atomic", 2227 + ] 2228 + 2229 + [[package]] 2230 + name = "markup5ever" 2231 + version = "0.12.1" 2232 + source = "registry+https://github.com/rust-lang/crates.io-index" 2233 + checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" 2234 + dependencies = [ 2235 + "log", 2236 + "phf", 2237 + "phf_codegen", 2238 + "string_cache", 2239 + "string_cache_codegen", 2240 + "tendril", 2241 + ] 2242 + 2243 + [[package]] 2244 + name = "markup5ever_rcdom" 2245 + version = "0.3.0" 2246 + source = "registry+https://github.com/rust-lang/crates.io-index" 2247 + checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" 2248 + dependencies = [ 2249 + "html5ever", 2250 + "markup5ever", 2251 + "tendril", 2252 + "xml5ever", 2253 + ] 2254 + 2255 + [[package]] 2024 2256 name = "match-lookup" 2025 2257 version = "0.1.2" 2026 2258 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2028 2260 dependencies = [ 2029 2261 "proc-macro2", 2030 2262 "quote", 2031 - "syn 2.0.117", 2263 + "syn", 2032 2264 ] 2033 2265 2034 2266 [[package]] ··· 2071 2303 dependencies = [ 2072 2304 "proc-macro2", 2073 2305 "quote", 2074 - "syn 2.0.117", 2306 + "syn", 2075 2307 ] 2076 2308 2077 2309 [[package]] ··· 2081 2313 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 2082 2314 2083 2315 [[package]] 2084 - name = "minimal-lexical" 2085 - version = "0.2.1" 2316 + name = "mini-moka-wasm" 2317 + version = "0.10.99" 2086 2318 source = "registry+https://github.com/rust-lang/crates.io-index" 2087 - checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2319 + checksum = "0102b9a2ad50fa47ca89eead2316c8222285ecfbd3f69ce99564fbe4253866e8" 2320 + dependencies = [ 2321 + "crossbeam-channel", 2322 + "crossbeam-utils", 2323 + "dashmap", 2324 + "smallvec", 2325 + "tagptr", 2326 + "triomphe", 2327 + "web-time", 2328 + ] 2088 2329 2089 2330 [[package]] 2090 2331 name = "miniz_oxide" ··· 2186 2427 "proc-macro-crate", 2187 2428 "proc-macro2", 2188 2429 "quote", 2189 - "syn 2.0.117", 2430 + "syn", 2190 2431 "synstructure", 2191 2432 ] 2433 + 2434 + [[package]] 2435 + name = "mycelium-bitfield" 2436 + version = "0.1.5" 2437 + source = "registry+https://github.com/rust-lang/crates.io-index" 2438 + checksum = "24e0cc5e2c585acbd15c5ce911dff71e1f4d5313f43345873311c4f5efd741cc" 2192 2439 2193 2440 [[package]] 2194 2441 name = "n0-future" ··· 2212 2459 ] 2213 2460 2214 2461 [[package]] 2215 - name = "nom" 2216 - version = "7.1.3" 2462 + name = "new_debug_unreachable" 2463 + version = "1.0.6" 2217 2464 source = "registry+https://github.com/rust-lang/crates.io-index" 2218 - checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2219 - dependencies = [ 2220 - "memchr", 2221 - "minimal-lexical", 2222 - ] 2465 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2223 2466 2224 2467 [[package]] 2225 2468 name = "ntapi" ··· 2240 2483 ] 2241 2484 2242 2485 [[package]] 2486 + name = "num-bigint-dig" 2487 + version = "0.8.6" 2488 + source = "registry+https://github.com/rust-lang/crates.io-index" 2489 + checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" 2490 + dependencies = [ 2491 + "lazy_static", 2492 + "libm", 2493 + "num-integer", 2494 + "num-iter", 2495 + "num-traits", 2496 + "rand 0.8.6", 2497 + "smallvec", 2498 + "zeroize", 2499 + ] 2500 + 2501 + [[package]] 2243 2502 name = "num-conv" 2244 2503 version = "0.2.1" 2245 2504 source = "registry+https://github.com/rust-lang/crates.io-index" 2246 2505 checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" 2247 2506 2248 2507 [[package]] 2508 + name = "num-integer" 2509 + version = "0.1.46" 2510 + source = "registry+https://github.com/rust-lang/crates.io-index" 2511 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 2512 + dependencies = [ 2513 + "num-traits", 2514 + ] 2515 + 2516 + [[package]] 2517 + name = "num-iter" 2518 + version = "0.1.45" 2519 + source = "registry+https://github.com/rust-lang/crates.io-index" 2520 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2521 + dependencies = [ 2522 + "autocfg", 2523 + "num-integer", 2524 + "num-traits", 2525 + ] 2526 + 2527 + [[package]] 2249 2528 name = "num-traits" 2250 2529 version = "0.2.19" 2251 2530 source = "registry+https://github.com/rust-lang/crates.io-index" 2252 2531 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 2253 2532 dependencies = [ 2254 2533 "autocfg", 2534 + "libm", 2255 2535 ] 2256 2536 2257 2537 [[package]] ··· 2306 2586 "proc-macro2", 2307 2587 "proc-macro2-diagnostics", 2308 2588 "quote", 2309 - "syn 2.0.117", 2589 + "syn", 2590 + ] 2591 + 2592 + [[package]] 2593 + name = "oxilangtag" 2594 + version = "0.1.5" 2595 + source = "registry+https://github.com/rust-lang/crates.io-index" 2596 + checksum = "23f3f87617a86af77fa3691e6350483e7154c2ead9f1261b75130e21ca0f8acb" 2597 + dependencies = [ 2598 + "serde", 2310 2599 ] 2311 2600 2312 2601 [[package]] ··· 2322 2611 ] 2323 2612 2324 2613 [[package]] 2614 + name = "p384" 2615 + version = "0.13.1" 2616 + source = "registry+https://github.com/rust-lang/crates.io-index" 2617 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 2618 + dependencies = [ 2619 + "ecdsa", 2620 + "elliptic-curve", 2621 + "primeorder", 2622 + "sha2 0.10.9", 2623 + ] 2624 + 2625 + [[package]] 2325 2626 name = "parking" 2326 2627 version = "2.2.1" 2327 2628 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2366 2667 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 2367 2668 2368 2669 [[package]] 2670 + name = "phf" 2671 + version = "0.11.3" 2672 + source = "registry+https://github.com/rust-lang/crates.io-index" 2673 + checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 2674 + dependencies = [ 2675 + "phf_macros", 2676 + "phf_shared", 2677 + ] 2678 + 2679 + [[package]] 2680 + name = "phf_codegen" 2681 + version = "0.11.3" 2682 + source = "registry+https://github.com/rust-lang/crates.io-index" 2683 + checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 2684 + dependencies = [ 2685 + "phf_generator", 2686 + "phf_shared", 2687 + ] 2688 + 2689 + [[package]] 2690 + name = "phf_generator" 2691 + version = "0.11.3" 2692 + source = "registry+https://github.com/rust-lang/crates.io-index" 2693 + checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 2694 + dependencies = [ 2695 + "phf_shared", 2696 + "rand 0.8.6", 2697 + ] 2698 + 2699 + [[package]] 2700 + name = "phf_macros" 2701 + version = "0.11.3" 2702 + source = "registry+https://github.com/rust-lang/crates.io-index" 2703 + checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" 2704 + dependencies = [ 2705 + "phf_generator", 2706 + "phf_shared", 2707 + "proc-macro2", 2708 + "quote", 2709 + "syn", 2710 + ] 2711 + 2712 + [[package]] 2713 + name = "phf_shared" 2714 + version = "0.11.3" 2715 + source = "registry+https://github.com/rust-lang/crates.io-index" 2716 + checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2717 + dependencies = [ 2718 + "siphasher", 2719 + ] 2720 + 2721 + [[package]] 2369 2722 name = "pin-project" 2370 2723 version = "1.1.11" 2371 2724 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2382 2735 dependencies = [ 2383 2736 "proc-macro2", 2384 2737 "quote", 2385 - "syn 2.0.117", 2738 + "syn", 2386 2739 ] 2387 2740 2388 2741 [[package]] ··· 2392 2745 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 2393 2746 2394 2747 [[package]] 2748 + name = "pkcs1" 2749 + version = "0.7.5" 2750 + source = "registry+https://github.com/rust-lang/crates.io-index" 2751 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 2752 + dependencies = [ 2753 + "der", 2754 + "pkcs8", 2755 + "spki", 2756 + ] 2757 + 2758 + [[package]] 2395 2759 name = "pkcs8" 2396 2760 version = "0.10.2" 2397 2761 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2428 2792 "futures-util", 2429 2793 "humantime", 2430 2794 "infer", 2795 + "jacquard-axum", 2431 2796 "jacquard-common", 2432 2797 "jacquard-identity", 2433 2798 "jemallocator", 2434 2799 "json-subscriber", 2800 + "lexgen", 2435 2801 "mime", 2436 2802 "moka", 2437 2803 "multihash-codetable", 2438 2804 "reqwest", 2439 2805 "serde", 2806 + "serde_json", 2440 2807 "subtle", 2441 2808 "sysinfo", 2442 2809 "thiserror 2.0.18", ··· 2484 2851 ] 2485 2852 2486 2853 [[package]] 2854 + name = "precomputed-hash" 2855 + version = "0.1.1" 2856 + source = "registry+https://github.com/rust-lang/crates.io-index" 2857 + checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 2858 + 2859 + [[package]] 2487 2860 name = "prettyplease" 2488 2861 version = "0.2.37" 2489 2862 source = "registry+https://github.com/rust-lang/crates.io-index" 2490 2863 checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" 2491 2864 dependencies = [ 2492 2865 "proc-macro2", 2493 - "syn 2.0.117", 2866 + "syn", 2494 2867 ] 2495 2868 2496 2869 [[package]] ··· 2512 2885 ] 2513 2886 2514 2887 [[package]] 2515 - name = "proc-macro-error" 2516 - version = "1.0.4" 2517 - source = "registry+https://github.com/rust-lang/crates.io-index" 2518 - checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 2519 - dependencies = [ 2520 - "proc-macro-error-attr", 2521 - "proc-macro2", 2522 - "quote", 2523 - "syn 1.0.109", 2524 - "version_check", 2525 - ] 2526 - 2527 - [[package]] 2528 - name = "proc-macro-error-attr" 2529 - version = "1.0.4" 2530 - source = "registry+https://github.com/rust-lang/crates.io-index" 2531 - checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 2532 - dependencies = [ 2533 - "proc-macro2", 2534 - "quote", 2535 - "version_check", 2536 - ] 2537 - 2538 - [[package]] 2539 2888 name = "proc-macro2" 2540 2889 version = "1.0.106" 2541 2890 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2552 2901 dependencies = [ 2553 2902 "proc-macro2", 2554 2903 "quote", 2555 - "syn 2.0.117", 2904 + "syn", 2556 2905 "version_check", 2557 2906 "yansi", 2558 2907 ] ··· 2586 2935 "bytes", 2587 2936 "getrandom 0.3.4", 2588 2937 "lru-slab", 2589 - "rand", 2938 + "rand 0.9.4", 2590 2939 "ring", 2591 2940 "rustc-hash", 2592 2941 "rustls", ··· 2632 2981 version = "6.0.0" 2633 2982 source = "registry+https://github.com/rust-lang/crates.io-index" 2634 2983 checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 2984 + 2985 + [[package]] 2986 + name = "rand" 2987 + version = "0.8.6" 2988 + source = "registry+https://github.com/rust-lang/crates.io-index" 2989 + checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" 2990 + dependencies = [ 2991 + "libc", 2992 + "rand_chacha 0.3.1", 2993 + "rand_core 0.6.4", 2994 + ] 2635 2995 2636 2996 [[package]] 2637 2997 name = "rand" ··· 2639 2999 source = "registry+https://github.com/rust-lang/crates.io-index" 2640 3000 checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" 2641 3001 dependencies = [ 2642 - "rand_chacha", 3002 + "rand_chacha 0.9.0", 2643 3003 "rand_core 0.9.5", 2644 3004 ] 2645 3005 2646 3006 [[package]] 2647 3007 name = "rand_chacha" 3008 + version = "0.3.1" 3009 + source = "registry+https://github.com/rust-lang/crates.io-index" 3010 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 3011 + dependencies = [ 3012 + "ppv-lite86", 3013 + "rand_core 0.6.4", 3014 + ] 3015 + 3016 + [[package]] 3017 + name = "rand_chacha" 2648 3018 version = "0.9.0" 2649 3019 source = "registry+https://github.com/rust-lang/crates.io-index" 2650 3020 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" ··· 2672 3042 ] 2673 3043 2674 3044 [[package]] 2675 - name = "range-traits" 2676 - version = "0.3.2" 2677 - source = "registry+https://github.com/rust-lang/crates.io-index" 2678 - checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" 2679 - 2680 - [[package]] 2681 3045 name = "redox_syscall" 2682 3046 version = "0.5.18" 2683 3047 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2703 3067 dependencies = [ 2704 3068 "proc-macro2", 2705 3069 "quote", 2706 - "syn 2.0.117", 3070 + "syn", 2707 3071 ] 2708 3072 2709 3073 [[package]] ··· 2819 3183 ] 2820 3184 2821 3185 [[package]] 3186 + name = "rsa" 3187 + version = "0.9.10" 3188 + source = "registry+https://github.com/rust-lang/crates.io-index" 3189 + checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" 3190 + dependencies = [ 3191 + "const-oid", 3192 + "digest 0.10.7", 3193 + "num-bigint-dig", 3194 + "num-integer", 3195 + "num-traits", 3196 + "pkcs1", 3197 + "pkcs8", 3198 + "rand_core 0.6.4", 3199 + "signature", 3200 + "spki", 3201 + "subtle", 3202 + "zeroize", 3203 + ] 3204 + 3205 + [[package]] 2822 3206 name = "rustc-hash" 2823 3207 version = "2.1.2" 2824 3208 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2881 3265 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 2882 3266 2883 3267 [[package]] 2884 - name = "schemars" 2885 - version = "0.9.0" 2886 - source = "registry+https://github.com/rust-lang/crates.io-index" 2887 - checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" 2888 - dependencies = [ 2889 - "dyn-clone", 2890 - "ref-cast", 2891 - "serde", 2892 - "serde_json", 2893 - ] 2894 - 2895 - [[package]] 2896 - name = "schemars" 2897 - version = "1.2.1" 2898 - source = "registry+https://github.com/rust-lang/crates.io-index" 2899 - checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" 2900 - dependencies = [ 2901 - "dyn-clone", 2902 - "ref-cast", 2903 - "serde", 2904 - "serde_json", 2905 - ] 2906 - 2907 - [[package]] 2908 3268 name = "scoped-tls" 2909 3269 version = "1.0.1" 2910 3270 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2979 3339 dependencies = [ 2980 3340 "proc-macro2", 2981 3341 "quote", 2982 - "syn 2.0.117", 3342 + "syn", 2983 3343 ] 2984 3344 2985 3345 [[package]] 2986 3346 name = "serde_html_form" 2987 - version = "0.2.8" 3347 + version = "0.3.2" 2988 3348 source = "registry+https://github.com/rust-lang/crates.io-index" 2989 - checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" 3349 + checksum = "2acf96b1d9364968fce46ebb548f1c0e1d7eceae27bdff73865d42e6c7369d94" 2990 3350 dependencies = [ 2991 3351 "form_urlencoded", 2992 - "indexmap 2.14.0", 3352 + "indexmap", 2993 3353 "itoa", 2994 - "ryu", 2995 3354 "serde_core", 2996 3355 ] 2997 3356 ··· 3039 3398 dependencies = [ 3040 3399 "proc-macro2", 3041 3400 "quote", 3042 - "syn 2.0.117", 3401 + "syn", 3043 3402 ] 3044 3403 3045 3404 [[package]] ··· 3063 3422 "base64", 3064 3423 "chrono", 3065 3424 "hex", 3066 - "indexmap 1.9.3", 3067 - "indexmap 2.14.0", 3068 - "schemars 0.9.0", 3069 - "schemars 1.2.1", 3070 3425 "serde_core", 3071 3426 "serde_json", 3072 3427 "serde_with_macros", ··· 3082 3437 "darling", 3083 3438 "proc-macro2", 3084 3439 "quote", 3085 - "syn 2.0.117", 3440 + "syn", 3086 3441 ] 3087 3442 3088 3443 [[package]] ··· 3181 3536 checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" 3182 3537 3183 3538 [[package]] 3539 + name = "siphasher" 3540 + version = "1.0.2" 3541 + source = "registry+https://github.com/rust-lang/crates.io-index" 3542 + checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" 3543 + 3544 + [[package]] 3184 3545 name = "slab" 3185 3546 version = "0.4.12" 3186 3547 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3244 3605 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 3245 3606 3246 3607 [[package]] 3247 - name = "static-regular-grammar" 3248 - version = "2.0.2" 3608 + name = "static_assertions" 3609 + version = "1.1.0" 3249 3610 source = "registry+https://github.com/rust-lang/crates.io-index" 3250 - checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" 3611 + checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 3612 + 3613 + [[package]] 3614 + name = "string_cache" 3615 + version = "0.8.9" 3616 + source = "registry+https://github.com/rust-lang/crates.io-index" 3617 + checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 3251 3618 dependencies = [ 3252 - "abnf", 3253 - "btree-range-map", 3254 - "ciborium", 3255 - "hex_fmt", 3256 - "indoc", 3257 - "proc-macro-error", 3258 - "proc-macro2", 3259 - "quote", 3619 + "new_debug_unreachable", 3620 + "parking_lot", 3621 + "phf_shared", 3622 + "precomputed-hash", 3260 3623 "serde", 3261 - "sha2 0.10.9", 3262 - "syn 2.0.117", 3263 - "thiserror 1.0.69", 3264 3624 ] 3265 3625 3266 3626 [[package]] 3267 - name = "static_assertions" 3268 - version = "1.1.0" 3627 + name = "string_cache_codegen" 3628 + version = "0.5.4" 3269 3629 source = "registry+https://github.com/rust-lang/crates.io-index" 3270 - checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 3630 + checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 3631 + dependencies = [ 3632 + "phf_generator", 3633 + "phf_shared", 3634 + "proc-macro2", 3635 + "quote", 3636 + ] 3271 3637 3272 3638 [[package]] 3273 3639 name = "strsim" ··· 3283 3649 3284 3650 [[package]] 3285 3651 name = "syn" 3286 - version = "1.0.109" 3287 - source = "registry+https://github.com/rust-lang/crates.io-index" 3288 - checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 3289 - dependencies = [ 3290 - "proc-macro2", 3291 - "unicode-ident", 3292 - ] 3293 - 3294 - [[package]] 3295 - name = "syn" 3296 3652 version = "2.0.117" 3297 3653 source = "registry+https://github.com/rust-lang/crates.io-index" 3298 3654 checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" ··· 3319 3675 dependencies = [ 3320 3676 "proc-macro2", 3321 3677 "quote", 3322 - "syn 2.0.117", 3678 + "syn", 3323 3679 ] 3324 3680 3325 3681 [[package]] ··· 3364 3720 checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 3365 3721 3366 3722 [[package]] 3723 + name = "tendril" 3724 + version = "0.4.3" 3725 + source = "registry+https://github.com/rust-lang/crates.io-index" 3726 + checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 3727 + dependencies = [ 3728 + "futf", 3729 + "mac", 3730 + "utf-8", 3731 + ] 3732 + 3733 + [[package]] 3367 3734 name = "thiserror" 3368 3735 version = "1.0.69" 3369 3736 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3389 3756 dependencies = [ 3390 3757 "proc-macro2", 3391 3758 "quote", 3392 - "syn 2.0.117", 3759 + "syn", 3393 3760 ] 3394 3761 3395 3762 [[package]] ··· 3400 3767 dependencies = [ 3401 3768 "proc-macro2", 3402 3769 "quote", 3403 - "syn 2.0.117", 3770 + "syn", 3404 3771 ] 3405 3772 3406 3773 [[package]] ··· 3419 3786 checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" 3420 3787 dependencies = [ 3421 3788 "deranged", 3422 - "itoa", 3423 3789 "num-conv", 3424 3790 "powerfmt", 3425 3791 "serde_core", 3426 3792 "time-core", 3427 - "time-macros", 3428 3793 ] 3429 3794 3430 3795 [[package]] ··· 3432 3797 version = "0.1.8" 3433 3798 source = "registry+https://github.com/rust-lang/crates.io-index" 3434 3799 checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 3435 - 3436 - [[package]] 3437 - name = "time-macros" 3438 - version = "0.2.27" 3439 - source = "registry+https://github.com/rust-lang/crates.io-index" 3440 - checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" 3441 - dependencies = [ 3442 - "num-conv", 3443 - "time-core", 3444 - ] 3445 3800 3446 3801 [[package]] 3447 3802 name = "tinystr" ··· 3492 3847 dependencies = [ 3493 3848 "proc-macro2", 3494 3849 "quote", 3495 - "syn 2.0.117", 3850 + "syn", 3496 3851 ] 3497 3852 3498 3853 [[package]] ··· 3534 3889 source = "registry+https://github.com/rust-lang/crates.io-index" 3535 3890 checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" 3536 3891 dependencies = [ 3537 - "indexmap 2.14.0", 3892 + "indexmap", 3538 3893 "toml_datetime", 3539 3894 "toml_parser", 3540 3895 "winnow", ··· 3621 3976 dependencies = [ 3622 3977 "proc-macro2", 3623 3978 "quote", 3624 - "syn 2.0.117", 3979 + "syn", 3625 3980 ] 3626 3981 3627 3982 [[package]] ··· 3682 4037 dependencies = [ 3683 4038 "proc-macro2", 3684 4039 "quote", 3685 - "syn 2.0.117", 4040 + "syn", 3686 4041 ] 3687 4042 3688 4043 [[package]] 4044 + name = "triomphe" 4045 + version = "0.1.15" 4046 + source = "registry+https://github.com/rust-lang/crates.io-index" 4047 + checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" 4048 + 4049 + [[package]] 3689 4050 name = "try-lock" 3690 4051 version = "0.2.5" 3691 4052 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3743 4104 "idna", 3744 4105 "percent-encoding", 3745 4106 "serde", 3746 - "serde_derive", 3747 4107 ] 3748 4108 3749 4109 [[package]] 3750 - name = "urlencoding" 3751 - version = "2.1.3" 4110 + name = "utf-8" 4111 + version = "0.7.6" 3752 4112 source = "registry+https://github.com/rust-lang/crates.io-index" 3753 - checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 4113 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 3754 4114 3755 4115 [[package]] 3756 4116 name = "utf8_iter" ··· 3862 4222 "bumpalo", 3863 4223 "proc-macro2", 3864 4224 "quote", 3865 - "syn 2.0.117", 4225 + "syn", 3866 4226 "wasm-bindgen-shared", 3867 4227 ] 3868 4228 ··· 3892 4252 checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 3893 4253 dependencies = [ 3894 4254 "anyhow", 3895 - "indexmap 2.14.0", 4255 + "indexmap", 3896 4256 "wasm-encoder", 3897 4257 "wasmparser", 3898 4258 ] ··· 3918 4278 dependencies = [ 3919 4279 "bitflags", 3920 4280 "hashbrown 0.15.5", 3921 - "indexmap 2.14.0", 4281 + "indexmap", 3922 4282 "semver", 3923 4283 ] 3924 4284 ··· 3943 4303 ] 3944 4304 3945 4305 [[package]] 4306 + name = "webpage" 4307 + version = "2.0.1" 4308 + source = "registry+https://github.com/rust-lang/crates.io-index" 4309 + checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac" 4310 + dependencies = [ 4311 + "html5ever", 4312 + "markup5ever_rcdom", 4313 + "serde_json", 4314 + "url", 4315 + ] 4316 + 4317 + [[package]] 3946 4318 name = "webpki-roots" 3947 4319 version = "1.0.7" 3948 4320 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4026 4398 dependencies = [ 4027 4399 "proc-macro2", 4028 4400 "quote", 4029 - "syn 2.0.117", 4401 + "syn", 4030 4402 ] 4031 4403 4032 4404 [[package]] ··· 4037 4409 dependencies = [ 4038 4410 "proc-macro2", 4039 4411 "quote", 4040 - "syn 2.0.117", 4412 + "syn", 4041 4413 ] 4042 4414 4043 4415 [[package]] ··· 4293 4665 dependencies = [ 4294 4666 "anyhow", 4295 4667 "heck 0.5.0", 4296 - "indexmap 2.14.0", 4668 + "indexmap", 4297 4669 "prettyplease", 4298 - "syn 2.0.117", 4670 + "syn", 4299 4671 "wasm-metadata", 4300 4672 "wit-bindgen-core", 4301 4673 "wit-component", ··· 4311 4683 "prettyplease", 4312 4684 "proc-macro2", 4313 4685 "quote", 4314 - "syn 2.0.117", 4686 + "syn", 4315 4687 "wit-bindgen-core", 4316 4688 "wit-bindgen-rust", 4317 4689 ] ··· 4324 4696 dependencies = [ 4325 4697 "anyhow", 4326 4698 "bitflags", 4327 - "indexmap 2.14.0", 4699 + "indexmap", 4328 4700 "log", 4329 4701 "serde", 4330 4702 "serde_derive", ··· 4343 4715 dependencies = [ 4344 4716 "anyhow", 4345 4717 "id-arena", 4346 - "indexmap 2.14.0", 4718 + "indexmap", 4347 4719 "log", 4348 4720 "semver", 4349 4721 "serde", ··· 4360 4732 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" 4361 4733 4362 4734 [[package]] 4735 + name = "xml5ever" 4736 + version = "0.18.1" 4737 + source = "registry+https://github.com/rust-lang/crates.io-index" 4738 + checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" 4739 + dependencies = [ 4740 + "log", 4741 + "mac", 4742 + "markup5ever", 4743 + ] 4744 + 4745 + [[package]] 4363 4746 name = "yansi" 4364 4747 version = "1.0.1" 4365 4748 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4384 4767 dependencies = [ 4385 4768 "proc-macro2", 4386 4769 "quote", 4387 - "syn 2.0.117", 4770 + "syn", 4388 4771 "synstructure", 4389 4772 ] 4390 4773 ··· 4405 4788 dependencies = [ 4406 4789 "proc-macro2", 4407 4790 "quote", 4408 - "syn 2.0.117", 4791 + "syn", 4409 4792 ] 4410 4793 4411 4794 [[package]] ··· 4425 4808 dependencies = [ 4426 4809 "proc-macro2", 4427 4810 "quote", 4428 - "syn 2.0.117", 4811 + "syn", 4429 4812 "synstructure", 4430 4813 ] 4431 4814 ··· 4434 4817 version = "1.8.2" 4435 4818 source = "registry+https://github.com/rust-lang/crates.io-index" 4436 4819 checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 4820 + dependencies = [ 4821 + "serde", 4822 + ] 4437 4823 4438 4824 [[package]] 4439 4825 name = "zerotrie" ··· 4465 4851 dependencies = [ 4466 4852 "proc-macro2", 4467 4853 "quote", 4468 - "syn 2.0.117", 4854 + "syn", 4469 4855 ] 4470 4856 4471 4857 [[package]]
+3 -107
Cargo.toml
··· 1 - [package] 2 - name = "porxie" 3 - description = "A correct and efficient ATProto blob proxy for secure content delivery." 4 - authors = ["Blooym"] 5 - repository = "https://codeberg.org/Blooym/porxie" 6 - homepage = "https://codeberg.org/Blooym/porxie/src/branch/main/README.md" 7 - documentation = "https://codeberg.org/Blooym/porxie/src/branch/main/README.md" 8 - license = "AGPL-3.0-or-later" 9 - version = "0.1.2" 10 - edition = "2024" 1 + [workspace] 2 + members = ["crates/porxie", "crates/lexgen"] 3 + resolver = "3" 11 4 12 5 [profile.release] 13 6 lto = true 14 7 codegen-units = 1 15 8 opt-level = 3 16 9 strip = "debuginfo" 17 - 18 - [dependencies] 19 - anyhow = { version = "1.0.102", features = ["std"], default-features = false } 20 - axum = { version = "0.8.8", features = [ 21 - "http1", 22 - "http2", 23 - "json", 24 - "matched-path", 25 - "tokio", 26 - "tower-log", 27 - "tracing", 28 - ], default-features = false } 29 - axum-extra = { version = "0.12.5", features = [ 30 - "typed-header", 31 - "tracing", 32 - ], default-features = false } 33 - bytes = { version = "1.11.1", features = ["std"], default-features = false } 34 - bytesize = { version = "2.3.1", features = ["std"], default-features = false } 35 - cid = { version = "0.11.1", features = ["std"], default-features = false } 36 - clap = { version = "4.5.60", features = [ 37 - "color", 38 - "derive", 39 - "env", 40 - "error-context", 41 - "help", 42 - "std", 43 - "suggestions", 44 - "usage", 45 - ], default-features = false } 46 - dotenvy = { version = "0.15.7", default-features = false } 47 - futures-util = { version = "0.3.32", default-features = false } 48 - humantime = { version = "2.3.0", default-features = false } 49 - infer = { version = "0.19.0", default-features = false, features = ["std"] } 50 - jacquard-common = { version = "0.9.5", default-features = false } 51 - jacquard-identity = { version = "0.9.5", features = ["tracing"] } 52 - jemallocator = "0.5.4" 53 - json-subscriber = { version = "0.2.8", default-features = false, features = [ 54 - "tracing-log", 55 - "env-filter", 56 - ] } 57 - mime = { version = "0.3.17", default-features = false } 58 - moka = { version = "0.12.14", features = [ 59 - "future", 60 - "logging", 61 - ], default-features = false } 62 - multihash-codetable = { version = "0.2.1", features = [ 63 - "sha2", 64 - # "blake3", # if it ever gets added to the spec. 65 - "std", 66 - ], default-features = false } 67 - reqwest = { version = "0.12.28", default-features = false, features = [ 68 - "http2", 69 - "system-proxy", 70 - "stream", 71 - "socks", 72 - "rustls-tls", 73 - "gzip", 74 - "brotli", 75 - "zstd", 76 - "deflate", 77 - ] } 78 - serde = { version = "1.0.228", features = [ 79 - "derive", 80 - "std", 81 - ], default-features = false } 82 - subtle = { version = "2.6", default-features = false, features = ["std"] } 83 - sysinfo = { version = "0.38.4", default-features = false, features = [ 84 - "system", 85 - ] } 86 - thiserror = { version = "2.0.18", default-features = false, features = ["std"] } 87 - tokio = { version = "1.50.0", default-features = false, features = [ 88 - "macros", 89 - "rt-multi-thread", 90 - "signal", 91 - "net", 92 - ] } 93 - tower-http = { version = "0.6.8", features = [ 94 - "catch-panic", 95 - "normalize-path", 96 - "trace", 97 - "timeout", 98 - "tracing", 99 - ], default-features = false } 100 - tracing = { version = "0.1.44", features = [ 101 - "attributes", 102 - "std", 103 - ], default-features = false } 104 - tracing-subscriber = { version = "0.3.22", features = [ 105 - "ansi", 106 - "env-filter", 107 - "fmt", 108 - "parking_lot", 109 - "smallvec", 110 - "std", 111 - "tracing", 112 - "tracing-log", 113 - ], default-features = false }
+8 -5
Dockerfile
··· 12 12 RUN apk add --no-cache --update build-base 13 13 14 14 # Pre-cache dependencies 15 - COPY ["Cargo.toml", "Cargo.lock", "./"] 16 - RUN mkdir src \ 17 - && echo "// Placeholder" > src/lib.rs \ 15 + COPY Cargo.toml Cargo.lock ./ 16 + COPY crates/porxie/Cargo.toml crates/porxie/Cargo.toml 17 + COPY crates/lexgen/Cargo.toml crates/lexgen/Cargo.toml 18 + RUN mkdir -p crates/porxie/src crates/lexgen/src \ 19 + && echo "// Placeholder" > crates/porxie/src/lib.rs \ 20 + && echo "// Placeholder" > crates/lexgen/src/lib.rs \ 18 21 && cargo build --release \ 19 - && rm src/lib.rs 22 + && rm crates/porxie/src/lib.rs crates/lexgen/src/lib.rs 20 23 21 24 # Build 22 - COPY src ./src 25 + COPY crates ./crates 23 26 RUN cargo build --release 24 27 25 28 # -----------
+38 -51
README.md
··· 10 10 11 11 ## Features 12 12 13 - - Blob validation - verifies blob content matches its CID and rejects invalid/tampered content. 14 - - Secure serving - blobs are always served with secure headers to help improve end-user security. 15 - - MIME filtering - detects blob content MIME-types and enforces an optional allowlist of permitted types. 16 - - Policy enforcement - optionally integrate with an external policy service (like an AppView) to control which blobs can be served. 17 - - In-memory cache - configurable in-memory caching for fast repeat access with support for manual cache purging via authenticated HTTP DELETE. 18 - 19 - ## Routes 20 - 21 - - [GET] `/{did}/{cid}`: Resolve and fetch a blob from its origin. 22 - - [DELETE] `/cache/{cid or did}`: Invalidate all valid cache items for a specific blob CID or for an entire user DID. Requires configured bearer auth token. 13 + - Blob validation: verifies blob content matches its CID and rejects invalid/tampered content. 14 + - Secure serving: blobs are always served with secure headers to help improve end-user security. 15 + - MIME filtering: detects blob content MIME-types and enforces an optional allowlist of permitted types. 16 + - Policy enforcement: optionally integrate with an external policy service (like an AppView) to control which blobs can be served. 17 + - In-memory cache: configurable in-memory caching for fast repeat access with support for manual cache purging via authenticated HTTP DELETE. 23 18 24 19 ## Usage 25 20 26 21 > [!NOTE] 27 - > Porxie does not handle TLS, so it should be placed behind a reverse proxy like [Caddy](https://caddyserver.com), [Traefik](https://traefik.io/traefik), or [NGINX](https://nginx.org). Ensure that any intermediaries between Porxie and the client pass through the `Cache-Control`, `Content-Security-Policy` and `Content-Disposition` headers, or otherwise set them securely. 28 - > 29 - > Putting a CDN in front of Porxie is also recommended for better long-term caching and worldwide latency. 22 + > Porxie does not handle TLS, so it should be placed behind a reverse proxy like [Caddy](https://caddyserver.com), [Traefik](https://traefik.io/traefik), or [NGINX](https://nginx.org). It is also recommended to use a dedicated caching layer in-between Porxie and your clients such as Varnish, Cloudflare, or similar. 23 + > 24 + > Please ensure that any intermediary services between Porxie and the client pass through the following headers or set them the same as Porxie does: 25 + > - `Content-Type` (if unmodified by the service) 26 + > - `Cache-Control` 27 + > - `Content-Security-Policy` 28 + > - `Content-Disposition` 29 + > - `X-Content-Type-Options` 30 30 31 31 ### Run: Binary 32 32 ··· 44 44 porxie 45 45 ``` 46 46 47 - ### Run: Docker 48 - 49 - To run Porxie with the Docker CLI and default settings, use the following command: 50 - 51 - ```sh 52 - docker run -d \ 53 - --name porxie \ 54 - --restart unless-stopped \ 55 - -p 6314:6314 \ 56 - blooym/porxie:latest 57 - ``` 58 - 59 47 ### Run: Docker Compose 60 48 61 49 To run Porxie with Docker Compose, you can start with the following `compose.yml` template: ··· 78 66 79 67 To run Porxie with Nix, you can use the [package](https://search.nixos.org/packages?channel=unstable&query=porxie) or [NixOS module](https://search.nixos.org/options?channel=unstable&query=porxie) provided directly in nixpkgs. 80 68 69 + ## Routes 70 + 71 + - [GET] `/{did}/{cid}`: Fetch a blob either from cache or origin. 72 + - [GET] `/xrpc/dev.blooym.porxie.getBlob?did=<did>&cid=<cid>`: XRPC Compatibility alias for the fetch blob endpoint. 73 + - [POST] `/xrpc/dev.blooym.porxie.cache.purgeActor?did=<did>`: Purge all cached items relating to an actor DID. 74 + - [POST] `/xrpc/dev.blooym.porxie.cache.purgeBlob?cid=<cid>`: Purge all cache items relating to a blob CID. 75 + 76 + 81 77 ## Policy Service 82 78 83 79 Porxie can optionally check with an external HTTP service before serving any blob. You build and run this service yourself - Porxie just calls it and acts on the response. This is useful for things like content takedowns or blob allow lists. 84 80 85 - For every incoming request, Porxie sends `GET <policy-service-url>/<did>/<cid>` and expects one of the following responses: 86 - 87 - - **200 OK** - the blob is allowed and will be served. 88 - - **410 Gone** - the blob is restricted and Porxie will refuse to serve it to the client. 81 + For every incoming request, Porxie sends `GET <policy-service-url>/xrpc/dev.blooym.porxie.getBlobPolicy` and expects a response that conforms to the (`lexicon xrpc output`)[lexicons/dev/blooym/porxie/getBlobPolicy.json]. 89 82 90 - Any other status code is treated as an error for now. 91 - 92 - Policy decisions are cached per DID+CID pair, so your service won't be hit on every request. To clear a cached decision early, use the `DELETE /cache/{cid}` endpoint. 83 + Policy decisions are cached per DID+CID pair, so your service won't be hit on every request. The policy cache can be cleared for a blob or actor via the cache clearing xrpc endpoints. 93 84 94 85 By default, Porxie will fail-closed: if the policy service errors, the blob request fails too. This can be changed to fail-open if preferred. 95 86 ··· 111 102 [env: PORXIE_SERVER_ADDRESS=] 112 103 [default: ip:127.0.0.1:6314] 113 104 114 - --server-auth-token <SA_SERVER_AUTH_TOKEN> 115 - Bearer token for authenticating admin requests. 105 + --server-admin-password <SA_SERVER_ADMIN_PASSWORD> 106 + Admin password for authenticating privileged requests. 116 107 117 108 When unset, all authenticated endpoints will reject requests with HTTP 401. 118 109 119 - [env: PORXIE_SERVER_AUTH_TOKEN=] 110 + Authenticated requests always expect the username `admin` as per specification. 111 + 112 + [env: PORXIE_SERVER_ADMIN_TOKEN=] 120 113 ``` 121 114 122 115 ### Blob ··· 139 132 140 133 --blob-max-size <BA_BLOB_MAX_SIZE> 141 134 Maximum blob size that can be fetched and served. 142 - 135 + 143 136 Blobs that exceed this limit will return HTTP 413. 144 - 137 + 145 138 The minimum value is 512kb and the maximum is the system's total memory. 146 139 147 140 [env: PORXIE_BLOB_MAX_SIZE=] ··· 155 148 cleared manually for changes to take effect quickly. 156 149 157 150 [env: PORXIE_BLOB_CACHE_HEADER=] 158 - [default: "public, max-age=604800, must-revalidate, immutable"] 151 + [default: "public, max-age=604800, immutable"] 159 152 160 153 --blob-processing-timeout <BA_BLOB_PROCESSING_TIMEOUT> 161 154 Maximum duration a blob can be processed by this server before aborting ··· 209 202 ``` 210 203 --cache-allocation <CA_CACHE_ALLOCATION> 211 204 Total memory allocation for the internal cache. 212 - 205 + 213 206 Blobs are cached using an LFU policy. The most frequently requested blobs are kept longest when the cache approaches its limit. 214 - 207 + 215 208 For production deployments, a CDN or caching layer in front of this server is recommended for lower latency and better global availability. 216 - 209 + 217 210 The minimum value is 8mb and the maximum is the system's total memory. 218 211 219 212 [env: PORXIE_CACHE_ALLOCATION=] ··· 250 243 --policy-url <PA_POLICY_URL> 251 244 Policy service URL that DID+CID pairs will be checked against. 252 245 253 - Requests are sent as HTTP GET <url>/<did>/<cid>. 254 - 255 - The service is expected to return HTTP 200 (OK) if permitted or HTTP 410 (GONE) if 256 - restricted. 246 + Requests are sent via XRPC tp <url>/xrpc/dev.blooym.porxie.getBlobPolicy?did=<did>&cid=<cid>. 257 247 258 248 [env: PORXIE_POLICY_URL=] 259 249 ··· 266 256 267 257 As pipes are used as a delimiter, they cannot be contained in headers. 268 258 269 - Example (cli): '--policy-request-headers "Authorization: Bearer token" 270 - --policy-request-headers "X-Api-Key: your-key"' 259 + Example (cli): '--policy-request-headers "X-Hello: world" --policy-request-headers "X-Foo: bar"' 271 260 272 - Example (env): 'PORXIE_POLICY_REQUEST_HEADERS="Authorization: Bearer 273 - token|X-Api-Key: your-key"' 261 + Example (env): 'PORXIE_POLICY_REQUEST_HEADERS="X-Hello: world|X-Foo: bar"' 274 262 275 263 [env: PORXIE_POLICY_REQUEST_HEADERS=] 276 264 277 265 --policy-fail-open 278 - Allow requests to proceed if the policy service is unavailable or returns an 279 - unexpected status code. 266 + Allow requests to proceed if the policy service is unavailable. 280 267 281 268 Warning: enabling this means restricted blobs may be served when the policy service 282 269 is unreachable.
+16
crates/lexgen/Cargo.toml
··· 1 + [package] 2 + name = "lexgen" 3 + version = "0.0.0" 4 + edition = "2024" 5 + 6 + [features] 7 + default = ["dev_blooym"] 8 + dev_blooym = [] 9 + 10 + [dependencies] 11 + jacquard-common = { version = "0.11.0", default-features = false } 12 + jacquard-derive = { version = "0.11.0", default-features = false } 13 + jacquard-lexicon = { version = "0.11.1", default-features = false } 14 + miette = { version = "7.6.0", default-features = false } 15 + serde = { version = "1.0.228", default-features = false } 16 + thiserror = { version = "2.0.18", default-features = false }
+45
crates/lexgen/src/builder_types.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + /// Marker type indicating a builder field has been set 7 + pub struct Set<T>(pub T); 8 + impl<T> Set<T> { 9 + /// Extract the inner value 10 + #[inline] 11 + pub fn into_inner(self) -> T { 12 + self.0 13 + } 14 + } 15 + 16 + /// Marker type indicating a builder field has not been set 17 + pub struct Unset; 18 + /// Trait indicating a builder field is set (has a value) 19 + 20 + #[jacquard_common::deps::codegen::rustversion::attr( 21 + since(1.78.0), 22 + diagnostic::on_unimplemented( 23 + message = "the field `{Self}` was not set, but this method requires it to be set", 24 + label = "the field `{Self}` was not set" 25 + ) 26 + )] 27 + pub trait IsSet: private::Sealed {} 28 + /// Trait indicating a builder field is unset (no value yet) 29 + 30 + #[jacquard_common::deps::codegen::rustversion::attr( 31 + since(1.78.0), 32 + diagnostic::on_unimplemented( 33 + message = "the field `{Self}` was already set, but this method requires it to be unset", 34 + label = "the field `{Self}` was already set" 35 + ) 36 + )] 37 + pub trait IsUnset: private::Sealed {} 38 + impl<T> IsSet for Set<T> {} 39 + impl IsUnset for Unset {} 40 + mod private { 41 + /// Sealed trait to prevent external implementations 42 + pub trait Sealed {} 43 + impl<T> Sealed for super::Set<T> {} 44 + impl Sealed for super::Unset {} 45 + }
+6
crates/lexgen/src/dev_blooym.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod porxie;
+8
crates/lexgen/src/dev_blooym/porxie.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod cache; 7 + pub mod get_blob; 8 + pub mod get_blob_policy;
+7
crates/lexgen/src/dev_blooym/porxie/cache.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + pub mod purge_actor; 7 + pub mod purge_blob;
+159
crates/lexgen/src/dev_blooym/porxie/cache/purge_actor.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: dev.blooym.porxie.cache.purgeActor 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[allow(unused_imports)] 9 + use alloc::collections::BTreeMap; 10 + 11 + #[allow(unused_imports)] 12 + use core::marker::PhantomData; 13 + use jacquard_common::types::string::Did; 14 + use jacquard_derive::{IntoStatic, lexicon}; 15 + use serde::{Serialize, Deserialize}; 16 + 17 + #[lexicon] 18 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 19 + #[serde(rename_all = "camelCase")] 20 + pub struct PurgeActor<'a> { 21 + #[serde(borrow)] 22 + pub did: Did<'a>, 23 + } 24 + 25 + 26 + #[lexicon] 27 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 28 + #[serde(rename_all = "camelCase")] 29 + pub struct PurgeActorOutput<'a> {} 30 + /// Response type for dev.blooym.porxie.cache.purgeActor 31 + pub struct PurgeActorResponse; 32 + impl jacquard_common::xrpc::XrpcResp for PurgeActorResponse { 33 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeActor"; 34 + const ENCODING: &'static str = "application/json"; 35 + type Output<'de> = PurgeActorOutput<'de>; 36 + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 37 + } 38 + 39 + impl<'a> jacquard_common::xrpc::XrpcRequest for PurgeActor<'a> { 40 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeActor"; 41 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 42 + "application/json", 43 + ); 44 + type Response = PurgeActorResponse; 45 + } 46 + 47 + /// Endpoint type for dev.blooym.porxie.cache.purgeActor 48 + pub struct PurgeActorRequest; 49 + impl jacquard_common::xrpc::XrpcEndpoint for PurgeActorRequest { 50 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.cache.purgeActor"; 51 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 52 + "application/json", 53 + ); 54 + type Request<'de> = PurgeActor<'de>; 55 + type Response = PurgeActorResponse; 56 + } 57 + 58 + pub mod purge_actor_state { 59 + 60 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 61 + #[allow(unused)] 62 + use ::core::marker::PhantomData; 63 + mod sealed { 64 + pub trait Sealed {} 65 + } 66 + /// State trait tracking which required fields have been set 67 + pub trait State: sealed::Sealed { 68 + type Did; 69 + } 70 + /// Empty state - all required fields are unset 71 + pub struct Empty(()); 72 + impl sealed::Sealed for Empty {} 73 + impl State for Empty { 74 + type Did = Unset; 75 + } 76 + ///State transition - sets the `did` field to Set 77 + pub struct SetDid<S: State = Empty>(PhantomData<fn() -> S>); 78 + impl<S: State> sealed::Sealed for SetDid<S> {} 79 + impl<S: State> State for SetDid<S> { 80 + type Did = Set<members::did>; 81 + } 82 + /// Marker types for field names 83 + #[allow(non_camel_case_types)] 84 + pub mod members { 85 + ///Marker type for the `did` field 86 + pub struct did(()); 87 + } 88 + } 89 + 90 + /// Builder for constructing an instance of this type 91 + pub struct PurgeActorBuilder<'a, S: purge_actor_state::State> { 92 + _state: PhantomData<fn() -> S>, 93 + _fields: (Option<Did<'a>>,), 94 + _lifetime: PhantomData<&'a ()>, 95 + } 96 + 97 + impl<'a> PurgeActor<'a> { 98 + /// Create a new builder for this type 99 + pub fn new() -> PurgeActorBuilder<'a, purge_actor_state::Empty> { 100 + PurgeActorBuilder::new() 101 + } 102 + } 103 + 104 + impl<'a> PurgeActorBuilder<'a, purge_actor_state::Empty> { 105 + /// Create a new builder with all fields unset 106 + pub fn new() -> Self { 107 + PurgeActorBuilder { 108 + _state: PhantomData, 109 + _fields: (None,), 110 + _lifetime: PhantomData, 111 + } 112 + } 113 + } 114 + 115 + impl<'a, S> PurgeActorBuilder<'a, S> 116 + where 117 + S: purge_actor_state::State, 118 + S::Did: purge_actor_state::IsUnset, 119 + { 120 + /// Set the `did` field (required) 121 + pub fn did( 122 + mut self, 123 + value: impl Into<Did<'a>>, 124 + ) -> PurgeActorBuilder<'a, purge_actor_state::SetDid<S>> { 125 + self._fields.0 = Option::Some(value.into()); 126 + PurgeActorBuilder { 127 + _state: PhantomData, 128 + _fields: self._fields, 129 + _lifetime: PhantomData, 130 + } 131 + } 132 + } 133 + 134 + impl<'a, S> PurgeActorBuilder<'a, S> 135 + where 136 + S: purge_actor_state::State, 137 + S::Did: purge_actor_state::IsSet, 138 + { 139 + /// Build the final struct 140 + pub fn build(self) -> PurgeActor<'a> { 141 + PurgeActor { 142 + did: self._fields.0.unwrap(), 143 + extra_data: Default::default(), 144 + } 145 + } 146 + /// Build the final struct with custom extra_data 147 + pub fn build_with_data( 148 + self, 149 + extra_data: BTreeMap< 150 + jacquard_common::deps::smol_str::SmolStr, 151 + jacquard_common::types::value::Data<'a>, 152 + >, 153 + ) -> PurgeActor<'a> { 154 + PurgeActor { 155 + did: self._fields.0.unwrap(), 156 + extra_data: Some(extra_data), 157 + } 158 + } 159 + }
+159
crates/lexgen/src/dev_blooym/porxie/cache/purge_blob.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: dev.blooym.porxie.cache.purgeBlob 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[allow(unused_imports)] 9 + use alloc::collections::BTreeMap; 10 + 11 + #[allow(unused_imports)] 12 + use core::marker::PhantomData; 13 + use jacquard_common::types::string::Cid; 14 + use jacquard_derive::{IntoStatic, lexicon}; 15 + use serde::{Serialize, Deserialize}; 16 + 17 + #[lexicon] 18 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 19 + #[serde(rename_all = "camelCase")] 20 + pub struct PurgeBlob<'a> { 21 + #[serde(borrow)] 22 + pub cid: Cid<'a>, 23 + } 24 + 25 + 26 + #[lexicon] 27 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 28 + #[serde(rename_all = "camelCase")] 29 + pub struct PurgeBlobOutput<'a> {} 30 + /// Response type for dev.blooym.porxie.cache.purgeBlob 31 + pub struct PurgeBlobResponse; 32 + impl jacquard_common::xrpc::XrpcResp for PurgeBlobResponse { 33 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeBlob"; 34 + const ENCODING: &'static str = "application/json"; 35 + type Output<'de> = PurgeBlobOutput<'de>; 36 + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 37 + } 38 + 39 + impl<'a> jacquard_common::xrpc::XrpcRequest for PurgeBlob<'a> { 40 + const NSID: &'static str = "dev.blooym.porxie.cache.purgeBlob"; 41 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 42 + "application/json", 43 + ); 44 + type Response = PurgeBlobResponse; 45 + } 46 + 47 + /// Endpoint type for dev.blooym.porxie.cache.purgeBlob 48 + pub struct PurgeBlobRequest; 49 + impl jacquard_common::xrpc::XrpcEndpoint for PurgeBlobRequest { 50 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.cache.purgeBlob"; 51 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Procedure( 52 + "application/json", 53 + ); 54 + type Request<'de> = PurgeBlob<'de>; 55 + type Response = PurgeBlobResponse; 56 + } 57 + 58 + pub mod purge_blob_state { 59 + 60 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 61 + #[allow(unused)] 62 + use ::core::marker::PhantomData; 63 + mod sealed { 64 + pub trait Sealed {} 65 + } 66 + /// State trait tracking which required fields have been set 67 + pub trait State: sealed::Sealed { 68 + type Cid; 69 + } 70 + /// Empty state - all required fields are unset 71 + pub struct Empty(()); 72 + impl sealed::Sealed for Empty {} 73 + impl State for Empty { 74 + type Cid = Unset; 75 + } 76 + ///State transition - sets the `cid` field to Set 77 + pub struct SetCid<S: State = Empty>(PhantomData<fn() -> S>); 78 + impl<S: State> sealed::Sealed for SetCid<S> {} 79 + impl<S: State> State for SetCid<S> { 80 + type Cid = Set<members::cid>; 81 + } 82 + /// Marker types for field names 83 + #[allow(non_camel_case_types)] 84 + pub mod members { 85 + ///Marker type for the `cid` field 86 + pub struct cid(()); 87 + } 88 + } 89 + 90 + /// Builder for constructing an instance of this type 91 + pub struct PurgeBlobBuilder<'a, S: purge_blob_state::State> { 92 + _state: PhantomData<fn() -> S>, 93 + _fields: (Option<Cid<'a>>,), 94 + _lifetime: PhantomData<&'a ()>, 95 + } 96 + 97 + impl<'a> PurgeBlob<'a> { 98 + /// Create a new builder for this type 99 + pub fn new() -> PurgeBlobBuilder<'a, purge_blob_state::Empty> { 100 + PurgeBlobBuilder::new() 101 + } 102 + } 103 + 104 + impl<'a> PurgeBlobBuilder<'a, purge_blob_state::Empty> { 105 + /// Create a new builder with all fields unset 106 + pub fn new() -> Self { 107 + PurgeBlobBuilder { 108 + _state: PhantomData, 109 + _fields: (None,), 110 + _lifetime: PhantomData, 111 + } 112 + } 113 + } 114 + 115 + impl<'a, S> PurgeBlobBuilder<'a, S> 116 + where 117 + S: purge_blob_state::State, 118 + S::Cid: purge_blob_state::IsUnset, 119 + { 120 + /// Set the `cid` field (required) 121 + pub fn cid( 122 + mut self, 123 + value: impl Into<Cid<'a>>, 124 + ) -> PurgeBlobBuilder<'a, purge_blob_state::SetCid<S>> { 125 + self._fields.0 = Option::Some(value.into()); 126 + PurgeBlobBuilder { 127 + _state: PhantomData, 128 + _fields: self._fields, 129 + _lifetime: PhantomData, 130 + } 131 + } 132 + } 133 + 134 + impl<'a, S> PurgeBlobBuilder<'a, S> 135 + where 136 + S: purge_blob_state::State, 137 + S::Cid: purge_blob_state::IsSet, 138 + { 139 + /// Build the final struct 140 + pub fn build(self) -> PurgeBlob<'a> { 141 + PurgeBlob { 142 + cid: self._fields.0.unwrap(), 143 + extra_data: Default::default(), 144 + } 145 + } 146 + /// Build the final struct with custom extra_data 147 + pub fn build_with_data( 148 + self, 149 + extra_data: BTreeMap< 150 + jacquard_common::deps::smol_str::SmolStr, 151 + jacquard_common::types::value::Data<'a>, 152 + >, 153 + ) -> PurgeBlob<'a> { 154 + PurgeBlob { 155 + cid: self._fields.0.unwrap(), 156 + extra_data: Some(extra_data), 157 + } 158 + } 159 + }
+299
crates/lexgen/src/dev_blooym/porxie/get_blob.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: dev.blooym.porxie.getBlob 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[allow(unused_imports)] 9 + use core::marker::PhantomData; 10 + use jacquard_common::CowStr; 11 + use jacquard_common::deps::bytes::Bytes; 12 + use jacquard_common::types::string::{Did, Cid}; 13 + use jacquard_derive::{IntoStatic, open_union}; 14 + use serde::{Serialize, Deserialize}; 15 + 16 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 17 + #[serde(rename_all = "camelCase")] 18 + pub struct GetBlob<'a> { 19 + #[serde(borrow)] 20 + pub cid: Cid<'a>, 21 + #[serde(borrow)] 22 + pub did: Did<'a>, 23 + } 24 + 25 + 26 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 27 + #[serde(rename_all = "camelCase")] 28 + pub struct GetBlobOutput { 29 + pub body: Bytes, 30 + } 31 + 32 + 33 + #[open_union] 34 + #[derive( 35 + Serialize, 36 + Deserialize, 37 + Debug, 38 + Clone, 39 + PartialEq, 40 + Eq, 41 + thiserror::Error, 42 + miette::Diagnostic, 43 + IntoStatic 44 + )] 45 + 46 + #[serde(tag = "error", content = "message")] 47 + #[serde(bound(deserialize = "'de: 'a"))] 48 + pub enum GetBlobError<'a> { 49 + #[serde(rename = "BlobCidMismatch")] 50 + BlobCidMismatch(Option<CowStr<'a>>), 51 + #[serde(rename = "BlobFetchFailed")] 52 + BlobFetchFailed(Option<CowStr<'a>>), 53 + #[serde(rename = "BlobForbiddenType")] 54 + BlobForbiddenType(Option<CowStr<'a>>), 55 + #[serde(rename = "BlobNotFound")] 56 + BlobNotFound(Option<CowStr<'a>>), 57 + #[serde(rename = "BlobTooLarge")] 58 + BlobTooLarge(Option<CowStr<'a>>), 59 + #[serde(rename = "CannotResolve")] 60 + CannotResolve(Option<CowStr<'a>>), 61 + #[serde(rename = "CidUnsupported")] 62 + CidUnsupported(Option<CowStr<'a>>), 63 + #[serde(rename = "InternalServerError")] 64 + InternalServerError(Option<CowStr<'a>>), 65 + #[serde(rename = "PolicyForbidden")] 66 + PolicyForbidden(Option<CowStr<'a>>), 67 + } 68 + 69 + impl core::fmt::Display for GetBlobError<'_> { 70 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 71 + match self { 72 + Self::BlobCidMismatch(msg) => { 73 + write!(f, "BlobCidMismatch")?; 74 + if let Some(msg) = msg { 75 + write!(f, ": {}", msg)?; 76 + } 77 + Ok(()) 78 + } 79 + Self::BlobFetchFailed(msg) => { 80 + write!(f, "BlobFetchFailed")?; 81 + if let Some(msg) = msg { 82 + write!(f, ": {}", msg)?; 83 + } 84 + Ok(()) 85 + } 86 + Self::BlobForbiddenType(msg) => { 87 + write!(f, "BlobForbiddenType")?; 88 + if let Some(msg) = msg { 89 + write!(f, ": {}", msg)?; 90 + } 91 + Ok(()) 92 + } 93 + Self::BlobNotFound(msg) => { 94 + write!(f, "BlobNotFound")?; 95 + if let Some(msg) = msg { 96 + write!(f, ": {}", msg)?; 97 + } 98 + Ok(()) 99 + } 100 + Self::BlobTooLarge(msg) => { 101 + write!(f, "BlobTooLarge")?; 102 + if let Some(msg) = msg { 103 + write!(f, ": {}", msg)?; 104 + } 105 + Ok(()) 106 + } 107 + Self::CannotResolve(msg) => { 108 + write!(f, "CannotResolve")?; 109 + if let Some(msg) = msg { 110 + write!(f, ": {}", msg)?; 111 + } 112 + Ok(()) 113 + } 114 + Self::CidUnsupported(msg) => { 115 + write!(f, "CidUnsupported")?; 116 + if let Some(msg) = msg { 117 + write!(f, ": {}", msg)?; 118 + } 119 + Ok(()) 120 + } 121 + Self::InternalServerError(msg) => { 122 + write!(f, "InternalServerError")?; 123 + if let Some(msg) = msg { 124 + write!(f, ": {}", msg)?; 125 + } 126 + Ok(()) 127 + } 128 + Self::PolicyForbidden(msg) => { 129 + write!(f, "PolicyForbidden")?; 130 + if let Some(msg) = msg { 131 + write!(f, ": {}", msg)?; 132 + } 133 + Ok(()) 134 + } 135 + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), 136 + } 137 + } 138 + } 139 + 140 + /// Response type for dev.blooym.porxie.getBlob 141 + pub struct GetBlobResponse; 142 + impl jacquard_common::xrpc::XrpcResp for GetBlobResponse { 143 + const NSID: &'static str = "dev.blooym.porxie.getBlob"; 144 + const ENCODING: &'static str = "*/*"; 145 + type Output<'de> = GetBlobOutput; 146 + type Err<'de> = GetBlobError<'de>; 147 + fn encode_output( 148 + output: &Self::Output<'_>, 149 + ) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> { 150 + Ok(output.body.to_vec()) 151 + } 152 + fn decode_output<'de>( 153 + body: &'de [u8], 154 + ) -> Result<Self::Output<'de>, jacquard_common::error::DecodeError> 155 + where 156 + Self::Output<'de>: serde::Deserialize<'de>, 157 + { 158 + Ok(GetBlobOutput { 159 + body: jacquard_common::deps::bytes::Bytes::copy_from_slice(body), 160 + }) 161 + } 162 + } 163 + 164 + impl<'a> jacquard_common::xrpc::XrpcRequest for GetBlob<'a> { 165 + const NSID: &'static str = "dev.blooym.porxie.getBlob"; 166 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 167 + type Response = GetBlobResponse; 168 + } 169 + 170 + /// Endpoint type for dev.blooym.porxie.getBlob 171 + pub struct GetBlobRequest; 172 + impl jacquard_common::xrpc::XrpcEndpoint for GetBlobRequest { 173 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.getBlob"; 174 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 175 + type Request<'de> = GetBlob<'de>; 176 + type Response = GetBlobResponse; 177 + } 178 + 179 + pub mod get_blob_state { 180 + 181 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 182 + #[allow(unused)] 183 + use ::core::marker::PhantomData; 184 + mod sealed { 185 + pub trait Sealed {} 186 + } 187 + /// State trait tracking which required fields have been set 188 + pub trait State: sealed::Sealed { 189 + type Cid; 190 + type Did; 191 + } 192 + /// Empty state - all required fields are unset 193 + pub struct Empty(()); 194 + impl sealed::Sealed for Empty {} 195 + impl State for Empty { 196 + type Cid = Unset; 197 + type Did = Unset; 198 + } 199 + ///State transition - sets the `cid` field to Set 200 + pub struct SetCid<S: State = Empty>(PhantomData<fn() -> S>); 201 + impl<S: State> sealed::Sealed for SetCid<S> {} 202 + impl<S: State> State for SetCid<S> { 203 + type Cid = Set<members::cid>; 204 + type Did = S::Did; 205 + } 206 + ///State transition - sets the `did` field to Set 207 + pub struct SetDid<S: State = Empty>(PhantomData<fn() -> S>); 208 + impl<S: State> sealed::Sealed for SetDid<S> {} 209 + impl<S: State> State for SetDid<S> { 210 + type Cid = S::Cid; 211 + type Did = Set<members::did>; 212 + } 213 + /// Marker types for field names 214 + #[allow(non_camel_case_types)] 215 + pub mod members { 216 + ///Marker type for the `cid` field 217 + pub struct cid(()); 218 + ///Marker type for the `did` field 219 + pub struct did(()); 220 + } 221 + } 222 + 223 + /// Builder for constructing an instance of this type 224 + pub struct GetBlobBuilder<'a, S: get_blob_state::State> { 225 + _state: PhantomData<fn() -> S>, 226 + _fields: (Option<Cid<'a>>, Option<Did<'a>>), 227 + _lifetime: PhantomData<&'a ()>, 228 + } 229 + 230 + impl<'a> GetBlob<'a> { 231 + /// Create a new builder for this type 232 + pub fn new() -> GetBlobBuilder<'a, get_blob_state::Empty> { 233 + GetBlobBuilder::new() 234 + } 235 + } 236 + 237 + impl<'a> GetBlobBuilder<'a, get_blob_state::Empty> { 238 + /// Create a new builder with all fields unset 239 + pub fn new() -> Self { 240 + GetBlobBuilder { 241 + _state: PhantomData, 242 + _fields: (None, None), 243 + _lifetime: PhantomData, 244 + } 245 + } 246 + } 247 + 248 + impl<'a, S> GetBlobBuilder<'a, S> 249 + where 250 + S: get_blob_state::State, 251 + S::Cid: get_blob_state::IsUnset, 252 + { 253 + /// Set the `cid` field (required) 254 + pub fn cid( 255 + mut self, 256 + value: impl Into<Cid<'a>>, 257 + ) -> GetBlobBuilder<'a, get_blob_state::SetCid<S>> { 258 + self._fields.0 = Option::Some(value.into()); 259 + GetBlobBuilder { 260 + _state: PhantomData, 261 + _fields: self._fields, 262 + _lifetime: PhantomData, 263 + } 264 + } 265 + } 266 + 267 + impl<'a, S> GetBlobBuilder<'a, S> 268 + where 269 + S: get_blob_state::State, 270 + S::Did: get_blob_state::IsUnset, 271 + { 272 + /// Set the `did` field (required) 273 + pub fn did( 274 + mut self, 275 + value: impl Into<Did<'a>>, 276 + ) -> GetBlobBuilder<'a, get_blob_state::SetDid<S>> { 277 + self._fields.1 = Option::Some(value.into()); 278 + GetBlobBuilder { 279 + _state: PhantomData, 280 + _fields: self._fields, 281 + _lifetime: PhantomData, 282 + } 283 + } 284 + } 285 + 286 + impl<'a, S> GetBlobBuilder<'a, S> 287 + where 288 + S: get_blob_state::State, 289 + S::Cid: get_blob_state::IsSet, 290 + S::Did: get_blob_state::IsSet, 291 + { 292 + /// Build the final struct 293 + pub fn build(self) -> GetBlob<'a> { 294 + GetBlob { 295 + cid: self._fields.0.unwrap(), 296 + did: self._fields.1.unwrap(), 297 + } 298 + } 299 + }
+318
crates/lexgen/src/dev_blooym/porxie/get_blob_policy.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: dev.blooym.porxie.getBlobPolicy 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[allow(unused_imports)] 9 + use alloc::collections::BTreeMap; 10 + 11 + #[allow(unused_imports)] 12 + use core::marker::PhantomData; 13 + 14 + #[allow(unused_imports)] 15 + use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation; 16 + use jacquard_common::types::string::{Did, Cid}; 17 + use jacquard_derive::{IntoStatic, lexicon}; 18 + use jacquard_lexicon::lexicon::LexiconDoc; 19 + use jacquard_lexicon::schema::LexiconSchema; 20 + 21 + #[allow(unused_imports)] 22 + use jacquard_lexicon::validation::{ConstraintError, ValidationPath}; 23 + use serde::{Serialize, Deserialize}; 24 + use crate::dev_blooym::porxie::get_blob_policy; 25 + /// Blob is allowed to be served. 26 + 27 + #[lexicon] 28 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 29 + #[serde(rename_all = "camelCase")] 30 + pub struct Allowed<'a> {} 31 + /// Blob is not allowed to be served. 32 + 33 + #[lexicon] 34 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Default)] 35 + #[serde(rename_all = "camelCase")] 36 + pub struct Forbidden<'a> {} 37 + 38 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 39 + #[serde(rename_all = "camelCase")] 40 + pub struct GetBlobPolicy<'a> { 41 + #[serde(borrow)] 42 + pub cid: Cid<'a>, 43 + #[serde(borrow)] 44 + pub did: Did<'a>, 45 + } 46 + 47 + 48 + #[lexicon] 49 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 50 + #[serde(rename_all = "camelCase")] 51 + pub struct GetBlobPolicyOutput<'a> { 52 + #[serde(borrow)] 53 + pub policy: GetBlobPolicyOutputPolicy<'a>, 54 + } 55 + 56 + 57 + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)] 58 + #[serde(tag = "$type")] 59 + #[serde(bound(deserialize = "'de: 'a"))] 60 + pub enum GetBlobPolicyOutputPolicy<'a> { 61 + #[serde(rename = "dev.blooym.porxie.getBlobPolicy#allowed")] 62 + Allowed(Box<get_blob_policy::Allowed<'a>>), 63 + #[serde(rename = "dev.blooym.porxie.getBlobPolicy#forbidden")] 64 + Forbidden(Box<get_blob_policy::Forbidden<'a>>), 65 + } 66 + 67 + impl<'a> LexiconSchema for Allowed<'a> { 68 + fn nsid() -> &'static str { 69 + "dev.blooym.porxie.getBlobPolicy" 70 + } 71 + fn def_name() -> &'static str { 72 + "allowed" 73 + } 74 + fn lexicon_doc() -> LexiconDoc<'static> { 75 + lexicon_doc_dev_blooym_porxie_getBlobPolicy() 76 + } 77 + fn validate(&self) -> Result<(), ConstraintError> { 78 + Ok(()) 79 + } 80 + } 81 + 82 + impl<'a> LexiconSchema for Forbidden<'a> { 83 + fn nsid() -> &'static str { 84 + "dev.blooym.porxie.getBlobPolicy" 85 + } 86 + fn def_name() -> &'static str { 87 + "forbidden" 88 + } 89 + fn lexicon_doc() -> LexiconDoc<'static> { 90 + lexicon_doc_dev_blooym_porxie_getBlobPolicy() 91 + } 92 + fn validate(&self) -> Result<(), ConstraintError> { 93 + Ok(()) 94 + } 95 + } 96 + 97 + /// Response type for dev.blooym.porxie.getBlobPolicy 98 + pub struct GetBlobPolicyResponse; 99 + impl jacquard_common::xrpc::XrpcResp for GetBlobPolicyResponse { 100 + const NSID: &'static str = "dev.blooym.porxie.getBlobPolicy"; 101 + const ENCODING: &'static str = "application/json"; 102 + type Output<'de> = GetBlobPolicyOutput<'de>; 103 + type Err<'de> = jacquard_common::xrpc::GenericError<'de>; 104 + } 105 + 106 + impl<'a> jacquard_common::xrpc::XrpcRequest for GetBlobPolicy<'a> { 107 + const NSID: &'static str = "dev.blooym.porxie.getBlobPolicy"; 108 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 109 + type Response = GetBlobPolicyResponse; 110 + } 111 + 112 + /// Endpoint type for dev.blooym.porxie.getBlobPolicy 113 + pub struct GetBlobPolicyRequest; 114 + impl jacquard_common::xrpc::XrpcEndpoint for GetBlobPolicyRequest { 115 + const PATH: &'static str = "/xrpc/dev.blooym.porxie.getBlobPolicy"; 116 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 117 + type Request<'de> = GetBlobPolicy<'de>; 118 + type Response = GetBlobPolicyResponse; 119 + } 120 + 121 + fn lexicon_doc_dev_blooym_porxie_getBlobPolicy() -> LexiconDoc<'static> { 122 + #[allow(unused_imports)] 123 + use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType}; 124 + use jacquard_lexicon::lexicon::*; 125 + use alloc::collections::BTreeMap; 126 + LexiconDoc { 127 + lexicon: Lexicon::Lexicon1, 128 + id: CowStr::new_static("dev.blooym.porxie.getBlobPolicy"), 129 + defs: { 130 + let mut map = BTreeMap::new(); 131 + map.insert( 132 + SmolStr::new_static("allowed"), 133 + LexUserType::Object(LexObject { 134 + description: Some( 135 + CowStr::new_static("Blob is allowed to be served."), 136 + ), 137 + properties: { 138 + #[allow(unused_mut)] 139 + let mut map = BTreeMap::new(); 140 + map 141 + }, 142 + ..Default::default() 143 + }), 144 + ); 145 + map.insert( 146 + SmolStr::new_static("forbidden"), 147 + LexUserType::Object(LexObject { 148 + description: Some( 149 + CowStr::new_static("Blob is not allowed to be served."), 150 + ), 151 + properties: { 152 + #[allow(unused_mut)] 153 + let mut map = BTreeMap::new(); 154 + map 155 + }, 156 + ..Default::default() 157 + }), 158 + ); 159 + map.insert( 160 + SmolStr::new_static("main"), 161 + LexUserType::XrpcQuery(LexXrpcQuery { 162 + parameters: Some( 163 + LexXrpcQueryParameter::Params(LexXrpcParameters { 164 + required: Some( 165 + vec![SmolStr::new_static("did"), SmolStr::new_static("cid")], 166 + ), 167 + properties: { 168 + #[allow(unused_mut)] 169 + let mut map = BTreeMap::new(); 170 + map.insert( 171 + SmolStr::new_static("cid"), 172 + LexXrpcParametersProperty::String(LexString { 173 + format: Some(LexStringFormat::Cid), 174 + ..Default::default() 175 + }), 176 + ); 177 + map.insert( 178 + SmolStr::new_static("did"), 179 + LexXrpcParametersProperty::String(LexString { 180 + format: Some(LexStringFormat::Did), 181 + ..Default::default() 182 + }), 183 + ); 184 + map 185 + }, 186 + ..Default::default() 187 + }), 188 + ), 189 + ..Default::default() 190 + }), 191 + ); 192 + map 193 + }, 194 + ..Default::default() 195 + } 196 + } 197 + 198 + pub mod get_blob_policy_state { 199 + 200 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 201 + #[allow(unused)] 202 + use ::core::marker::PhantomData; 203 + mod sealed { 204 + pub trait Sealed {} 205 + } 206 + /// State trait tracking which required fields have been set 207 + pub trait State: sealed::Sealed { 208 + type Did; 209 + type Cid; 210 + } 211 + /// Empty state - all required fields are unset 212 + pub struct Empty(()); 213 + impl sealed::Sealed for Empty {} 214 + impl State for Empty { 215 + type Did = Unset; 216 + type Cid = Unset; 217 + } 218 + ///State transition - sets the `did` field to Set 219 + pub struct SetDid<S: State = Empty>(PhantomData<fn() -> S>); 220 + impl<S: State> sealed::Sealed for SetDid<S> {} 221 + impl<S: State> State for SetDid<S> { 222 + type Did = Set<members::did>; 223 + type Cid = S::Cid; 224 + } 225 + ///State transition - sets the `cid` field to Set 226 + pub struct SetCid<S: State = Empty>(PhantomData<fn() -> S>); 227 + impl<S: State> sealed::Sealed for SetCid<S> {} 228 + impl<S: State> State for SetCid<S> { 229 + type Did = S::Did; 230 + type Cid = Set<members::cid>; 231 + } 232 + /// Marker types for field names 233 + #[allow(non_camel_case_types)] 234 + pub mod members { 235 + ///Marker type for the `did` field 236 + pub struct did(()); 237 + ///Marker type for the `cid` field 238 + pub struct cid(()); 239 + } 240 + } 241 + 242 + /// Builder for constructing an instance of this type 243 + pub struct GetBlobPolicyBuilder<'a, S: get_blob_policy_state::State> { 244 + _state: PhantomData<fn() -> S>, 245 + _fields: (Option<Cid<'a>>, Option<Did<'a>>), 246 + _lifetime: PhantomData<&'a ()>, 247 + } 248 + 249 + impl<'a> GetBlobPolicy<'a> { 250 + /// Create a new builder for this type 251 + pub fn new() -> GetBlobPolicyBuilder<'a, get_blob_policy_state::Empty> { 252 + GetBlobPolicyBuilder::new() 253 + } 254 + } 255 + 256 + impl<'a> GetBlobPolicyBuilder<'a, get_blob_policy_state::Empty> { 257 + /// Create a new builder with all fields unset 258 + pub fn new() -> Self { 259 + GetBlobPolicyBuilder { 260 + _state: PhantomData, 261 + _fields: (None, None), 262 + _lifetime: PhantomData, 263 + } 264 + } 265 + } 266 + 267 + impl<'a, S> GetBlobPolicyBuilder<'a, S> 268 + where 269 + S: get_blob_policy_state::State, 270 + S::Cid: get_blob_policy_state::IsUnset, 271 + { 272 + /// Set the `cid` field (required) 273 + pub fn cid( 274 + mut self, 275 + value: impl Into<Cid<'a>>, 276 + ) -> GetBlobPolicyBuilder<'a, get_blob_policy_state::SetCid<S>> { 277 + self._fields.0 = Option::Some(value.into()); 278 + GetBlobPolicyBuilder { 279 + _state: PhantomData, 280 + _fields: self._fields, 281 + _lifetime: PhantomData, 282 + } 283 + } 284 + } 285 + 286 + impl<'a, S> GetBlobPolicyBuilder<'a, S> 287 + where 288 + S: get_blob_policy_state::State, 289 + S::Did: get_blob_policy_state::IsUnset, 290 + { 291 + /// Set the `did` field (required) 292 + pub fn did( 293 + mut self, 294 + value: impl Into<Did<'a>>, 295 + ) -> GetBlobPolicyBuilder<'a, get_blob_policy_state::SetDid<S>> { 296 + self._fields.1 = Option::Some(value.into()); 297 + GetBlobPolicyBuilder { 298 + _state: PhantomData, 299 + _fields: self._fields, 300 + _lifetime: PhantomData, 301 + } 302 + } 303 + } 304 + 305 + impl<'a, S> GetBlobPolicyBuilder<'a, S> 306 + where 307 + S: get_blob_policy_state::State, 308 + S::Did: get_blob_policy_state::IsSet, 309 + S::Cid: get_blob_policy_state::IsSet, 310 + { 311 + /// Build the final struct 312 + pub fn build(self) -> GetBlobPolicy<'a> { 313 + GetBlobPolicy { 314 + cid: self._fields.0.unwrap(), 315 + did: self._fields.1.unwrap(), 316 + } 317 + } 318 + }
+11
crates/lexgen/src/lib.rs
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // This file was automatically generated from Lexicon schemas. 4 + // Any manual changes will be overwritten on the next regeneration. 5 + 6 + extern crate alloc; 7 + pub mod builder_types; 8 + 9 + 10 + #[cfg(feature = "dev_blooym")] 11 + pub mod dev_blooym;
+113
crates/porxie/Cargo.toml
··· 1 + [package] 2 + name = "porxie" 3 + description = "A correct and efficient ATProto blob proxy for secure content delivery." 4 + authors = ["Blooym"] 5 + repository = "https://codeberg.org/Blooym/porxie" 6 + homepage = "https://codeberg.org/Blooym/porxie/src/branch/main/README.md" 7 + documentation = "https://codeberg.org/Blooym/porxie/src/branch/main/README.md" 8 + license = "AGPL-3.0-or-later" 9 + version = "0.1.2" 10 + edition = "2024" 11 + 12 + [dependencies] 13 + anyhow = { version = "1.0.102", features = ["std"], default-features = false } 14 + axum = { version = "0.8.8", features = [ 15 + "http1", 16 + "http2", 17 + "json", 18 + "matched-path", 19 + "tokio", 20 + "tower-log", 21 + "tracing", 22 + ], default-features = false } 23 + axum-extra = { version = "0.12.5", features = [ 24 + "typed-header", 25 + "tracing", 26 + ], default-features = false } 27 + bytes = { version = "1.11.1", features = ["std"], default-features = false } 28 + bytesize = { version = "2.3.1", features = ["std"], default-features = false } 29 + cid = { version = "0.11.1", features = ["std"], default-features = false } 30 + clap = { version = "4.5.60", features = [ 31 + "color", 32 + "derive", 33 + "env", 34 + "error-context", 35 + "help", 36 + "std", 37 + "suggestions", 38 + "usage", 39 + ], default-features = false } 40 + dotenvy = { version = "0.15.7", default-features = false } 41 + futures-util = { version = "0.3.32", default-features = false } 42 + humantime = { version = "2.3.0", default-features = false } 43 + infer = { version = "0.19.0", default-features = false, features = ["std"] } 44 + jacquard-axum = { version = "0.11.0", default-features = false, features = [ 45 + "tracing", 46 + ] } 47 + jacquard-common = { version = "0.11.0", default-features = false } 48 + jacquard-identity = { version = "0.11.0", features = ["tracing"] } 49 + jemallocator = "0.5.4" 50 + json-subscriber = { version = "0.2.8", default-features = false, features = [ 51 + "tracing-log", 52 + "env-filter", 53 + ] } 54 + lexgen = { path = "../lexgen" } 55 + mime = { version = "0.3.17", default-features = false } 56 + moka = { version = "0.12.14", features = [ 57 + "future", 58 + "logging", 59 + ], default-features = false } 60 + multihash-codetable = { version = "0.2.1", features = [ 61 + "sha2", 62 + # "blake3", # if it ever gets added to the spec. 63 + "std", 64 + ], default-features = false } 65 + reqwest = { version = "0.12.28", default-features = false, features = [ 66 + "http2", 67 + "system-proxy", 68 + "stream", 69 + "socks", 70 + "rustls-tls", 71 + "gzip", 72 + "brotli", 73 + "zstd", 74 + "deflate", 75 + ] } 76 + serde = { version = "1.0.228", features = [ 77 + "derive", 78 + "std", 79 + ], default-features = false } 80 + serde_json = "1.0.149" 81 + subtle = { version = "2.6", default-features = false, features = ["std"] } 82 + sysinfo = { version = "0.38.4", default-features = false, features = [ 83 + "system", 84 + ] } 85 + thiserror = { version = "2.0.18", default-features = false, features = ["std"] } 86 + tokio = { version = "1.50.0", default-features = false, features = [ 87 + "macros", 88 + "rt-multi-thread", 89 + "signal", 90 + "net", 91 + ] } 92 + tower-http = { version = "0.6.8", features = [ 93 + "catch-panic", 94 + "normalize-path", 95 + "trace", 96 + "timeout", 97 + "tracing", 98 + "cors", 99 + ], default-features = false } 100 + tracing = { version = "0.1.44", features = [ 101 + "attributes", 102 + "std", 103 + ], default-features = false } 104 + tracing-subscriber = { version = "0.3.22", features = [ 105 + "ansi", 106 + "env-filter", 107 + "fmt", 108 + "parking_lot", 109 + "smallvec", 110 + "std", 111 + "tracing", 112 + "tracing-log", 113 + ], default-features = false }
+55
crates/porxie/src/server/extractors/admin_xrpc_auth.rs
··· 1 + use crate::server::ServerState; 2 + use axum::{ 3 + extract::FromRequestParts, 4 + http::{StatusCode, request::Parts}, 5 + }; 6 + use axum_extra::{ 7 + TypedHeader, 8 + headers::{Authorization, authorization::Basic}, 9 + }; 10 + use std::sync::Arc; 11 + use subtle::ConstantTimeEq; 12 + 13 + /// Enforce a valid admin XRPC authentication and reject the request if invalid. 14 + /// 15 + /// Uses the password configured the Router's [`AppState`]. 16 + /// 17 + /// Specification: <https://atproto.com/specs/xrpc#admin-token-temporary-specification>. 18 + pub struct AdminXrpcAuth; 19 + 20 + impl FromRequestParts<Arc<ServerState>> for AdminXrpcAuth { 21 + type Rejection = StatusCode; 22 + 23 + async fn from_request_parts( 24 + parts: &mut Parts, 25 + state: &Arc<ServerState>, 26 + ) -> Result<Self, Self::Rejection> { 27 + let Ok(basic_auth) = 28 + TypedHeader::<Authorization<Basic>>::from_request_parts(parts, state).await 29 + else { 30 + return Err(StatusCode::UNAUTHORIZED); 31 + }; 32 + 33 + // Enforce admin as username as per specification. 34 + if basic_auth.username() != "admin" { 35 + return Err(StatusCode::UNAUTHORIZED); 36 + } 37 + 38 + // Check password with a constant time check. 39 + if !state 40 + .admin_password 41 + .as_ref() 42 + .map(|expected| { 43 + expected 44 + .as_bytes() 45 + .ct_eq(basic_auth.password().as_bytes()) 46 + .into() 47 + }) 48 + .unwrap_or(false) 49 + { 50 + return Err(StatusCode::UNAUTHORIZED); 51 + } 52 + 53 + Ok(AdminXrpcAuth) 54 + } 55 + }
+3
crates/porxie/src/server/extractors/mod.rs
··· 1 + mod admin_xrpc_auth; 2 + 3 + pub use admin_xrpc_auth::AdminXrpcAuth;
+17
crates/porxie/src/server/middlewares/mod.rs
··· 1 + use axum::{ 2 + extract::Request, 3 + http::{HeaderValue, header}, 4 + middleware::Next, 5 + response::Response, 6 + }; 7 + 8 + pub async fn server_headers_middleware(req: Request, next: Next) -> Response { 9 + let mut res = next.run(req).await; 10 + let res_headers = res.headers_mut(); 11 + res_headers.insert( 12 + header::SERVER, 13 + const { HeaderValue::from_static(env!("CARGO_PKG_NAME")) }, 14 + ); 15 + res_headers.insert("X-Robots-Tag", const { HeaderValue::from_static("none") }); 16 + res 17 + }
+165
crates/porxie/src/server/mod.rs
··· 1 + mod extractors; 2 + mod middlewares; 3 + mod routes; 4 + mod socket_address; 5 + 6 + pub use socket_address::SocketAddress; 7 + 8 + use crate::{ 9 + blob_service::BlobService, 10 + identity_service::IdentityService, 11 + policy_client::PolicyClient, 12 + server::{ 13 + middlewares::server_headers_middleware, 14 + routes::{ 15 + get_blob_handler, get_index_handler, 16 + xrpc::{ 17 + dev_blooym::porxie::{ 18 + cache::{xrpc_cache_purge_actor_handler, xrpc_cache_purge_blob_handler}, 19 + xrpc_compat_get_blob_handler, 20 + }, 21 + xrpc_fallback_handler, xrpc_get_health_handler, 22 + }, 23 + }, 24 + }, 25 + }; 26 + use anyhow::Context; 27 + use axum::{ 28 + Router, 29 + http::HeaderValue, 30 + middleware::{self as axum_middleware}, 31 + routing::{any, get, post}, 32 + }; 33 + use core::{num::NonZeroU64, time::Duration}; 34 + use mime::Mime; 35 + use reqwest::StatusCode; 36 + use std::sync::Arc; 37 + use tower_http::{ 38 + catch_panic::CatchPanicLayer, 39 + cors::CorsLayer, 40 + normalize_path::NormalizePathLayer, 41 + timeout::TimeoutLayer, 42 + trace::{self, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer}, 43 + }; 44 + use tracing::Level; 45 + 46 + struct ServerState { 47 + admin_password: Option<String>, 48 + allowed_mimetypes: Vec<Mime>, 49 + blob_service: BlobService, 50 + cache_control_header: HeaderValue, 51 + identity_service: IdentityService, 52 + max_blob_size: NonZeroU64, 53 + policy_client: Option<PolicyClient>, 54 + policy_fail_open: bool, 55 + } 56 + 57 + pub struct PorxieServerOptions { 58 + pub admin_password: Option<String>, 59 + pub allowed_mimetypes: Vec<Mime>, 60 + pub blob_processing_timeout: Duration, 61 + pub blob_service: BlobService, 62 + pub cache_control_header: HeaderValue, 63 + pub identity_service: IdentityService, 64 + pub max_blob_size: NonZeroU64, 65 + pub policy_client: Option<PolicyClient>, 66 + pub policy_fail_open: bool, 67 + } 68 + 69 + pub struct PorxieServer { 70 + router: Router, 71 + } 72 + 73 + impl PorxieServer { 74 + /// Create a new Porxie server. 75 + pub fn new(options: PorxieServerOptions) -> Self { 76 + let router = Router::new() 77 + .route("/", get(get_index_handler)) 78 + .route( 79 + "/{did}/{cid}", 80 + get(get_blob_handler).layer(TimeoutLayer::with_status_code( 81 + StatusCode::REQUEST_TIMEOUT, 82 + options.blob_processing_timeout, 83 + )), 84 + ) 85 + .nest( 86 + "/xrpc", 87 + Router::new() 88 + .route("/_health", get(xrpc_get_health_handler)) 89 + .route( 90 + "/dev.blooym.porxie.getBlob", 91 + get(xrpc_compat_get_blob_handler).layer(TimeoutLayer::with_status_code( 92 + StatusCode::REQUEST_TIMEOUT, 93 + options.blob_processing_timeout, 94 + )), 95 + ) 96 + .route( 97 + "/dev.blooym.porxie.cache.purgeActor", 98 + post(xrpc_cache_purge_actor_handler), 99 + ) 100 + .route( 101 + "/dev.blooym.porxie.cache.purgeBlob", 102 + post(xrpc_cache_purge_blob_handler), 103 + ) 104 + // Ensure /xrpc/... routes don't fall through elsewhere. 105 + .route("/{rest}", any(xrpc_fallback_handler)), 106 + ) 107 + .layer(CatchPanicLayer::new()) 108 + .layer(NormalizePathLayer::trim_trailing_slash()) 109 + .layer(axum_middleware::from_fn(server_headers_middleware)) 110 + .layer(CorsLayer::permissive()) 111 + .layer( 112 + TraceLayer::new_for_http() 113 + .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) 114 + .on_request(DefaultOnRequest::default().level(Level::DEBUG)) 115 + .on_response(DefaultOnResponse::default().level(Level::INFO)) 116 + .on_failure(DefaultOnFailure::default().level(Level::ERROR)), 117 + ) 118 + .with_state(Arc::new(ServerState { 119 + admin_password: options.admin_password, 120 + allowed_mimetypes: options.allowed_mimetypes, 121 + blob_service: options.blob_service, 122 + cache_control_header: options.cache_control_header, 123 + identity_service: options.identity_service, 124 + max_blob_size: options.max_blob_size, 125 + policy_client: options.policy_client, 126 + policy_fail_open: options.policy_fail_open, 127 + })); 128 + 129 + Self { router } 130 + } 131 + 132 + /// Start server listener on specified address. 133 + pub async fn start<F: Future<Output = ()> + Send + 'static>( 134 + self, 135 + address: SocketAddress, 136 + shutdown_signal: F, 137 + ) -> anyhow::Result<()> { 138 + match address { 139 + SocketAddress::Ip(ip) => { 140 + let listener = tokio::net::TcpListener::bind(ip) 141 + .await 142 + .context("failed to bind tcp listener")?; 143 + tracing::info!("server listening on http://{ip}"); 144 + axum::serve(listener, self.router) 145 + .with_graceful_shutdown(shutdown_signal) 146 + .await?; 147 + Ok(()) 148 + } 149 + #[cfg(unix)] 150 + SocketAddress::Unix(path) => { 151 + use anyhow::Context; 152 + 153 + let _ = std::fs::remove_file(&path); 154 + let listener = tokio::net::UnixListener::bind(&path) 155 + .context("failed to bind unix listener")?; 156 + tracing::info!("server listening on unix:{}", path.display()); 157 + axum::serve(listener, self.router) 158 + .with_graceful_shutdown(shutdown_signal) 159 + .await?; 160 + let _ = std::fs::remove_file(&path); 161 + Ok(()) 162 + } 163 + } 164 + } 165 + }
+19
crates/porxie/src/server/routes/index.rs
··· 1 + pub async fn get_index_handler() -> &'static str { 2 + r#" 3 + _____ _ 4 + | __ \ (_) 5 + | |__) |__ _ ____ ___ ___ 6 + | ___/ _ \| '__\ \/ / |/ _ \ 7 + | | | (_) | | > <| | __/ 8 + |_| \___/|_| /_/\_\_|\___| 9 + 10 + 11 + A correct and efficient ATProto blob proxy for secure content delivery. 12 + 13 + Most API routes are under /xrpc/ 14 + 15 + Links: 16 + - Repo: https://codeberg.org/Blooym/porxie 17 + - ATProto: https://atproto.com 18 + "# 19 + }
+16
crates/porxie/src/server/routes/mod.rs
··· 1 + mod blob; 2 + mod index; 3 + pub mod xrpc; 4 + 5 + pub use blob::get_blob_handler; 6 + pub use index::get_index_handler; 7 + 8 + /// Cache-Control header value indicating the response cannot be cached. 9 + const CACHE_CONTROL_NOCACHE_VALUE: &str = "must-understand, no-store"; 10 + 11 + /// An xrpc-compatiable error response. 12 + #[derive(serde::Serialize)] 13 + pub struct XrpcErrorResponse { 14 + error: &'static str, 15 + message: Option<&'static str>, 16 + }
+1
crates/porxie/src/server/routes/xrpc/dev_blooym/mod.rs
··· 1 + pub mod porxie;
+5
crates/porxie/src/server/routes/xrpc/dev_blooym/porxie/cache/mod.rs
··· 1 + mod purge_actor; 2 + mod purge_blob; 3 + 4 + pub use purge_actor::xrpc_cache_purge_actor_handler; 5 + pub use purge_blob::xrpc_cache_purge_blob_handler;
+28
crates/porxie/src/server/routes/xrpc/dev_blooym/porxie/cache/purge_actor.rs
··· 1 + use crate::server::{ServerState, extractors::AdminXrpcAuth}; 2 + use axum::extract::State; 3 + use jacquard_axum::ExtractXrpc; 4 + use lexgen::dev_blooym::porxie::cache::purge_actor::PurgeActorRequest; 5 + use reqwest::StatusCode; 6 + use std::sync::Arc; 7 + 8 + pub async fn xrpc_cache_purge_actor_handler( 9 + _auth: AdminXrpcAuth, 10 + State(state): State<Arc<ServerState>>, 11 + ExtractXrpc(request): ExtractXrpc<PurgeActorRequest>, 12 + ) -> StatusCode { 13 + if let Some(ref policy_client) = state.policy_client { 14 + policy_client.invalidate_cache_entries({ 15 + let did = request.did.clone(); 16 + move |k, _v| k.0 == did 17 + }) 18 + } 19 + state 20 + .identity_service 21 + .invalidate_did_cache(&request.did) 22 + .await; 23 + state 24 + .blob_service 25 + .invalidate_blob_ownership_cache_entries(move |k, _v| k.1 == request.did); 26 + 27 + StatusCode::OK 28 + }
+51
crates/porxie/src/server/routes/xrpc/dev_blooym/porxie/cache/purge_blob.rs
··· 1 + use crate::{ 2 + server::{ 3 + ServerState, 4 + extractors::AdminXrpcAuth, 5 + routes::{CACHE_CONTROL_NOCACHE_VALUE, XrpcErrorResponse}, 6 + }, 7 + types::blob_cid::BlobCid, 8 + }; 9 + use axum::{ 10 + Json, 11 + extract::State, 12 + http::{HeaderName, header}, 13 + }; 14 + use jacquard_axum::ExtractXrpc; 15 + use lexgen::dev_blooym::porxie::cache::purge_blob::PurgeBlobRequest; 16 + use reqwest::StatusCode; 17 + use std::sync::Arc; 18 + 19 + pub async fn xrpc_cache_purge_blob_handler( 20 + _auth: AdminXrpcAuth, 21 + State(state): State<Arc<ServerState>>, 22 + ExtractXrpc(request): ExtractXrpc<PurgeBlobRequest>, 23 + ) -> Result< 24 + StatusCode, 25 + ( 26 + StatusCode, 27 + [(HeaderName, &'static str); 1], 28 + Json<XrpcErrorResponse>, 29 + ), 30 + > { 31 + let cid = BlobCid::try_from(request.cid.as_str()).map_err(|_| { 32 + ( 33 + StatusCode::UNPROCESSABLE_ENTITY, 34 + [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 35 + Json(XrpcErrorResponse { 36 + error: "MalformedCid", 37 + message: Some("Invalid or unprocessable CID"), 38 + }), 39 + ) 40 + })?; 41 + 42 + if let Some(ref policy_client) = state.policy_client { 43 + policy_client.invalidate_cache_entries(move |k, _v| k.1 == cid) 44 + } 45 + state.blob_service.invalidate_blob_cache_entry(&cid).await; 46 + state 47 + .blob_service 48 + .invalidate_blob_ownership_cache_entries(move |k, _v| k.0 == cid); 49 + 50 + Ok(StatusCode::OK) 51 + }
+22
crates/porxie/src/server/routes/xrpc/dev_blooym/porxie/get_blob.rs
··· 1 + use crate::server::{ServerState, routes::get_blob_handler}; 2 + use axum::{ 3 + extract::{Path, State}, 4 + response::IntoResponse, 5 + }; 6 + use jacquard_axum::ExtractXrpc; 7 + use lexgen::dev_blooym::porxie::get_blob::GetBlobRequest; 8 + use std::sync::Arc; 9 + 10 + /// Compatibility layer that converts the xrpc call into a 11 + /// regular get blob request. May become the primary method 12 + /// in the future. 13 + pub async fn xrpc_compat_get_blob_handler( 14 + state: State<Arc<ServerState>>, 15 + ExtractXrpc(request): ExtractXrpc<GetBlobRequest>, 16 + ) -> impl IntoResponse { 17 + get_blob_handler( 18 + Path((request.did.to_string(), request.cid.to_string())), 19 + state, 20 + ) 21 + .await 22 + }
+4
crates/porxie/src/server/routes/xrpc/dev_blooym/porxie/mod.rs
··· 1 + pub mod cache; 2 + mod get_blob; 3 + 4 + pub use get_blob::xrpc_compat_get_blob_handler;
+8
crates/porxie/src/server/routes/xrpc/mod.rs
··· 1 + pub mod dev_blooym; 2 + mod health; 3 + 4 + pub use health::xrpc_get_health_handler; 5 + 6 + pub async fn xrpc_fallback_handler() -> axum::http::StatusCode { 7 + axum::http::StatusCode::NOT_IMPLEMENTED 8 + }
+75
crates/porxie/src/server/socket_address.rs
··· 1 + use anyhow::bail; 2 + use core::str::FromStr; 3 + 4 + #[derive(Debug, Clone)] 5 + pub enum SocketAddress { 6 + /// An IP socket address. 7 + Ip(std::net::SocketAddr), 8 + 9 + /// A UNIX socket path. 10 + #[cfg(unix)] 11 + Unix(std::path::PathBuf), 12 + } 13 + 14 + impl FromStr for SocketAddress { 15 + type Err = anyhow::Error; 16 + 17 + fn from_str(s: &str) -> Result<Self, Self::Err> { 18 + #[cfg(unix)] 19 + if let Some(path) = s.strip_prefix("unix:") { 20 + if path.ends_with("/") { 21 + bail!("unix socket path cannot be a directory") 22 + } 23 + return Ok(SocketAddress::Unix(std::path::PathBuf::from(path))); 24 + } 25 + if let Some(ip) = s.strip_prefix("ip:") { 26 + return Ok(ip.parse::<std::net::SocketAddr>().map(SocketAddress::Ip)?); 27 + } 28 + 29 + #[cfg(unix)] 30 + bail!("unknown address binding type, expected 'ip:<addr>' or 'unix:<path>'".to_string(),); 31 + #[cfg(not(unix))] 32 + bail!("unknown address binding type, expected 'ip:<addr>'".to_string()); 33 + } 34 + } 35 + 36 + impl From<std::net::SocketAddr> for SocketAddress { 37 + fn from(value: std::net::SocketAddr) -> Self { 38 + Self::Ip(value) 39 + } 40 + } 41 + 42 + #[cfg(unix)] 43 + impl From<std::path::PathBuf> for SocketAddress { 44 + fn from(value: std::path::PathBuf) -> Self { 45 + Self::Unix(value) 46 + } 47 + } 48 + 49 + #[cfg(unix)] 50 + impl From<&std::path::Path> for SocketAddress { 51 + fn from(value: &std::path::Path) -> Self { 52 + Self::Unix(value.to_path_buf()) 53 + } 54 + } 55 + 56 + #[cfg(test)] 57 + mod tests { 58 + use crate::server::socket_address::SocketAddress; 59 + use core::str::FromStr; 60 + 61 + #[test] 62 + fn parse_ip_address() { 63 + assert!(SocketAddress::from_str("ip:127.0.0.1:3000").is_ok()); 64 + assert!(SocketAddress::from_str("ip:1.1.1.1:80").is_ok()); 65 + assert!(SocketAddress::from_str("ip:1.1.1.1").is_err()); 66 + assert!(SocketAddress::from_str("ip:1.1.1.1.2").is_err()); 67 + } 68 + 69 + #[test] 70 + #[cfg(unix)] 71 + fn parse_unix_path() { 72 + assert!(SocketAddress::from_str("unix:/run/porxie/porxie.sock").is_ok()); 73 + assert!(SocketAddress::from_str("unix:/just/a/directory/").is_err()); 74 + } 75 + }
+3 -3
flake.lock
··· 2 2 "nodes": { 3 3 "nixpkgs": { 4 4 "locked": { 5 - "lastModified": 1776329215, 6 - "narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", 5 + "lastModified": 1777641297, 6 + "narHash": "sha256-WNGcmeOZ8Tr9dq6ztCspYbzWFswr2mPebM9LpsfGxPk=", 7 7 "owner": "nixos", 8 8 "repo": "nixpkgs", 9 - "rev": "b86751bc4085f48661017fa226dee99fab6c651b", 9 + "rev": "c6d65881c5624c9cae5ea6cedef24699b0c0a4c0", 10 10 "type": "github" 11 11 }, 12 12 "original": {
+32
lexicons/dev/blooym/porxie/cache/purgeActor.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.blooym.porxie.cache.purgeActor", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Clear all cache entries relating to the specified actor. Requires admin authentication.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "did" 14 + ], 15 + "properties": { 16 + "did": { 17 + "type": "string", 18 + "format": "did" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "properties": {} 28 + } 29 + } 30 + } 31 + } 32 + }
+32
lexicons/dev/blooym/porxie/cache/purgeBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.blooym.porxie.cache.purgeBlob", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Clear all cache entries relating to the specified blob. Requires admin authentication.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": [ 13 + "cid" 14 + ], 15 + "properties": { 16 + "cid": { 17 + "type": "string", 18 + "format": "cid" 19 + } 20 + } 21 + } 22 + }, 23 + "output": { 24 + "encoding": "application/json", 25 + "schema": { 26 + "type": "object", 27 + "properties": {} 28 + } 29 + } 30 + } 31 + } 32 + }
+59
lexicons/dev/blooym/porxie/getBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.blooym.porxie.getBlob", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Fetch and validate a blob from the origin via the service. May implement caching depending on implementation.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "did", 12 + "cid" 13 + ], 14 + "properties": { 15 + "did": { 16 + "type": "string", 17 + "format": "did" 18 + }, 19 + "cid": { 20 + "type": "string", 21 + "format": "cid" 22 + } 23 + } 24 + }, 25 + "output": { 26 + "encoding": "*/*" 27 + }, 28 + "errors": [ 29 + { 30 + "name": "BlobCidMismatch" 31 + }, 32 + { 33 + "name": "BlobFetchFailed" 34 + }, 35 + { 36 + "name": "BlobForbiddenType" 37 + }, 38 + { 39 + "name": "BlobNotFound" 40 + }, 41 + { 42 + "name": "BlobTooLarge" 43 + }, 44 + { 45 + "name": "CannotResolve" 46 + }, 47 + { 48 + "name": "CidUnsupported" 49 + }, 50 + { 51 + "name": "InternalServerError" 52 + }, 53 + { 54 + "name": "PolicyForbidden" 55 + } 56 + ] 57 + } 58 + } 59 + }
+56
lexicons/dev/blooym/porxie/getBlobPolicy.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "dev.blooym.porxie.getBlobPolicy", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Returns the policy status of the given actor + blob combination.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "did", 12 + "cid" 13 + ], 14 + "properties": { 15 + "did": { 16 + "type": "string", 17 + "format": "did" 18 + }, 19 + "cid": { 20 + "type": "string", 21 + "format": "cid" 22 + } 23 + } 24 + }, 25 + "output": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "required": [ 30 + "policy" 31 + ], 32 + "properties": { 33 + "policy": { 34 + "type": "union", 35 + "closed": true, 36 + "refs": [ 37 + "#allowed", 38 + "#forbidden" 39 + ] 40 + } 41 + } 42 + } 43 + } 44 + }, 45 + "allowed": { 46 + "type": "object", 47 + "description": "Blob is allowed to be served.", 48 + "properties": {} 49 + }, 50 + "forbidden": { 51 + "type": "object", 52 + "description": "Blob is not allowed to be served.", 53 + "properties": {} 54 + } 55 + } 56 + }
+16 -17
src/blob_service.rs crates/porxie/src/blob_service.rs
··· 6 6 }; 7 7 use bytes::Bytes; 8 8 use cid::Cid; 9 + use core::{num::NonZeroU64, time::Duration}; 9 10 use jacquard_common::types::did::Did; 10 11 use mime::Mime; 11 12 use moka::{future::Cache as MokaCache, policy::EvictionPolicy}; 12 13 use multihash_codetable::{Code, MultihashDigest}; 13 14 use reqwest::{StatusCode, header, header::HeaderValue}; 14 - use std::{num::NonZeroU64, sync::Arc, time::Duration}; 15 + use std::sync::Arc; 15 16 use thiserror::Error; 16 17 use tracing::instrument; 17 18 ··· 172 173 } 173 174 }; 174 175 175 - let validated_bytes = { 176 + let bytes = { 176 177 let response = self.http_client.get(blob_url).send().await.map_err(|err| { 177 178 tracing::warn!("failed to request blob from origin: {err:?}"); 178 179 BlobDownloadError::FetchFailure ··· 211 212 // 212 213 // This operation is done via spawn_blocking as creating the digest will block 213 214 // this task's executor from switching to other tasks for as long it runs. 215 + // 216 + // Passes the bytes as a return value instead of incrementing the reference count. 214 217 tokio::task::spawn_blocking({ 215 - let bytes = bytes.clone(); 216 218 let cid = *cid; 217 219 move || { 218 220 // Enabled Multihashes are set in the multihash-codetable crate features. ··· 234 236 return Err(BlobDownloadError::CidMismatch); 235 237 } 236 238 237 - Ok(()) 239 + Ok(bytes) 238 240 } 239 241 }) 240 242 .await 241 - .expect("CID computing task should not panic")?; 242 - 243 - bytes 243 + .expect("CID computing task should not panic")? 244 244 }; 245 245 246 246 // Infer MIME type from content bytes rather than headers; this is fallible ··· 248 248 // 249 249 // TODO: Merge this with the download stream process to reject bad MIMEs 250 250 // early? 251 - let mime_type = sniff_mime(&validated_bytes); 251 + let mime_type = sniff_mime(&bytes); 252 252 if !is_mime_allowed(&mime_type, allowed_mimetypes) { 253 253 tracing::debug!("blob was inferred to be a disallowed mime type: {mime_type}"); 254 254 return Err(BlobDownloadError::ForbiddenMimeType); ··· 257 257 // Mark this DID+CID pair as ownership-verified since we just fetched it from the origin. 258 258 self.ownership_cache.insert((*cid, did.clone()), ()).await; 259 259 260 - Ok(BlobData { 261 - bytes: validated_bytes, 262 - mime_type, 263 - }) 260 + Ok(BlobData { bytes, mime_type }) 264 261 }) 265 262 .await 266 - } 267 - 268 - pub async fn invalidate_blob(&self, cid: &BlobCid) { 269 - self.data_cache.invalidate(cid).await 270 263 } 271 264 272 265 /// Fetch whether the user owns the given blob either from the cache if available or the upstream source. ··· 337 330 .await 338 331 } 339 332 340 - pub fn invalidate_blob_ownership< 333 + /// Invalid a specific blob cache entry. 334 + pub async fn invalidate_blob_cache_entry(&self, cid: &BlobCid) { 335 + self.data_cache.invalidate(cid).await 336 + } 337 + 338 + /// Invalidate blob ownership cache entries if they match the predicate. 339 + pub fn invalidate_blob_ownership_cache_entries< 341 340 F: Fn(&(BlobCid, Did<'static>), &()) -> bool + Send + Sync + 'static, 342 341 >( 343 342 &self,
+1 -1
src/cache.rs crates/porxie/src/cache.rs
··· 1 - use std::{cmp, num::NonZeroU64}; 1 + use core::{cmp, num::NonZeroU64}; 2 2 use thiserror::Error; 3 3 4 4 #[derive(Debug, Error)]
+1 -1
src/http.rs crates/porxie/src/http.rs
··· 1 1 use bytes::Bytes; 2 + use core::num::NonZeroU64; 2 3 use futures_util::StreamExt; 3 - use std::num::NonZeroU64; 4 4 use thiserror::Error; 5 5 6 6 pub const PORXIE_USER_AGENT: &str = concat!(
+10 -3
src/identity_service.rs crates/porxie/src/identity_service.rs
··· 1 1 use crate::http::PORXIE_USER_AGENT; 2 + use core::{str::FromStr, time::Duration}; 2 3 use jacquard_common::types::did::Did; 3 4 use jacquard_identity::{ 4 5 JacquardResolver, ··· 6 7 }; 7 8 use moka::{future::Cache as MokaCache, policy::EvictionPolicy}; 8 9 use reqwest::Url; 9 - use std::{sync::Arc, time::Duration}; 10 + use std::sync::Arc; 10 11 use thiserror::Error; 11 12 use tracing::instrument; 12 13 ··· 57 58 .map_err(CreateIdentityServiceError::HttpClient)?, 58 59 ResolverOptions { 59 60 plc_source: PlcSource::PlcDirectory { 60 - base: options.plc_directory_url, 61 + base: jacquard_common::deps::fluent_uri::Uri::from_str( 62 + options.plc_directory_url.as_str(), 63 + ) 64 + .unwrap(), 61 65 }, 62 66 public_fallback_for_handle: true, 63 67 validate_doc_id: true, ··· 85 89 #[instrument(skip_all, fields(did = %did))] 86 90 pub async fn pds_for_did(&self, did: &Did<'static>) -> Result<Url, Arc<IdentityError>> { 87 91 self.cache 88 - .try_get_with_by_ref(did, self.resolver.pds_for_did(did)) 92 + .try_get_with_by_ref(did, async { 93 + let url = self.resolver.pds_for_did(did).await?; 94 + Ok(Url::parse(url.as_str()).unwrap()) 95 + }) 89 96 .await 90 97 } 91 98
+21 -137
src/main.rs crates/porxie/src/main.rs
··· 6 6 mod identity_service; 7 7 mod mime; 8 8 mod policy_client; 9 - mod routes; 9 + mod server; 10 10 mod types; 11 11 12 12 use crate::{ ··· 14 14 cache::compute_cache_sizes, 15 15 identity_service::{IdentityService, IdentityServiceOptions}, 16 16 policy_client::{PolicyClient, PolicyClientOptions}, 17 - routes::{delete_cache_handler, get_blob_handler, get_health_handler, get_index_handler}, 17 + server::{PorxieServer, PorxieServerOptions, SocketAddress}, 18 18 }; 19 19 use ::mime::Mime; 20 - use anyhow::{Context, bail}; 21 - use axum::{ 22 - Router, 23 - extract::Request, 24 - http::{HeaderName, HeaderValue, StatusCode, header}, 25 - middleware::{self as axum_middleware, Next}, 26 - response::Response, 27 - routing::{delete, get}, 28 - }; 20 + use axum::http::{HeaderName, HeaderValue}; 29 21 use bytesize::ByteSize; 30 22 use clap::{Args, Parser}; 23 + use core::num::NonZeroU64; 31 24 use dotenvy::dotenv; 32 25 use reqwest::Url; 33 - use std::{net::SocketAddr, num::NonZeroU64, path::PathBuf, str::FromStr, sync::Arc}; 34 - use tower_http::{ 35 - catch_panic::CatchPanicLayer, 36 - normalize_path::NormalizePathLayer, 37 - timeout::TimeoutLayer, 38 - trace::{self, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer}, 39 - }; 40 - use tracing::Level; 41 26 use tracing_subscriber::EnvFilter; 42 27 43 28 // Jemalloc seems to perform better compared to most system allocators, ··· 48 33 #[global_allocator] 49 34 static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; 50 35 51 - #[derive(Debug, Clone)] 52 - enum AddressType { 53 - /// An IP socket address. 54 - Ip(SocketAddr), 55 - /// A UNIX socket path. 56 - #[cfg(unix)] 57 - Unix(PathBuf), 58 - } 59 - 60 - impl FromStr for AddressType { 61 - type Err = anyhow::Error; 62 - 63 - fn from_str(s: &str) -> Result<Self, Self::Err> { 64 - #[cfg(unix)] 65 - if let Some(path) = s.strip_prefix("unix:") { 66 - return Ok(AddressType::Unix(PathBuf::from(path))); 67 - } 68 - if let Some(ip) = s.strip_prefix("ip:") { 69 - return Ok(ip.parse::<SocketAddr>().map(AddressType::Ip)?); 70 - } 71 - 72 - #[cfg(unix)] 73 - bail!("unknown address binding type, expected 'ip:<addr>' or 'unix:<path>'".to_string(),); 74 - #[cfg(not(unix))] 75 - bail!("unknown address binding type, expected 'ip:<addr>'".to_string()); 76 - } 77 - } 78 - 79 36 #[derive(Parser)] 80 37 #[clap( 81 38 author, ··· 114 71 env = "PORXIE_SERVER_ADDRESS", 115 72 default_value = "ip:127.0.0.1:6314" 116 73 )] 117 - address: AddressType, 74 + address: SocketAddress, 118 75 119 - /// Bearer token for authenticating admin requests. 76 + /// Admin password for authenticating priviledged requests. 120 77 /// 121 78 /// When unset, all authenticated endpoints will reject requests with HTTP 401. 79 + /// 80 + /// Authenticated requests always expect the username `admin` as per specification. 122 81 #[arg( 123 - id = "SA_SERVER_AUTH_TOKEN", 124 - long = "server-auth-token", 125 - env = "PORXIE_SERVER_AUTH_TOKEN" 82 + id = "SA_SERVER_ADMIN_PASSWORD", 83 + long = "server-admin-password", 84 + env = "PORXIE_SERVER_ADMIN_TOKEN" 126 85 )] 127 - auth_token: Option<String>, 86 + admin_password: Option<String>, 128 87 } 129 88 130 89 #[derive(Args)] ··· 337 296 struct PolicyServiceArgs { 338 297 /// Policy service URL that DID+CID pairs will be checked against. 339 298 /// 340 - /// Requests are sent as HTTP GET <url>/<did>/<cid>. 341 - /// 342 - /// The service is expected to return HTTP 200 (OK) if permitted or HTTP 410 (GONE) if restricted. 299 + /// Requests are sent via XRPC tp <url>/xrpc/dev.blooym.porxie.getBlobPolicy?did=<did>&cid=<cid>. 343 300 #[arg(id = "PA_POLICY_URL", long = "policy-url", env = "PORXIE_POLICY_URL")] 344 301 url: Option<Url>, 345 302 ··· 350 307 /// 351 308 /// As pipes are used as a delimiter, they cannot be contained in headers. 352 309 /// 353 - /// Example (cli): '--policy-request-headers "Authorization: Bearer token" --policy-request-headers "X-Api-Key: your-key"' 310 + /// Example (cli): '--policy-request-headers "X-Hello: world" --policy-request-headers "X-Foo: bar"' 354 311 /// 355 - /// Example (env): 'PORXIE_POLICY_REQUEST_HEADERS="Authorization: Bearer token|X-Api-Key: your-key"' 312 + /// Example (env): 'PORXIE_POLICY_REQUEST_HEADERS="X-Hello: world|X-Foo: bar"' 356 313 #[arg( 357 314 id = "PA_POLICY_REQ_HEADERS", 358 315 long = "policy-request-headers", ··· 372 329 )] 373 330 request_headers: Vec<(HeaderName, HeaderValue)>, 374 331 375 - /// Allow requests to proceed if the policy service is unavailable or returns an 376 - /// unexpected status code. 332 + /// Allow requests to proceed if the policy service is unavailable.. 377 333 /// 378 334 /// Warning: enabling this means restricted blobs may be served when the policy service is unreachable. 379 335 #[arg( ··· 405 361 http_connect_timeout: humantime::Duration, 406 362 } 407 363 408 - struct AppState { 409 - // Authentication. 410 - auth_token: Option<String>, 411 - // Blob handling. 412 - allowed_mimetypes: Vec<Mime>, 413 - blob_service: BlobService, 414 - cache_control_header: HeaderValue, 415 - max_blob_size: NonZeroU64, 416 - // Policy. 417 - policy_client: Option<PolicyClient>, 418 - policy_fail_open: bool, 419 - // Identity. 420 - identity_service: IdentityService, 421 - } 422 - 423 364 #[tokio::main(flavor = "multi_thread")] 424 365 async fn main() -> anyhow::Result<()> { 425 366 dotenv().ok(); ··· 428 369 .init(); 429 370 let args = AppArgs::parse(); 430 371 431 - // Setup state. 432 372 let cache_sizes = compute_cache_sizes(args.cache.size)?; 433 - let app_state = Arc::new(AppState { 373 + let server = PorxieServer::new(PorxieServerOptions { 374 + blob_processing_timeout: args.blob.processing_timeout.into(), 434 375 identity_service: IdentityService::new(IdentityServiceOptions { 435 376 cache_memory_allocation: cache_sizes.identity, 436 377 cache_ttl: args.cache.identity_ttl.into(), ··· 461 402 ownership_cache_ttl: args.cache.ownership_ttl.into(), 462 403 })?, 463 404 464 - auth_token: args.server.auth_token, 405 + admin_password: args.server.admin_password, 465 406 allowed_mimetypes: args.blob.allowed_mimetypes, 466 407 max_blob_size: args.blob.max_size, 467 408 cache_control_header: args.blob.cache_header, ··· 469 410 policy_fail_open: args.policy.fail_open, 470 411 }); 471 412 472 - // Setup router. 473 - let router = Router::new() 474 - .route("/", get(get_index_handler)) 475 - .route("/health", get(get_health_handler)) 476 - .route( 477 - "/{did}/{cid}", 478 - get(get_blob_handler).layer(TimeoutLayer::with_status_code( 479 - StatusCode::REQUEST_TIMEOUT, 480 - args.blob.processing_timeout.into(), 481 - )), 482 - ) 483 - .route("/cache/{id}", delete(delete_cache_handler)) 484 - .layer( 485 - TraceLayer::new_for_http() 486 - .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) 487 - .on_request(DefaultOnRequest::default().level(Level::DEBUG)) 488 - .on_response(DefaultOnResponse::default().level(Level::INFO)) 489 - .on_failure(DefaultOnFailure::default().level(Level::ERROR)), 490 - ) 491 - .layer(NormalizePathLayer::trim_trailing_slash()) 492 - .layer(CatchPanicLayer::new()) 493 - .layer(axum_middleware::from_fn(additional_headers_middleware)) 494 - .with_state(app_state); 495 - 496 - // Start server listener on specified address. 497 - match args.server.address { 498 - AddressType::Ip(ip) => { 499 - let listener = tokio::net::TcpListener::bind(ip) 500 - .await 501 - .context("failed to bind tcp listener")?; 502 - tracing::info!("server listening on http://{ip}"); 503 - axum::serve(listener, router) 504 - .with_graceful_shutdown(shutdown_signal()) 505 - .await?; 506 - } 507 - #[cfg(unix)] 508 - AddressType::Unix(path) => { 509 - let _ = std::fs::remove_file(&path); 510 - let listener = 511 - tokio::net::UnixListener::bind(&path).context("failed to bind unix listener")?; 512 - tracing::info!("server listening on unix:{}", path.display()); 513 - axum::serve(listener, router) 514 - .with_graceful_shutdown(shutdown_signal()) 515 - .await?; 516 - let _ = std::fs::remove_file(&path); 517 - } 518 - } 413 + server.start(args.server.address, shutdown_signal()).await?; 519 414 520 415 Ok(()) 521 - } 522 - 523 - async fn additional_headers_middleware(req: Request, next: Next) -> Response { 524 - let mut res = next.run(req).await; 525 - let res_headers = res.headers_mut(); 526 - res_headers.insert( 527 - header::SERVER, 528 - const { HeaderValue::from_static(env!("CARGO_PKG_NAME")) }, 529 - ); 530 - res_headers.insert("X-Robots-Tag", const { HeaderValue::from_static("none") }); 531 - res 532 416 } 533 417 534 418 // https://github.com/tokio-rs/axum/blob/15917c6dbcb4a48707a20e9cfd021992a279a662/examples/graceful-shutdown/src/main.rs#L55 ··· 548 432 }; 549 433 550 434 #[cfg(not(unix))] 551 - let terminate = std::future::pending::<()>(); 435 + let terminate = core::future::pending::<()>(); 552 436 553 437 tokio::select! { 554 438 _ = ctrl_c => {},
+1 -1
src/mime.rs crates/porxie/src/mime.rs
··· 66 66 67 67 #[cfg(test)] 68 68 mod tests { 69 + use core::str::FromStr; 69 70 use mime::Mime; 70 - use std::str::FromStr; 71 71 72 72 #[test] 73 73 fn no_match() {
+60 -33
src/policy_client.rs crates/porxie/src/policy_client.rs
··· 1 1 use crate::{http::PORXIE_USER_AGENT, types::blob_cid::BlobCid}; 2 2 use jacquard_common::types::did::Did; 3 + use lexgen::dev_blooym::porxie::get_blob_policy::{GetBlobPolicyOutput, GetBlobPolicyOutputPolicy}; 3 4 use moka::{future::Cache as MokaCache, policy::EvictionPolicy}; 4 5 use reqwest::{ 5 6 StatusCode, Url, ··· 9 10 use thiserror::Error; 10 11 use tracing::instrument; 11 12 12 - #[derive(Debug, Clone)] 13 - pub struct PolicyDecision { 14 - /// Whether the service allows this blob can be served. 15 - pub can_serve: bool, 16 - } 17 - 18 13 #[derive(Debug, Error)] 19 14 #[non_exhaustive] 20 15 pub enum CreatePolicyClientError { ··· 23 18 HttpClient(#[from] reqwest::Error), 24 19 } 25 20 21 + #[derive(Debug, Clone)] 22 + pub enum PolicyDecision { 23 + Allowed, 24 + Forbidden, 25 + } 26 + 27 + impl PolicyDecision { 28 + fn from_service_output(response: &GetBlobPolicyOutput) -> Self { 29 + match response.policy { 30 + GetBlobPolicyOutputPolicy::Allowed(_) => Self::Allowed, 31 + GetBlobPolicyOutputPolicy::Forbidden(_) => Self::Forbidden, 32 + } 33 + } 34 + } 35 + 26 36 #[derive(Debug, Error)] 27 37 #[non_exhaustive] 28 38 pub enum GetBlobPolicyError { 29 - /// Policy service returned an unhandled status code (Not 200 OK or 410 GONE). 30 - #[error("received an unhandled status code from the policy service: {0}")] 31 - UnhandledStatusCode(StatusCode), 39 + /// Policy service returned an unsuccessful status code. 40 + #[error("received an unsuccessful status code from the policy service: {0}")] 41 + StatusCode(StatusCode), 42 + 43 + /// An internal deserialization error occured. 44 + #[error(transparent)] 45 + Deserialize(#[from] serde_json::Error), 46 + 32 47 /// An internal http client error occurred, see [`reqwest::Error`]. 33 48 #[error(transparent)] 34 49 HttpClient(#[from] reqwest::Error), ··· 63 78 tracing::debug!("creating policy service client with options: {options:?}"); 64 79 Ok(Self { 65 80 cache: MokaCache::<(Did<'static>, BlobCid), PolicyDecision>::builder() 66 - .name("blob-policy") 81 + .name("policy") 67 82 .weigher(|key, _value| { 68 83 (key.0.len() + key.1.encoded_len()) 69 84 .try_into() ··· 91 106 }) 92 107 } 93 108 94 - /// Query the policy service for the policy decision of this blob. 109 + /// Query the policy service for any policy decisions applied to this actor/blob. 95 110 /// 96 111 /// Concurrent requests for the same policy are coalesced. 97 112 #[instrument(skip_all, fields(did = %did, cid = %cid))] 98 - pub async fn get_policy_for_blob( 113 + pub async fn get_policy( 99 114 &self, 100 115 did: &Did<'static>, 101 116 cid: BlobCid, ··· 104 119 .try_get_with_by_ref(&(did.clone(), cid), async { 105 120 tracing::debug!("querying policy service for the status"); 106 121 107 - let mut policy_service_url = self.policy_service_url.clone(); 108 - policy_service_url 109 - .path_segments_mut() 110 - .expect("policy service URL should not be cannot-be-a-base") 111 - .push(did.as_str()) 112 - .push(&cid.to_string()); 122 + // Build policy service URL. 123 + let url = { 124 + let mut url = self.policy_service_url.clone(); 125 + url.set_path("/xrpc/dev.blooym.porxie.getBlobPolicy"); 126 + url.query_pairs_mut() 127 + .append_pair("did", did.as_str()) 128 + .append_pair("cid", &cid.to_string()); 129 + url 130 + }; 113 131 114 - let mut request = self.http_client.get(policy_service_url); 132 + // Build request. 133 + let mut request = self.http_client.get(url); 134 + // TODO: Swap this for xrpc admin authentication. 115 135 for (name, value) in &self.policy_service_req_headers { 116 136 request = request.header(name, value); 117 137 } 118 138 139 + // Fetch & deserialize policy data. 119 140 match request.send().await { 120 - Ok(response) => match response.status() { 121 - StatusCode::OK => { 122 - tracing::debug!("policy service allowed blob serving"); 123 - Ok(PolicyDecision { can_serve: true }) 124 - } 125 - StatusCode::GONE => { 126 - tracing::debug!("policy service forbids blob serving"); 127 - Ok(PolicyDecision { can_serve: false }) 141 + Ok(response) => { 142 + let status = response.status(); 143 + if !status.is_success() { 144 + tracing::error!( 145 + "policy service returned unsuccessful status: {status}", 146 + ); 147 + return Err(GetBlobPolicyError::StatusCode(status)); 128 148 } 129 - status => { 130 - tracing::error!("policy service returned unexpected status: {status}"); 131 - Err(GetBlobPolicyError::UnhandledStatusCode(status)) 149 + match serde_json::from_slice::<GetBlobPolicyOutput>( 150 + &response.bytes().await?, 151 + ) { 152 + Ok(output) => Ok(PolicyDecision::from_service_output(&output)), 153 + Err(err) => { 154 + tracing::error!( 155 + "failed to deserialize policy service response: {status}", 156 + ); 157 + Err(GetBlobPolicyError::Deserialize(err)) 158 + } 132 159 } 133 - }, 160 + } 134 161 Err(err) => { 135 162 tracing::error!("error occurred contacting the policy service: {err:?}"); 136 163 Err(GetBlobPolicyError::HttpClient(err)) ··· 140 167 .await 141 168 } 142 169 143 - /// Invalidate cached policy decisions with the given predicate. 144 - pub fn invalidate_policies< 170 + /// Invalidate cached policy entries if they match the predicate. 171 + pub fn invalidate_cache_entries< 145 172 F: Fn(&(Did<'static>, BlobCid), &PolicyDecision) -> bool + Send + Sync + 'static, 146 173 >( 147 174 &self,
+38 -31
src/routes/blob/get.rs crates/porxie/src/server/routes/blob.rs
··· 1 1 use crate::{ 2 - AppState, 3 2 blob_service::{BlobDownloadError, BlobOwnershipError, BlobUrlResolver}, 4 - routes::{CACHE_CONTROL_NOCACHE_VALUE, ErrorResponse}, 3 + policy_client::PolicyDecision, 4 + server::{ 5 + ServerState, 6 + routes::{CACHE_CONTROL_NOCACHE_VALUE, XrpcErrorResponse}, 7 + }, 5 8 types::blob_cid::BlobCid, 6 9 }; 7 10 use axum::{ ··· 17 20 /// Fetch a blob from a given upstream and return it. 18 21 pub async fn get_blob_handler( 19 22 Path((raw_did, raw_cid)): Path<(String, String)>, 20 - State(state): State<Arc<AppState>>, 23 + State(state): State<Arc<ServerState>>, 21 24 ) -> Result< 22 25 Response, 23 26 ( 24 27 StatusCode, 25 28 [(HeaderName, &'static str); 1], 26 - Json<ErrorResponse>, 29 + Json<XrpcErrorResponse>, 27 30 ), 28 31 > { 29 32 let (did, cid) = ( ··· 33 36 return Err(( 34 37 StatusCode::UNPROCESSABLE_ENTITY, 35 38 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 36 - Json(ErrorResponse { 39 + Json(XrpcErrorResponse { 37 40 error: "MalformedDid", 38 41 message: Some("Invalid or unprocessable DID"), 39 42 }), ··· 46 49 return Err(( 47 50 StatusCode::UNPROCESSABLE_ENTITY, 48 51 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 49 - Json(ErrorResponse { 52 + Json(XrpcErrorResponse { 50 53 error: "MalformedCid", 51 54 message: Some("Invalid or unprocessable CID"), 52 55 }), ··· 57 60 58 61 // Check the policy status of the blob. 59 62 if let Some(ref policy_client) = state.policy_client { 60 - match policy_client.get_policy_for_blob(&did, cid).await { 63 + match policy_client.get_policy(&did, cid).await { 61 64 Ok(policy) => { 62 - if !policy.can_serve { 63 - return Err(( 64 - StatusCode::GONE, 65 - [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 66 - Json(ErrorResponse { 67 - error: "PolicyForbidden", 68 - message: Some("Requested blob cannot be served by this service"), 69 - }), 70 - )); 71 - } 65 + match policy { 66 + PolicyDecision::Allowed => {} 67 + PolicyDecision::Forbidden => { 68 + return Err(( 69 + StatusCode::GONE, 70 + [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 71 + Json(XrpcErrorResponse { 72 + error: "PolicyForbidden", 73 + message: Some("Requested blob has been forbidden by this service"), 74 + }), 75 + )); 76 + } 77 + }; 72 78 } 73 79 Err(_) => { 74 80 if !state.policy_fail_open { ··· 76 82 return Err(( 77 83 StatusCode::INTERNAL_SERVER_ERROR, 78 84 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 79 - Json(ErrorResponse { 85 + Json(XrpcErrorResponse { 80 86 error: "InternalServerError", 81 87 message: Some("An internal server error occured."), 82 88 }), ··· 106 112 BlobDownloadError::NotFound => ( 107 113 StatusCode::NOT_FOUND, 108 114 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 109 - Json(ErrorResponse { 115 + Json(XrpcErrorResponse { 110 116 error: "BlobNotFound", 111 117 message: Some("Blob not found"), 112 118 }), ··· 114 120 BlobDownloadError::TooLarge => ( 115 121 StatusCode::PAYLOAD_TOO_LARGE, 116 122 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 117 - Json(ErrorResponse { 123 + Json(XrpcErrorResponse { 118 124 error: "BlobTooLarge", 119 125 message: Some("Blob exceeds maximum allowed size"), 120 126 }), ··· 122 128 BlobDownloadError::ForbiddenMimeType => ( 123 129 StatusCode::FORBIDDEN, 124 130 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 125 - Json(ErrorResponse { 131 + Json(XrpcErrorResponse { 126 132 error: "BlobForbiddenType", 127 133 message: Some("Content type is not allowed"), 128 134 }), ··· 130 136 BlobDownloadError::CidMismatch => ( 131 137 StatusCode::BAD_GATEWAY, 132 138 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 133 - Json(ErrorResponse { 139 + Json(XrpcErrorResponse { 134 140 error: "BlobCidMismatch", 135 141 message: Some("Blob content does not match CID"), 136 142 }), ··· 138 144 BlobDownloadError::CidUnsupportedMultihash => ( 139 145 StatusCode::NOT_IMPLEMENTED, 140 146 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 141 - Json(ErrorResponse { 147 + Json(XrpcErrorResponse { 142 148 error: "CidUnsupported", 143 149 message: Some("Unsupported CID multihash"), 144 150 }), ··· 146 152 BlobDownloadError::BlobResolutionFailure => ( 147 153 StatusCode::BAD_GATEWAY, 148 154 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 149 - Json(ErrorResponse { 155 + Json(XrpcErrorResponse { 150 156 error: "CannotResolve", 151 157 message: Some("Failed to resolve source of blob"), 152 158 }), ··· 156 162 | BlobDownloadError::StreamFailed => ( 157 163 StatusCode::BAD_GATEWAY, 158 164 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 159 - Json(ErrorResponse { 165 + Json(XrpcErrorResponse { 160 166 error: "BlobFetchFailed", 161 167 message: Some("Failed to fetch blob from origin"), 162 168 }), ··· 185 191 BlobOwnershipError::NotFound => ( 186 192 StatusCode::NOT_FOUND, 187 193 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 188 - Json(ErrorResponse { 194 + Json(XrpcErrorResponse { 189 195 error: "BlobNotFound", 190 196 message: Some("Blob not found"), 191 197 }), ··· 193 199 BlobOwnershipError::BlobResolutionFailure => ( 194 200 StatusCode::BAD_GATEWAY, 195 201 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 196 - Json(ErrorResponse { 197 - error: "CannotResolvePds", 198 - message: Some("Failed to resolve PDS for DID"), 202 + Json(XrpcErrorResponse { 203 + error: "CannotResolve", 204 + message: Some("Failed to resolve source of blob"), 199 205 }), 200 206 ), 201 207 BlobOwnershipError::ErrorStatusCode | BlobOwnershipError::FetchFailure => ( 202 208 StatusCode::BAD_GATEWAY, 203 209 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 204 - Json(ErrorResponse { 210 + Json(XrpcErrorResponse { 205 211 error: "BlobFetchFailed", 206 212 message: Some("Failed to fetch blob from origin"), 207 213 }), ··· 223 229 ) 224 230 .header( 225 231 header::CONTENT_DISPOSITION, 226 - const { HeaderValue::from_static("attachment") }, 232 + HeaderValue::from_str(&format!(r#"attachment, filename="{cid}""#)) 233 + .unwrap_or(const { HeaderValue::from_static("attachment") }), 227 234 ) 228 235 .body(Body::from(blob.bytes)) 229 236 .expect("response should always build successfully"))
-3
src/routes/blob/mod.rs
··· 1 - mod get; 2 - 3 - pub use get::get_blob_handler;
-93
src/routes/cache/delete.rs
··· 1 - use crate::{ 2 - AppState, 3 - routes::{CACHE_CONTROL_NOCACHE_VALUE, ErrorResponse}, 4 - types::blob_cid::BlobCid, 5 - }; 6 - use axum::{ 7 - Json, 8 - extract::{Path, State}, 9 - http::{HeaderName, StatusCode, header}, 10 - }; 11 - use axum_extra::{ 12 - TypedHeader, 13 - headers::{Authorization, authorization::Bearer}, 14 - }; 15 - use jacquard_common::types::did::Did; 16 - use std::sync::Arc; 17 - use subtle::ConstantTimeEq; 18 - 19 - pub async fn delete_cache_handler( 20 - Path(identifier): Path<String>, 21 - State(state): State<Arc<AppState>>, 22 - TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>, 23 - ) -> Result< 24 - StatusCode, 25 - ( 26 - StatusCode, 27 - [(HeaderName, &'static str); 1], 28 - Json<ErrorResponse>, 29 - ), 30 - > { 31 - if !state 32 - .auth_token 33 - .as_ref() 34 - .map(|expected| expected.as_bytes().ct_eq(bearer.token().as_bytes()).into()) 35 - .unwrap_or(false) 36 - { 37 - return Err(( 38 - StatusCode::UNAUTHORIZED, 39 - [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 40 - Json(ErrorResponse { 41 - error: "Unauthorized", 42 - message: None, 43 - }), 44 - )); 45 - } 46 - 47 - // TODO: Really need to expose a nicer cache purging API, 48 - // matching on prefix sucks. 49 - if identifier.starts_with("did:") { 50 - tracing::info!("invalidating DID cache entries"); 51 - let did = Did::new_owned(identifier).map_err(|_| { 52 - ( 53 - StatusCode::UNPROCESSABLE_ENTITY, 54 - [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 55 - Json(ErrorResponse { 56 - error: "MalformedDid", 57 - message: Some("Invalid or unprocessable DID"), 58 - }), 59 - ) 60 - })?; 61 - state.identity_service.invalidate_did_cache(&did).await; 62 - if let Some(ref policy_client) = state.policy_client { 63 - policy_client.invalidate_policies({ 64 - let did = did.clone(); 65 - move |k, _v| k.0 == did 66 - }) 67 - } 68 - state 69 - .blob_service 70 - .invalidate_blob_ownership(move |k, _v| k.1 == did); 71 - } else { 72 - tracing::info!("invalidating CID cache entries"); 73 - let cid = BlobCid::try_from(identifier.as_str()).map_err(|_| { 74 - ( 75 - StatusCode::UNPROCESSABLE_ENTITY, 76 - [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)], 77 - Json(ErrorResponse { 78 - error: "MalformedCid", 79 - message: Some("Invalid or unprocessable CID"), 80 - }), 81 - ) 82 - })?; 83 - state.blob_service.invalidate_blob(&cid).await; 84 - state 85 - .blob_service 86 - .invalidate_blob_ownership(move |k, _v| k.0 == cid); 87 - if let Some(ref policy_client) = state.policy_client { 88 - policy_client.invalidate_policies(move |k, _v| k.1 == cid) 89 - } 90 - } 91 - 92 - Ok(StatusCode::OK) 93 - }
-3
src/routes/cache/mod.rs
··· 1 - mod delete; 2 - 3 - pub use delete::*;
+3 -2
src/routes/health/get.rs crates/porxie/src/server/routes/xrpc/health.rs
··· 1 - use crate::routes::CACHE_CONTROL_NOCACHE_VALUE; 2 1 use axum::{ 3 2 Json, 4 3 http::{StatusCode, header}, 5 4 response::IntoResponse, 6 5 }; 7 6 use serde::Serialize; 7 + 8 + use crate::server::routes::CACHE_CONTROL_NOCACHE_VALUE; 8 9 9 10 #[derive(Serialize)] 10 11 struct GetHealthResponse { 11 12 version: &'static str, 12 13 } 13 14 14 - pub async fn get_health_handler() -> impl IntoResponse { 15 + pub async fn xrpc_get_health_handler() -> impl IntoResponse { 15 16 ( 16 17 StatusCode::OK, 17 18 [(header::CACHE_CONTROL, CACHE_CONTROL_NOCACHE_VALUE)],
-3
src/routes/health/mod.rs
··· 1 - pub mod get; 2 - 3 - pub use get::get_health_handler;
-38
src/routes/mod.rs
··· 1 - mod blob; 2 - mod cache; 3 - mod health; 4 - 5 - pub use blob::get_blob_handler; 6 - pub use cache::delete_cache_handler; 7 - pub use health::get_health_handler; 8 - 9 - /// A header value for [`header::CACHE_CONTROL`] indicating the response cannot be cached at all. 10 - pub const CACHE_CONTROL_NOCACHE_VALUE: &str = "must-understand, no-store"; 11 - 12 - #[derive(serde::Serialize)] 13 - pub struct ErrorResponse { 14 - error: &'static str, 15 - message: Option<&'static str>, 16 - } 17 - 18 - pub async fn get_index_handler() -> &'static str { 19 - r#" 20 - _____ _ 21 - | __ \ (_) 22 - | |__) |__ _ ____ ___ ___ 23 - | ___/ _ \| '__\ \/ / |/ _ \ 24 - | | | (_) | | > <| | __/ 25 - |_| \___/|_| /_/\_\_|\___| 26 - 27 - 28 - A correct and efficient ATProto blob proxy for secure content delivery. 29 - 30 - Links: 31 - - Repo: https://codeberg.org/Blooym/porxie 32 - - ATProto: https://atproto.com 33 - 34 - Routes: 35 - - HTTP GET /{did}/{cid} - Resolve and fetch a blob from its origin. 36 - - HTTP DELETE /cache/{cid or did} - Invalidate cache for either a CID (blob, policy, ownership) or for a DID (ownerships and policies). Requires auth. 37 - "# 38 - }
+1 -1
src/types/blob_cid.rs crates/porxie/src/types/blob_cid.rs
··· 92 92 } 93 93 94 94 impl core::fmt::Display for BlobCid { 95 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 95 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 96 96 self.0.fmt(f) 97 97 } 98 98 }
src/types/mod.rs crates/porxie/src/types/mod.rs