max / makenotwork
9 files changed,
+932 insertions,
-5 deletions
| @@ -331,6 +331,19 @@ dependencies = [ | |||
| 331 | 331 | ] | |
| 332 | 332 | ||
| 333 | 333 | [[package]] | |
| 334 | + | name = "chacha20poly1305" | |
| 335 | + | version = "0.10.1" | |
| 336 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 337 | + | checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" | |
| 338 | + | dependencies = [ | |
| 339 | + | "aead", | |
| 340 | + | "chacha20", | |
| 341 | + | "cipher", | |
| 342 | + | "poly1305", | |
| 343 | + | "zeroize", | |
| 344 | + | ] | |
| 345 | + | ||
| 346 | + | [[package]] | |
| 334 | 347 | name = "chrono" | |
| 335 | 348 | version = "0.4.44" | |
| 336 | 349 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -339,6 +352,7 @@ dependencies = [ | |||
| 339 | 352 | "iana-time-zone", | |
| 340 | 353 | "js-sys", | |
| 341 | 354 | "num-traits", | |
| 355 | + | "serde", | |
| 342 | 356 | "wasm-bindgen", | |
| 343 | 357 | "windows-link", | |
| 344 | 358 | ] | |
| @@ -351,6 +365,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" | |||
| 351 | 365 | dependencies = [ | |
| 352 | 366 | "crypto-common 0.1.7", | |
| 353 | 367 | "inout", | |
| 368 | + | "zeroize", | |
| 354 | 369 | ] | |
| 355 | 370 | ||
| 356 | 371 | [[package]] | |
| @@ -424,6 +439,26 @@ dependencies = [ | |||
| 424 | 439 | ] | |
| 425 | 440 | ||
| 426 | 441 | [[package]] | |
| 442 | + | name = "core-foundation" | |
| 443 | + | version = "0.9.4" | |
| 444 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 445 | + | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" | |
| 446 | + | dependencies = [ | |
| 447 | + | "core-foundation-sys", | |
| 448 | + | "libc", | |
| 449 | + | ] | |
| 450 | + | ||
| 451 | + | [[package]] | |
| 452 | + | name = "core-foundation" | |
| 453 | + | version = "0.10.1" | |
| 454 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 455 | + | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | |
| 456 | + | dependencies = [ | |
| 457 | + | "core-foundation-sys", | |
| 458 | + | "libc", | |
| 459 | + | ] | |
| 460 | + | ||
| 461 | + | [[package]] | |
| 427 | 462 | name = "core-foundation-sys" | |
| 428 | 463 | version = "0.8.7" | |
| 429 | 464 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -532,6 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 532 | 567 | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 533 | 568 | dependencies = [ | |
| 534 | 569 | "generic-array 0.14.7", | |
| 570 | + | "rand_core 0.6.4", | |
| 535 | 571 | "typenum", | |
| 536 | 572 | ] | |
| 537 | 573 | ||
| @@ -836,6 +872,15 @@ dependencies = [ | |||
| 836 | 872 | ] | |
| 837 | 873 | ||
| 838 | 874 | [[package]] | |
| 875 | + | name = "encoding_rs" | |
| 876 | + | version = "0.8.35" | |
| 877 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 878 | + | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" | |
| 879 | + | dependencies = [ | |
| 880 | + | "cfg-if", | |
| 881 | + | ] | |
| 882 | + | ||
| 883 | + | [[package]] | |
| 839 | 884 | name = "enum_dispatch" | |
| 840 | 885 | version = "0.3.13" | |
| 841 | 886 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -860,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 860 | 905 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 861 | 906 | dependencies = [ | |
| 862 | 907 | "libc", | |
| 863 | - | "windows-sys 0.59.0", | |
| 908 | + | "windows-sys 0.61.2", | |
| 864 | 909 | ] | |
| 865 | 910 | ||
| 866 | 911 | [[package]] | |
| @@ -883,6 +928,12 @@ dependencies = [ | |||
| 883 | 928 | ] | |
| 884 | 929 | ||
| 885 | 930 | [[package]] | |
| 931 | + | name = "fastrand" | |
| 932 | + | version = "2.4.1" | |
| 933 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 934 | + | checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" | |
| 935 | + | ||
| 936 | + | [[package]] | |
| 886 | 937 | name = "ff" | |
| 887 | 938 | version = "0.13.1" | |
| 888 | 939 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -968,6 +1019,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 968 | 1019 | checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | |
| 969 | 1020 | ||
| 970 | 1021 | [[package]] | |
| 1022 | + | name = "foreign-types" | |
| 1023 | + | version = "0.3.2" | |
| 1024 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1025 | + | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" | |
| 1026 | + | dependencies = [ | |
| 1027 | + | "foreign-types-shared", | |
| 1028 | + | ] | |
| 1029 | + | ||
| 1030 | + | [[package]] | |
| 1031 | + | name = "foreign-types-shared" | |
| 1032 | + | version = "0.1.1" | |
| 1033 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1034 | + | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" | |
| 1035 | + | ||
| 1036 | + | [[package]] | |
| 971 | 1037 | name = "form_urlencoded" | |
| 972 | 1038 | version = "1.2.2" | |
| 973 | 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1154,6 +1220,25 @@ dependencies = [ | |||
| 1154 | 1220 | ] | |
| 1155 | 1221 | ||
| 1156 | 1222 | [[package]] | |
| 1223 | + | name = "h2" | |
| 1224 | + | version = "0.4.14" | |
| 1225 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1226 | + | checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" | |
| 1227 | + | dependencies = [ | |
| 1228 | + | "atomic-waker", | |
| 1229 | + | "bytes", | |
| 1230 | + | "fnv", | |
| 1231 | + | "futures-core", | |
| 1232 | + | "futures-sink", | |
| 1233 | + | "http", | |
| 1234 | + | "indexmap", | |
| 1235 | + | "slab", | |
| 1236 | + | "tokio", | |
| 1237 | + | "tokio-util", | |
| 1238 | + | "tracing", | |
| 1239 | + | ] | |
| 1240 | + | ||
| 1241 | + | [[package]] | |
| 1157 | 1242 | name = "hashbrown" | |
| 1158 | 1243 | version = "0.15.5" | |
| 1159 | 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1310,6 +1395,7 @@ dependencies = [ | |||
| 1310 | 1395 | "bytes", | |
| 1311 | 1396 | "futures-channel", | |
| 1312 | 1397 | "futures-core", | |
| 1398 | + | "h2", | |
| 1313 | 1399 | "http", | |
| 1314 | 1400 | "http-body", | |
| 1315 | 1401 | "httparse", | |
| @@ -1339,6 +1425,22 @@ dependencies = [ | |||
| 1339 | 1425 | ] | |
| 1340 | 1426 | ||
| 1341 | 1427 | [[package]] | |
| 1428 | + | name = "hyper-tls" | |
| 1429 | + | version = "0.6.0" | |
| 1430 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1431 | + | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" | |
| 1432 | + | dependencies = [ | |
| 1433 | + | "bytes", | |
| 1434 | + | "http-body-util", | |
| 1435 | + | "hyper", | |
| 1436 | + | "hyper-util", | |
| 1437 | + | "native-tls", | |
| 1438 | + | "tokio", | |
| 1439 | + | "tokio-native-tls", | |
| 1440 | + | "tower-service", | |
| 1441 | + | ] | |
| 1442 | + | ||
| 1443 | + | [[package]] | |
| 1342 | 1444 | name = "hyper-util" | |
| 1343 | 1445 | version = "0.1.20" | |
| 1344 | 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1356,9 +1458,11 @@ dependencies = [ | |||
| 1356 | 1458 | "percent-encoding", | |
| 1357 | 1459 | "pin-project-lite", | |
| 1358 | 1460 | "socket2", | |
| 1461 | + | "system-configuration", | |
| 1359 | 1462 | "tokio", | |
| 1360 | 1463 | "tower-service", | |
| 1361 | 1464 | "tracing", | |
| 1465 | + | "windows-registry", | |
| 1362 | 1466 | ] | |
| 1363 | 1467 | ||
| 1364 | 1468 | [[package]] | |
| @@ -1841,6 +1945,12 @@ dependencies = [ | |||
| 1841 | 1945 | ] | |
| 1842 | 1946 | ||
| 1843 | 1947 | [[package]] | |
| 1948 | + | name = "mime" | |
| 1949 | + | version = "0.3.17" | |
| 1950 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1951 | + | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | |
| 1952 | + | ||
| 1953 | + | [[package]] | |
| 1844 | 1954 | name = "minimal-lexical" | |
| 1845 | 1955 | version = "0.2.1" | |
| 1846 | 1956 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1881,12 +1991,30 @@ dependencies = [ | |||
| 1881 | 1991 | "russh-sftp", | |
| 1882 | 1992 | "serde", | |
| 1883 | 1993 | "serde_json", | |
| 1994 | + | "synckit-client", | |
| 1884 | 1995 | "tokio", | |
| 1885 | 1996 | "tracing", | |
| 1886 | 1997 | "tracing-subscriber", | |
| 1887 | 1998 | ] | |
| 1888 | 1999 | ||
| 1889 | 2000 | [[package]] | |
| 2001 | + | name = "native-tls" | |
| 2002 | + | version = "0.2.18" | |
| 2003 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2004 | + | checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" | |
| 2005 | + | dependencies = [ | |
| 2006 | + | "libc", | |
| 2007 | + | "log", | |
| 2008 | + | "openssl", | |
| 2009 | + | "openssl-probe", | |
| 2010 | + | "openssl-sys", | |
| 2011 | + | "schannel", | |
| 2012 | + | "security-framework", | |
| 2013 | + | "security-framework-sys", | |
| 2014 | + | "tempfile", | |
| 2015 | + | ] | |
| 2016 | + | ||
| 2017 | + | [[package]] | |
| 1890 | 2018 | name = "nix" | |
| 1891 | 2019 | version = "0.29.0" | |
| 1892 | 2020 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -1915,7 +2043,7 @@ version = "0.50.3" | |||
| 1915 | 2043 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1916 | 2044 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 1917 | 2045 | dependencies = [ | |
| 1918 | - | "windows-sys 0.60.2", | |
| 2046 | + | "windows-sys 0.61.2", | |
| 1919 | 2047 | ] | |
| 1920 | 2048 | ||
| 1921 | 2049 | [[package]] | |
| @@ -2023,6 +2151,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 2023 | 2151 | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | |
| 2024 | 2152 | ||
| 2025 | 2153 | [[package]] | |
| 2154 | + | name = "openssl" | |
| 2155 | + | version = "0.10.80" | |
| 2156 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2157 | + | checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" | |
| 2158 | + | dependencies = [ | |
| 2159 | + | "bitflags 2.11.0", | |
| 2160 | + | "cfg-if", | |
| 2161 | + | "foreign-types", | |
| 2162 | + | "libc", | |
| 2163 | + | "openssl-macros", | |
| 2164 | + | "openssl-sys", | |
| 2165 | + | ] | |
| 2166 | + | ||
| 2167 | + | [[package]] | |
| 2168 | + | name = "openssl-macros" | |
| 2169 | + | version = "0.1.1" | |
| 2170 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2171 | + | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" | |
| 2172 | + | dependencies = [ | |
| 2173 | + | "proc-macro2", | |
| 2174 | + | "quote", | |
| 2175 | + | "syn 2.0.117", | |
| 2176 | + | ] | |
| 2177 | + | ||
| 2178 | + | [[package]] | |
| 2179 | + | name = "openssl-probe" | |
| 2180 | + | version = "0.2.1" | |
| 2181 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2182 | + | checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" | |
| 2183 | + | ||
| 2184 | + | [[package]] | |
| 2185 | + | name = "openssl-sys" | |
| 2186 | + | version = "0.9.116" | |
| 2187 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2188 | + | checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" | |
| 2189 | + | dependencies = [ | |
| 2190 | + | "cc", | |
| 2191 | + | "libc", | |
| 2192 | + | "pkg-config", | |
| 2193 | + | "vcpkg", | |
| 2194 | + | ] | |
| 2195 | + | ||
| 2196 | + | [[package]] | |
| 2026 | 2197 | name = "ordered-float" | |
| 2027 | 2198 | version = "4.6.0" | |
| 2028 | 2199 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2317,6 +2488,12 @@ dependencies = [ | |||
| 2317 | 2488 | ] | |
| 2318 | 2489 | ||
| 2319 | 2490 | [[package]] | |
| 2491 | + | name = "pkg-config" | |
| 2492 | + | version = "0.3.33" | |
| 2493 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2494 | + | checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" | |
| 2495 | + | ||
| 2496 | + | [[package]] | |
| 2320 | 2497 | name = "poly1305" | |
| 2321 | 2498 | version = "0.8.0" | |
| 2322 | 2499 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2691,15 +2868,20 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" | |||
| 2691 | 2868 | dependencies = [ | |
| 2692 | 2869 | "base64", | |
| 2693 | 2870 | "bytes", | |
| 2871 | + | "encoding_rs", | |
| 2694 | 2872 | "futures-core", | |
| 2873 | + | "h2", | |
| 2695 | 2874 | "http", | |
| 2696 | 2875 | "http-body", | |
| 2697 | 2876 | "http-body-util", | |
| 2698 | 2877 | "hyper", | |
| 2699 | 2878 | "hyper-rustls", | |
| 2879 | + | "hyper-tls", | |
| 2700 | 2880 | "hyper-util", | |
| 2701 | 2881 | "js-sys", | |
| 2702 | 2882 | "log", | |
| 2883 | + | "mime", | |
| 2884 | + | "native-tls", | |
| 2703 | 2885 | "percent-encoding", | |
| 2704 | 2886 | "pin-project-lite", | |
| 2705 | 2887 | "quinn", | |
| @@ -2710,6 +2892,7 @@ dependencies = [ | |||
| 2710 | 2892 | "serde_urlencoded", | |
| 2711 | 2893 | "sync_wrapper", | |
| 2712 | 2894 | "tokio", | |
| 2895 | + | "tokio-native-tls", | |
| 2713 | 2896 | "tokio-rustls", | |
| 2714 | 2897 | "tower", | |
| 2715 | 2898 | "tower-http", | |
| @@ -2891,7 +3074,7 @@ dependencies = [ | |||
| 2891 | 3074 | "errno", | |
| 2892 | 3075 | "libc", | |
| 2893 | 3076 | "linux-raw-sys", | |
| 2894 | - | "windows-sys 0.59.0", | |
| 3077 | + | "windows-sys 0.61.2", | |
| 2895 | 3078 | ] | |
| 2896 | 3079 | ||
| 2897 | 3080 | [[package]] | |
| @@ -2951,6 +3134,15 @@ dependencies = [ | |||
| 2951 | 3134 | ] | |
| 2952 | 3135 | ||
| 2953 | 3136 | [[package]] | |
| 3137 | + | name = "schannel" | |
| 3138 | + | version = "0.1.29" | |
| 3139 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3140 | + | checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" | |
| 3141 | + | dependencies = [ | |
| 3142 | + | "windows-sys 0.61.2", | |
| 3143 | + | ] | |
| 3144 | + | ||
| 3145 | + | [[package]] | |
| 2954 | 3146 | name = "scopeguard" | |
| 2955 | 3147 | version = "1.2.0" | |
| 2956 | 3148 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -2982,6 +3174,29 @@ dependencies = [ | |||
| 2982 | 3174 | ] | |
| 2983 | 3175 | ||
| 2984 | 3176 | [[package]] | |
| 3177 | + | name = "security-framework" | |
| 3178 | + | version = "3.7.0" | |
| 3179 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3180 | + | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" | |
| 3181 | + | dependencies = [ | |
| 3182 | + | "bitflags 2.11.0", | |
| 3183 | + | "core-foundation 0.10.1", | |
| 3184 | + | "core-foundation-sys", | |
| 3185 | + | "libc", | |
| 3186 | + | "security-framework-sys", | |
| 3187 | + | ] | |
| 3188 | + | ||
| 3189 | + | [[package]] | |
| 3190 | + | name = "security-framework-sys" | |
| 3191 | + | version = "2.17.0" | |
| 3192 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3193 | + | checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" | |
| 3194 | + | dependencies = [ | |
| 3195 | + | "core-foundation-sys", | |
| 3196 | + | "libc", | |
| 3197 | + | ] | |
| 3198 | + | ||
| 3199 | + | [[package]] | |
| 2985 | 3200 | name = "seize" | |
| 2986 | 3201 | version = "0.3.3" | |
| 2987 | 3202 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3199,7 +3414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 3199 | 3414 | checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" | |
| 3200 | 3415 | dependencies = [ | |
| 3201 | 3416 | "libc", | |
| 3202 | - | "windows-sys 0.60.2", | |
| 3417 | + | "windows-sys 0.61.2", | |
| 3203 | 3418 | ] | |
| 3204 | 3419 | ||
| 3205 | 3420 | [[package]] | |
| @@ -3334,6 +3549,30 @@ dependencies = [ | |||
| 3334 | 3549 | ] | |
| 3335 | 3550 | ||
| 3336 | 3551 | [[package]] | |
| 3552 | + | name = "synckit-client" | |
| 3553 | + | version = "0.4.0" | |
| 3554 | + | dependencies = [ | |
| 3555 | + | "argon2", | |
| 3556 | + | "base64", | |
| 3557 | + | "bytes", | |
| 3558 | + | "chacha20poly1305", | |
| 3559 | + | "chrono", | |
| 3560 | + | "parking_lot", | |
| 3561 | + | "rand 0.9.2", | |
| 3562 | + | "reqwest", | |
| 3563 | + | "serde", | |
| 3564 | + | "serde_json", | |
| 3565 | + | "thiserror 2.0.18", | |
| 3566 | + | "tokio", | |
| 3567 | + | "tokio-stream", | |
| 3568 | + | "tracing", | |
| 3569 | + | "unicode-normalization", | |
| 3570 | + | "urlencoding", | |
| 3571 | + | "uuid", | |
| 3572 | + | "zeroize", | |
| 3573 | + | ] | |
| 3574 | + | ||
| 3575 | + | [[package]] | |
| 3337 | 3576 | name = "synstructure" | |
| 3338 | 3577 | version = "0.13.2" | |
| 3339 | 3578 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3345,6 +3584,40 @@ dependencies = [ | |||
| 3345 | 3584 | ] | |
| 3346 | 3585 | ||
| 3347 | 3586 | [[package]] | |
| 3587 | + | name = "system-configuration" | |
| 3588 | + | version = "0.7.0" | |
| 3589 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3590 | + | checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" | |
| 3591 | + | dependencies = [ | |
| 3592 | + | "bitflags 2.11.0", | |
| 3593 | + | "core-foundation 0.9.4", | |
| 3594 | + | "system-configuration-sys", | |
| 3595 | + | ] | |
| 3596 | + | ||
| 3597 | + | [[package]] | |
| 3598 | + | name = "system-configuration-sys" | |
| 3599 | + | version = "0.6.0" | |
| 3600 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3601 | + | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" | |
| 3602 | + | dependencies = [ | |
| 3603 | + | "core-foundation-sys", | |
| 3604 | + | "libc", | |
| 3605 | + | ] | |
| 3606 | + | ||
| 3607 | + | [[package]] | |
| 3608 | + | name = "tempfile" | |
| 3609 | + | version = "3.27.0" | |
| 3610 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3611 | + | checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" | |
| 3612 | + | dependencies = [ | |
| 3613 | + | "fastrand", | |
| 3614 | + | "getrandom 0.4.2", | |
| 3615 | + | "once_cell", | |
| 3616 | + | "rustix", | |
| 3617 | + | "windows-sys 0.61.2", | |
| 3618 | + | ] | |
| 3619 | + | ||
| 3620 | + | [[package]] | |
| 3348 | 3621 | name = "terminfo" | |
| 3349 | 3622 | version = "0.9.0" | |
| 3350 | 3623 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3561,6 +3834,16 @@ dependencies = [ | |||
| 3561 | 3834 | ] | |
| 3562 | 3835 | ||
| 3563 | 3836 | [[package]] | |
| 3837 | + | name = "tokio-native-tls" | |
| 3838 | + | version = "0.3.1" | |
| 3839 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3840 | + | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" | |
| 3841 | + | dependencies = [ | |
| 3842 | + | "native-tls", | |
| 3843 | + | "tokio", | |
| 3844 | + | ] | |
| 3845 | + | ||
| 3846 | + | [[package]] | |
| 3564 | 3847 | name = "tokio-rustls" | |
| 3565 | 3848 | version = "0.26.4" | |
| 3566 | 3849 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3571,6 +3854,17 @@ dependencies = [ | |||
| 3571 | 3854 | ] | |
| 3572 | 3855 | ||
| 3573 | 3856 | [[package]] | |
| 3857 | + | name = "tokio-stream" | |
| 3858 | + | version = "0.1.18" | |
| 3859 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3860 | + | checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" | |
| 3861 | + | dependencies = [ | |
| 3862 | + | "futures-core", | |
| 3863 | + | "pin-project-lite", | |
| 3864 | + | "tokio", | |
| 3865 | + | ] | |
| 3866 | + | ||
| 3867 | + | [[package]] | |
| 3574 | 3868 | name = "tokio-util" | |
| 3575 | 3869 | version = "0.7.18" | |
| 3576 | 3870 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| @@ -3714,6 +4008,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| 3714 | 4008 | checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" | |
| 3715 | 4009 | ||
| 3716 | 4010 | [[package]] | |
| 4011 | + | name = "unicode-normalization" | |
| 4012 | + | version = "0.1.25" | |
| 4013 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4014 | + | checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" | |
| 4015 | + | dependencies = [ | |
| 4016 | + | "tinyvec", | |
| 4017 | + | ] | |
| 4018 | + | ||
| 4019 | + | [[package]] |
Lines truncated
| @@ -16,3 +16,4 @@ tracing = "0.1" | |||
| 16 | 16 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } | |
| 17 | 17 | anyhow = "1" | |
| 18 | 18 | bytes = "1" | |
| 19 | + | synckit-client = { path = "../shared/synckit-client", default-features = false } |
| @@ -8,6 +8,7 @@ mod api; | |||
| 8 | 8 | mod commands; | |
| 9 | 9 | mod config; | |
| 10 | 10 | mod format; | |
| 11 | + | mod ota; | |
| 11 | 12 | mod rate_limit; | |
| 12 | 13 | mod ssh; | |
| 13 | 14 | mod staging; | |
| @@ -23,6 +24,13 @@ use tracing_subscriber::EnvFilter; | |||
| 23 | 24 | ||
| 24 | 25 | #[tokio::main] | |
| 25 | 26 | async fn main() -> anyhow::Result<()> { | |
| 27 | + | // One-shot operator subcommand: `mnw-cli ota publish ...`. Routed before the | |
| 28 | + | // SSH daemon boots so the same binary doubles as the OTA publisher. | |
| 29 | + | let argv: Vec<String> = std::env::args().collect(); | |
| 30 | + | if argv.get(1).map(String::as_str) == Some("ota") { | |
| 31 | + | return ota::run(&argv[2..]).await; | |
| 32 | + | } | |
| 33 | + | ||
| 26 | 34 | tracing_subscriber::fmt() | |
| 27 | 35 | .with_env_filter(EnvFilter::from_default_env().add_directive("mnw_cli=info".parse()?)) | |
| 28 | 36 | .init(); |
| @@ -0,0 +1,332 @@ | |||
| 1 | + | //! `mnw-cli ota publish` — typed OTA release publisher. | |
| 2 | + | //! | |
| 3 | + | //! Replaces the old `server/deploy/ota-publish.sh`. Authenticates against the | |
| 4 | + | //! MNW SyncKit API, creates a release, registers the artifact (which returns a | |
| 5 | + | //! presigned S3 PUT URL), uploads the bytes, and verifies the public Tauri | |
| 6 | + | //! updater endpoint now serves it. | |
| 7 | + | //! | |
| 8 | + | //! Invoked as `mnw-cli ota publish [flags]` — `main()` routes here before the | |
| 9 | + | //! SSH daemon starts when the first argument is `ota`. | |
| 10 | + | ||
| 11 | + | use std::path::PathBuf; | |
| 12 | + | ||
| 13 | + | use anyhow::{bail, Context, Result}; | |
| 14 | + | use synckit_client::{SyncKitClient, SyncKitConfig}; | |
| 15 | + | ||
| 16 | + | const DEFAULT_SERVER: &str = "https://makenot.work"; | |
| 17 | + | const ALLOWED_TARGETS: &[&str] = &["linux", "darwin", "windows"]; | |
| 18 | + | const ALLOWED_ARCHS: &[&str] = &["x86_64", "aarch64"]; | |
| 19 | + | ||
| 20 | + | /// Entry point for the `ota` subcommand. `rest` is everything after `ota`. | |
| 21 | + | pub async fn run(rest: &[String]) -> Result<()> { | |
| 22 | + | match rest.first().map(String::as_str) { | |
| 23 | + | Some("publish") => publish(&rest[1..]).await, | |
| 24 | + | Some("-h") | Some("--help") | None => { | |
| 25 | + | print_usage(); | |
| 26 | + | Ok(()) | |
| 27 | + | } | |
| 28 | + | Some(other) => { | |
| 29 | + | eprintln!("Unknown ota subcommand: {other}\n"); | |
| 30 | + | print_usage(); | |
| 31 | + | std::process::exit(2); | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | fn print_usage() { | |
| 37 | + | eprintln!( | |
| 38 | + | "Usage: mnw-cli ota publish --slug SLUG --version X.Y.Z --target OS --arch ARCH --artifact FILE\n\ | |
| 39 | + | \n\ | |
| 40 | + | Required:\n\ | |
| 41 | + | \x20 --slug App slug (e.g. goingson, audiofiles)\n\ | |
| 42 | + | \x20 --version Semver version (e.g. 0.4.1)\n\ | |
| 43 | + | \x20 --target Target OS: {}\n\ | |
| 44 | + | \x20 --arch Architecture: {}\n\ | |
| 45 | + | \x20 --artifact Path to the built artifact file\n\ | |
| 46 | + | \n\ | |
| 47 | + | Optional:\n\ | |
| 48 | + | \x20 --notes Release notes (default: empty)\n\ | |
| 49 | + | \x20 --signature Minisign signature for Tauri verification (REQUIRED for a working update)\n\ | |
| 50 | + | \x20 --email MNW account email (env MNW_OTA_EMAIL)\n\ | |
| 51 | + | \x20 --password MNW account password (env MNW_OTA_PASSWORD)\n\ | |
| 52 | + | \x20 --api-key SyncKit app API key (env MNW_OTA_API_KEY)\n\ | |
| 53 | + | \x20 --key SyncKit SDK key (env MNW_OTA_KEY)\n\ | |
| 54 | + | \x20 --server Server URL (env MNW_OTA_SERVER, default {DEFAULT_SERVER})", | |
| 55 | + | ALLOWED_TARGETS.join(", "), | |
| 56 | + | ALLOWED_ARCHS.join(", "), | |
| 57 | + | ); | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | struct PublishArgs { | |
| 61 | + | slug: String, | |
| 62 | + | version: String, | |
| 63 | + | target: String, | |
| 64 | + | arch: String, | |
| 65 | + | artifact: PathBuf, | |
| 66 | + | notes: String, | |
| 67 | + | signature: String, | |
| 68 | + | email: String, | |
| 69 | + | password: String, | |
| 70 | + | api_key: String, | |
| 71 | + | key: String, | |
| 72 | + | server: String, | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Manual Debug that redacts the credentials so they never reach logs or a | |
| 76 | + | // failing-test backtrace. | |
| 77 | + | impl std::fmt::Debug for PublishArgs { | |
| 78 | + | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 79 | + | f.debug_struct("PublishArgs") | |
| 80 | + | .field("slug", &self.slug) | |
| 81 | + | .field("version", &self.version) | |
| 82 | + | .field("target", &self.target) | |
| 83 | + | .field("arch", &self.arch) | |
| 84 | + | .field("artifact", &self.artifact) | |
| 85 | + | .field("notes", &self.notes) | |
| 86 | + | .field("signature", &self.signature) | |
| 87 | + | .field("email", &self.email) | |
| 88 | + | .field("password", &"<redacted>") | |
| 89 | + | .field("api_key", &"<redacted>") | |
| 90 | + | .field("key", &"<redacted>") | |
| 91 | + | .field("server", &self.server) | |
| 92 | + | .finish() | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | /// Parse flags with environment-variable fallbacks for the credentials. | |
| 97 | + | fn parse_args(flags: &[String]) -> Result<PublishArgs> { | |
| 98 | + | let mut slug = None; | |
| 99 | + | let mut version = None; | |
| 100 | + | let mut target = None; | |
| 101 | + | let mut arch = None; | |
| 102 | + | let mut artifact = None; | |
| 103 | + | let mut notes = String::new(); | |
| 104 | + | let mut signature = String::new(); | |
| 105 | + | let mut email = std::env::var("MNW_OTA_EMAIL").ok(); | |
| 106 | + | let mut password = std::env::var("MNW_OTA_PASSWORD").ok(); | |
| 107 | + | let mut api_key = std::env::var("MNW_OTA_API_KEY").ok(); | |
| 108 | + | let mut key = std::env::var("MNW_OTA_KEY").ok(); | |
| 109 | + | let mut server = std::env::var("MNW_OTA_SERVER").unwrap_or_else(|_| DEFAULT_SERVER.to_string()); | |
| 110 | + | ||
| 111 | + | let mut it = flags.iter(); | |
| 112 | + | while let Some(flag) = it.next() { | |
| 113 | + | let mut take = |name: &str| -> Result<String> { | |
| 114 | + | it.next() | |
| 115 | + | .cloned() | |
| 116 | + | .with_context(|| format!("{name} requires a value")) | |
| 117 | + | }; | |
| 118 | + | match flag.as_str() { | |
| 119 | + | "--slug" => slug = Some(take("--slug")?), | |
| 120 | + | "--version" => version = Some(take("--version")?), | |
| 121 | + | "--target" => target = Some(take("--target")?), | |
| 122 | + | "--arch" => arch = Some(take("--arch")?), | |
| 123 | + | "--artifact" => artifact = Some(PathBuf::from(take("--artifact")?)), | |
| 124 | + | "--notes" => notes = take("--notes")?, | |
| 125 | + | "--signature" => signature = take("--signature")?, | |
| 126 | + | "--email" => email = Some(take("--email")?), | |
| 127 | + | "--password" => password = Some(take("--password")?), | |
| 128 | + | "--api-key" => api_key = Some(take("--api-key")?), | |
| 129 | + | "--key" => key = Some(take("--key")?), | |
| 130 | + | "--server" => server = take("--server")?, | |
| 131 | + | "-h" | "--help" => { | |
| 132 | + | print_usage(); | |
| 133 | + | std::process::exit(0); | |
| 134 | + | } | |
| 135 | + | other => bail!("Unknown flag: {other}"), | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | let missing = |name: &str| anyhow::anyhow!("missing required {name}"); | |
| 140 | + | let target = target.ok_or_else(|| missing("--target"))?; | |
| 141 | + | let arch = arch.ok_or_else(|| missing("--arch"))?; | |
| 142 | + | ||
| 143 | + | if !ALLOWED_TARGETS.contains(&target.as_str()) { | |
| 144 | + | bail!("invalid --target '{target}'. Allowed: {}", ALLOWED_TARGETS.join(", ")); | |
| 145 | + | } | |
| 146 | + | if !ALLOWED_ARCHS.contains(&arch.as_str()) { | |
| 147 | + | bail!("invalid --arch '{arch}'. Allowed: {}", ALLOWED_ARCHS.join(", ")); | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | Ok(PublishArgs { | |
| 151 | + | slug: slug.ok_or_else(|| missing("--slug"))?, | |
| 152 | + | version: version.ok_or_else(|| missing("--version"))?, | |
| 153 | + | target, | |
| 154 | + | arch, | |
| 155 | + | artifact: artifact.ok_or_else(|| missing("--artifact"))?, | |
| 156 | + | notes, | |
| 157 | + | signature, | |
| 158 | + | email: email.ok_or_else(|| missing("--email / MNW_OTA_EMAIL"))?, | |
| 159 | + | password: password.ok_or_else(|| missing("--password / MNW_OTA_PASSWORD"))?, | |
| 160 | + | api_key: api_key.ok_or_else(|| missing("--api-key / MNW_OTA_API_KEY"))?, | |
| 161 | + | key: key.ok_or_else(|| missing("--key / MNW_OTA_KEY"))?, | |
| 162 | + | server, | |
| 163 | + | }) | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | async fn publish(flags: &[String]) -> Result<()> { | |
| 167 | + | let args = parse_args(flags)?; | |
| 168 | + | ||
| 169 | + | let bytes = std::fs::read(&args.artifact) | |
| 170 | + | .with_context(|| format!("reading artifact {}", args.artifact.display()))?; | |
| 171 | + | let file_size: i64 = bytes | |
| 172 | + | .len() | |
| 173 | + | .try_into() | |
| 174 | + | .context("artifact is too large to publish")?; | |
| 175 | + | if file_size == 0 { | |
| 176 | + | bail!("artifact is empty: {}", args.artifact.display()); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | if args.signature.trim().is_empty() { | |
| 180 | + | eprintln!( | |
| 181 | + | "warning: --signature is empty. Tauri's updater silently refuses an update with no \ | |
| 182 | + | signature, so installed apps will NOT apply this release. Publish with the minisign \ | |
| 183 | + | signature of the artifact for a working update." | |
| 184 | + | ); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | println!( | |
| 188 | + | "Publishing {} v{} ({}/{}, {} bytes) to {}", | |
| 189 | + | args.slug, args.version, args.target, args.arch, file_size, args.server | |
| 190 | + | ); | |
| 191 | + | ||
| 192 | + | let client = SyncKitClient::new(SyncKitConfig { | |
| 193 | + | server_url: args.server.clone(), | |
| 194 | + | api_key: args.api_key.clone(), | |
| 195 | + | }); | |
| 196 | + | ||
| 197 | + | print!(" authenticating... "); | |
| 198 | + | client | |
| 199 | + | .authenticate(&args.email, &args.password, &args.key) | |
| 200 | + | .await | |
| 201 | + | .context("authentication failed")?; | |
| 202 | + | let app_id = client | |
| 203 | + | .session_info() | |
| 204 | + | .map(|s| s.app_id.to_string()) | |
| 205 | + | .unwrap_or_default(); | |
| 206 | + | println!("ok (app {app_id})"); | |
| 207 | + | ||
| 208 | + | print!(" creating release v{}... ", args.version); | |
| 209 | + | let release = client | |
| 210 | + | .ota_create_release(&args.version, &args.notes, &args.signature) | |
| 211 | + | .await | |
| 212 | + | .context("create release failed")?; | |
| 213 | + | println!("ok (release {})", release.id); | |
| 214 | + | ||
| 215 | + | print!(" registering artifact... "); | |
| 216 | + | let upload = client | |
| 217 | + | .ota_register_artifact(release.id, &args.target, &args.arch, file_size) | |
| 218 | + | .await | |
| 219 | + | .context("register artifact failed")?; | |
| 220 | + | println!("ok ({})", upload.s3_key); | |
| 221 | + | ||
| 222 | + | print!(" uploading {file_size} bytes... "); | |
| 223 | + | client | |
| 224 | + | .ota_upload_artifact(&upload.upload_url, bytes) | |
| 225 | + | .await | |
| 226 | + | .context("artifact upload failed")?; | |
| 227 | + | println!("ok"); | |
| 228 | + | ||
| 229 | + | print!(" verifying updater endpoint... "); | |
| 230 | + | match client | |
| 231 | + | .ota_updater_check(&args.slug, &args.target, &args.arch, "0.0.1") | |
| 232 | + | .await | |
| 233 | + | .context("updater check failed")? | |
| 234 | + | { | |
| 235 | + | Some(manifest) if manifest.version == args.version => { | |
| 236 | + | println!("ok (serving v{})", manifest.version); | |
| 237 | + | } | |
| 238 | + | Some(manifest) => { | |
| 239 | + | println!( | |
| 240 | + | "warning: updater serves v{} but just published v{} (a newer release may exist)", | |
| 241 | + | manifest.version, args.version | |
| 242 | + | ); | |
| 243 | + | } | |
| 244 | + | None => { | |
| 245 | + | println!( | |
| 246 | + | "warning: updater returned no update (204). The release was created but is not \ | |
| 247 | + | being served for {}/{} yet.", | |
| 248 | + | args.target, args.arch | |
| 249 | + | ); | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | println!( | |
| 254 | + | "\nPublished {} v{} ({}/{})\nUpdater URL: {}/api/v1/sync/ota/{}/{}/{}/{}", | |
| 255 | + | args.slug, | |
| 256 | + | args.version, | |
| 257 | + | args.target, | |
| 258 | + | args.arch, | |
| 259 | + | args.server.trim_end_matches('/'), | |
| 260 | + | args.slug, | |
| 261 | + | args.target, | |
| 262 | + | args.arch, | |
| 263 | + | args.version, | |
| 264 | + | ); | |
| 265 | + | Ok(()) | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | #[cfg(test)] | |
| 269 | + | mod tests { | |
| 270 | + | use super::*; | |
| 271 | + | ||
| 272 | + | fn base_flags() -> Vec<String> { | |
| 273 | + | [ | |
| 274 | + | "--slug", "goingson", | |
| 275 | + | "--version", "0.4.1", | |
| 276 | + | "--target", "darwin", | |
| 277 | + | "--arch", "aarch64", | |
| 278 | + | "--artifact", "/tmp/x", | |
| 279 | + | "--email", "me@example.com", | |
| 280 | + | "--password", "pw", | |
| 281 | + | "--api-key", "ak", | |
| 282 | + | "--key", "sdk", | |
| 283 | + | "--server", "https://example.test", | |
| 284 | + | ] | |
| 285 | + | .iter() | |
| 286 | + | .map(|s| s.to_string()) | |
| 287 | + | .collect() | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | #[test] | |
| 291 | + | fn parses_full_flag_set() { | |
| 292 | + | let a = parse_args(&base_flags()).unwrap(); | |
| 293 | + | assert_eq!(a.slug, "goingson"); | |
| 294 | + | assert_eq!(a.version, "0.4.1"); | |
| 295 | + | assert_eq!(a.target, "darwin"); | |
| 296 | + | assert_eq!(a.arch, "aarch64"); | |
| 297 | + | assert_eq!(a.server, "https://example.test"); | |
| 298 | + | assert!(a.notes.is_empty()); | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | #[test] | |
| 302 | + | fn rejects_invalid_target() { | |
| 303 | + | let mut flags = base_flags(); | |
| 304 | + | let i = flags.iter().position(|f| f == "darwin").unwrap(); | |
| 305 | + | flags[i] = "macos".to_string(); | |
| 306 | + | let err = parse_args(&flags).unwrap_err().to_string(); | |
| 307 | + | assert!(err.contains("invalid --target"), "{err}"); | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | #[test] | |
| 311 | + | fn rejects_invalid_arch() { | |
| 312 | + | let mut flags = base_flags(); | |
| 313 | + | let i = flags.iter().position(|f| f == "aarch64").unwrap(); | |
| 314 | + | flags[i] = "arm64".to_string(); | |
| 315 | + | let err = parse_args(&flags).unwrap_err().to_string(); | |
| 316 | + | assert!(err.contains("invalid --arch"), "{err}"); | |
| 317 | + | } | |
| 318 | + | ||
| 319 | + | #[test] | |
| 320 | + | fn missing_required_flag_is_reported() { | |
| 321 | + | // Drop the trailing --server pair and the --slug pair. | |
| 322 | + | let flags: Vec<String> = base_flags().into_iter().skip(2).collect(); // skip --slug goingson | |
| 323 | + | let err = parse_args(&flags).unwrap_err().to_string(); | |
| 324 | + | assert!(err.contains("--slug"), "{err}"); | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | #[test] | |
| 328 | + | fn flag_without_value_errors() { | |
| 329 | + | let err = parse_args(&["--slug".to_string()]).unwrap_err().to_string(); | |
| 330 | + | assert!(err.contains("--slug requires a value"), "{err}"); | |
| 331 | + | } | |
| 332 | + | } |
| @@ -1,6 +1,11 @@ | |||
| 1 | 1 | #!/usr/bin/env bash | |
| 2 | 2 | # ota-publish.sh — Publish an OTA update to makenot.work | |
| 3 | 3 | # | |
| 4 | + | # DEPRECATED: superseded by `mnw-cli ota publish` (typed, tested, retried). | |
| 5 | + | # This script also predates the SyncKit SDK-key auth field, so it no longer | |
| 6 | + | # authenticates against the current server. Kept for reference only. | |
| 7 | + | # See: _private/docs/meta/ota-release-runbook.md | |
| 8 | + | # | |
| 4 | 9 | # Usage: | |
| 5 | 10 | # ./deploy/ota-publish.sh --slug goingson --version 0.2.2 --target linux --arch x86_64 \ | |
| 6 | 11 | # --artifact path/to/bundle.tar.gz [--notes "Bug fixes"] [--signature "..."] |
| @@ -53,11 +53,13 @@ mod auth; | |||
| 53 | 53 | mod blob; | |
| 54 | 54 | mod encryption; | |
| 55 | 55 | pub(crate) mod helpers; | |
| 56 | + | mod ota; | |
| 56 | 57 | mod rotation; | |
| 57 | 58 | mod subscribe; | |
| 58 | 59 | pub mod subscription; | |
| 59 | 60 | mod sync; | |
| 60 | 61 | ||
| 62 | + | pub use ota::{OtaArtifactUpload, OtaManifest, OtaRelease}; | |
| 61 | 63 | pub use subscribe::SyncNotifyStream; | |
| 62 | 64 | ||
| 63 | 65 | use parking_lot::RwLock; |
| @@ -0,0 +1,265 @@ | |||
| 1 | + | //! OTA (Over-The-Air) release publishing. | |
| 2 | + | //! | |
| 3 | + | //! These methods drive the app-owner side of the MNW OTA system: create a | |
| 4 | + | //! release, register an artifact (which returns a presigned S3 PUT URL), upload | |
| 5 | + | //! the bytes, and verify the public Tauri updater endpoint now serves it. They | |
| 6 | + | //! reuse the authenticated [`SyncKitClient`] session (JWT + app id), so a | |
| 7 | + | //! publisher authenticates once with [`authenticate`](SyncKitClient::authenticate) | |
| 8 | + | //! and then calls these in sequence. | |
| 9 | + | //! | |
| 10 | + | //! Unlike blobs, OTA artifacts are NOT end-to-end encrypted — they are public | |
| 11 | + | //! downloads served to every installed app, so the bytes are uploaded as-is. | |
| 12 | + | //! | |
| 13 | + | //! Server contract: `server/src/routes/ota.rs`. The Tauri updater manifest | |
| 14 | + | //! ([`OtaManifest`]) field names are load-bearing — Tauri's updater plugin | |
| 15 | + | //! deserializes exactly these five fields. | |
| 16 | + | ||
| 17 | + | use bytes::Bytes; | |
| 18 | + | use serde::{Deserialize, Serialize}; | |
| 19 | + | use tracing::instrument; | |
| 20 | + | use uuid::Uuid; | |
| 21 | + | ||
| 22 | + | use super::helpers::check_response; | |
| 23 | + | use super::SyncKitClient; | |
| 24 | + | use crate::error::Result; | |
| 25 | + | ||
| 26 | + | /// A created OTA release (subset of the server's release response). | |
| 27 | + | #[derive(Debug, Clone, Deserialize)] | |
| 28 | + | pub struct OtaRelease { | |
| 29 | + | /// Release id, used when registering artifacts. | |
| 30 | + | pub id: Uuid, | |
| 31 | + | pub version: String, | |
| 32 | + | pub notes: String, | |
| 33 | + | pub signature: String, | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | /// A presigned upload target for an OTA artifact. | |
| 37 | + | #[derive(Debug, Clone, Deserialize)] | |
| 38 | + | pub struct OtaArtifactUpload { | |
| 39 | + | /// Presigned S3 PUT URL — upload the artifact bytes here. | |
| 40 | + | pub upload_url: String, | |
| 41 | + | /// The S3 object key the artifact will live at. | |
| 42 | + | pub s3_key: String, | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | /// The Tauri-compatible updater manifest returned by the public updater check. | |
| 46 | + | /// | |
| 47 | + | /// Field names mirror the server's `TauriUpdaterResponse` exactly; Tauri's | |
| 48 | + | /// updater plugin reads these five and nothing else. | |
| 49 | + | #[derive(Debug, Clone, Deserialize)] | |
| 50 | + | pub struct OtaManifest { | |
| 51 | + | pub version: String, | |
| 52 | + | pub url: String, | |
| 53 | + | pub signature: String, | |
| 54 | + | pub notes: String, | |
| 55 | + | pub pub_date: String, | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | #[derive(Serialize)] | |
| 59 | + | struct CreateReleaseBody<'a> { | |
| 60 | + | version: &'a str, | |
| 61 | + | notes: &'a str, | |
| 62 | + | signature: &'a str, | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | #[derive(Serialize)] | |
| 66 | + | struct RegisterArtifactBody<'a> { | |
| 67 | + | target: &'a str, | |
| 68 | + | arch: &'a str, | |
| 69 | + | file_size: i64, | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | impl SyncKitClient { | |
| 73 | + | /// Base URL for OTA management endpoints scoped to the authenticated app. | |
| 74 | + | fn ota_app_base(&self) -> Result<String> { | |
| 75 | + | let (app_id, _user_id) = self.require_session_ids()?; | |
| 76 | + | let base = self.config().server_url.trim_end_matches('/'); | |
| 77 | + | Ok(format!("{base}/api/v1/sync/ota/apps/{app_id}")) | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | /// Create a new OTA release for the authenticated app. | |
| 81 | + | /// | |
| 82 | + | /// Pass an empty `signature` only for unsigned platforms — Tauri's updater | |
| 83 | + | /// silently refuses an update whose manifest signature is empty, so a real | |
| 84 | + | /// release must carry the minisign signature of the artifact. | |
| 85 | + | #[instrument(skip(self, signature))] | |
| 86 | + | pub async fn ota_create_release( | |
| 87 | + | &self, | |
| 88 | + | version: &str, | |
| 89 | + | notes: &str, | |
| 90 | + | signature: &str, | |
| 91 | + | ) -> Result<OtaRelease> { | |
| 92 | + | let token = self.require_token()?; | |
| 93 | + | let url = format!("{}/releases", self.ota_app_base()?); | |
| 94 | + | ||
| 95 | + | let body = Bytes::from(serde_json::to_vec(&CreateReleaseBody { | |
| 96 | + | version, | |
| 97 | + | notes, | |
| 98 | + | signature, | |
| 99 | + | })?); | |
| 100 | + | ||
| 101 | + | self.retry_request_json(|| { | |
| 102 | + | let req = self | |
| 103 | + | .http | |
| 104 | + | .post(&url) | |
| 105 | + | .bearer_auth(&token) | |
| 106 | + | .header("content-type", "application/json") | |
| 107 | + | .body(body.clone()); | |
| 108 | + | async move { check_response(req.send().await?).await } | |
| 109 | + | }) | |
| 110 | + | .await | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | /// Register an artifact for a release and obtain a presigned upload URL. | |
| 114 | + | /// | |
| 115 | + | /// `target` is the OS (`linux`/`darwin`/`windows`), `arch` is the CPU | |
| 116 | + | /// (`x86_64`/`aarch64`), and `file_size` is the artifact size in bytes. | |
| 117 | + | #[instrument(skip(self))] | |
| 118 | + | pub async fn ota_register_artifact( | |
| 119 | + | &self, | |
| 120 | + | release_id: Uuid, | |
| 121 | + | target: &str, | |
| 122 | + | arch: &str, | |
| 123 | + | file_size: i64, | |
| 124 | + | ) -> Result<OtaArtifactUpload> { | |
| 125 | + | let token = self.require_token()?; | |
| 126 | + | let url = format!("{}/releases/{release_id}/artifacts", self.ota_app_base()?); | |
| 127 | + | ||
| 128 | + | let body = Bytes::from(serde_json::to_vec(&RegisterArtifactBody { | |
| 129 | + | target, | |
| 130 | + | arch, | |
| 131 | + | file_size, | |
| 132 | + | })?); | |
| 133 | + | ||
| 134 | + | self.retry_request_json(|| { | |
| 135 | + | let req = self | |
| 136 | + | .http | |
| 137 | + | .post(&url) | |
| 138 | + | .bearer_auth(&token) | |
| 139 | + | .header("content-type", "application/json") | |
| 140 | + | .body(body.clone()); | |
| 141 | + | async move { check_response(req.send().await?).await } | |
| 142 | + | }) | |
| 143 | + | .await | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | /// Upload artifact bytes to S3 via a presigned PUT URL. | |
| 147 | + | /// | |
| 148 | + | /// The bytes are sent as-is (no encryption — OTA artifacts are public). | |
| 149 | + | #[instrument(skip(self, presigned_url, data))] | |
| 150 | + | pub async fn ota_upload_artifact(&self, presigned_url: &str, data: Vec<u8>) -> Result<()> { | |
| 151 | + | let data = Bytes::from(data); | |
| 152 | + | self.retry_request(|| { | |
| 153 | + | let req = self | |
| 154 | + | .http | |
| 155 | + | .put(presigned_url) | |
| 156 | + | .header("content-type", "application/octet-stream") | |
| 157 | + | .body(data.clone()); | |
| 158 | + | async move { check_response(req.send().await?).await } | |
| 159 | + | }) | |
| 160 | + | .await?; | |
| 161 | + | Ok(()) | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | /// Check the public Tauri updater endpoint. | |
| 165 | + | /// | |
| 166 | + | /// Returns `Some(manifest)` when a newer version than `current_version` is | |
| 167 | + | /// available for `slug`/`target`/`arch`, or `None` when the client is up to | |
| 168 | + | /// date (HTTP 204). Use this to verify a freshly published release is live. | |
| 169 | + | #[instrument(skip(self))] | |
| 170 | + | pub async fn ota_updater_check( | |
| 171 | + | &self, | |
| 172 | + | slug: &str, | |
| 173 | + | target: &str, | |
| 174 | + | arch: &str, | |
| 175 | + | current_version: &str, | |
| 176 | + | ) -> Result<Option<OtaManifest>> { | |
| 177 | + | let base = self.config().server_url.trim_end_matches('/'); | |
| 178 | + | let url = format!("{base}/api/v1/sync/ota/{slug}/{target}/{arch}/{current_version}"); | |
| 179 | + | ||
| 180 | + | let resp = self | |
| 181 | + | .retry_request(|| { | |
| 182 | + | let req = self.http.get(&url); | |
| 183 | + | async move { check_response(req.send().await?).await } | |
| 184 | + | }) | |
| 185 | + | .await?; | |
| 186 | + | ||
| 187 | + | if resp.status() == reqwest::StatusCode::NO_CONTENT { | |
| 188 | + | return Ok(None); | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | Ok(Some(resp.json::<OtaManifest>().await?)) | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | #[cfg(test)] | |
| 196 | + | mod tests { | |
| 197 | + | use super::*; | |
| 198 | + | ||
| 199 | + | #[test] | |
| 200 | + | fn create_release_body_serializes_expected_fields() { | |
| 201 | + | let body = CreateReleaseBody { | |
| 202 | + | version: "0.4.1", | |
| 203 | + | notes: "Bug fixes", | |
| 204 | + | signature: "RWS...==", | |
| 205 | + | }; | |
| 206 | + | let v: serde_json::Value = serde_json::to_value(&body).unwrap(); | |
| 207 | + | assert_eq!(v["version"], "0.4.1"); | |
| 208 | + | assert_eq!(v["notes"], "Bug fixes"); | |
| 209 | + | assert_eq!(v["signature"], "RWS...=="); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | #[test] | |
| 213 | + | fn register_artifact_body_serializes_expected_fields() { | |
| 214 | + | let body = RegisterArtifactBody { | |
| 215 | + | target: "darwin", | |
| 216 | + | arch: "aarch64", | |
| 217 | + | file_size: 12_345, | |
| 218 | + | }; | |
| 219 | + | let v: serde_json::Value = serde_json::to_value(&body).unwrap(); | |
| 220 | + | assert_eq!(v["target"], "darwin"); | |
| 221 | + | assert_eq!(v["arch"], "aarch64"); | |
| 222 | + | assert_eq!(v["file_size"], 12_345); | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | #[test] | |
| 226 | + | fn release_response_deserializes_with_uuid_id() { | |
| 227 | + | let json = r#"{ | |
| 228 | + | "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", | |
| 229 | + | "version": "0.4.1", | |
| 230 | + | "notes": "", | |
| 231 | + | "signature": "RWS=", | |
| 232 | + | "pub_date": "2026-06-07T00:00:00Z", | |
| 233 | + | "created_at": "2026-06-07T00:00:00Z" | |
| 234 | + | }"#; | |
| 235 | + | let r: OtaRelease = serde_json::from_str(json).unwrap(); | |
| 236 | + | assert_eq!(r.version, "0.4.1"); | |
| 237 | + | assert_eq!( | |
| 238 | + | r.id, | |
| 239 | + | Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap() | |
| 240 | + | ); | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | #[test] | |
| 244 | + | fn artifact_upload_response_deserializes() { | |
| 245 | + | let json = r#"{"upload_url": "https://s3.example/put?sig=abc", "s3_key": "ota/app/0.4.1/darwin/aarch64/artifact"}"#; | |
| 246 | + | let u: OtaArtifactUpload = serde_json::from_str(json).unwrap(); | |
| 247 | + | assert_eq!(u.upload_url, "https://s3.example/put?sig=abc"); | |
| 248 | + | assert!(u.s3_key.ends_with("/artifact")); | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | #[test] | |
| 252 | + | fn manifest_deserializes_tauri_five_fields() { | |
| 253 | + | let json = r#"{ | |
| 254 | + | "version": "0.4.1", | |
| 255 | + | "url": "https://makenot.work/api/sync/ota/goingson/download/abc/darwin/aarch64", | |
| 256 | + | "signature": "RWS=", | |
| 257 | + | "notes": "Bug fixes", | |
| 258 | + | "pub_date": "2026-06-07T00:00:00+00:00" | |
| 259 | + | }"#; | |
| 260 | + | let m: OtaManifest = serde_json::from_str(json).unwrap(); | |
| 261 | + | assert_eq!(m.version, "0.4.1"); | |
| 262 | + | assert!(m.url.contains("/download/")); | |
| 263 | + | assert_eq!(m.signature, "RWS="); | |
| 264 | + | } | |
| 265 | + | } |
| @@ -11,16 +11,25 @@ | |||
| 11 | 11 | //! `load_key` will return a `Keychain` error. | |
| 12 | 12 | //! - **Windows**: Credential Manager. | |
| 13 | 13 | ||
| 14 | - | use crate::error::{Result, SyncKitError}; | |
| 14 | + | use crate::error::Result; | |
| 15 | + | // Only the keychain code paths (and the test module) construct SyncKitError | |
| 16 | + | // directly; without the feature a plain lib build would see it as unused. | |
| 17 | + | #[cfg(any(feature = "keychain", test))] | |
| 18 | + | use crate::error::SyncKitError; | |
| 19 | + | #[cfg(any(feature = "keychain", test))] | |
| 15 | 20 | use base64::{engine::general_purpose::STANDARD as B64, Engine}; | |
| 16 | 21 | use uuid::Uuid; | |
| 17 | 22 | ||
| 23 | + | // These keychain helpers are only referenced by the keychain code paths and the | |
| 24 | + | // test module; gate them so a no-keychain lib build stays warning-clean. | |
| 25 | + | #[cfg(any(feature = "keychain", test))] | |
| 18 | 26 | const SERVICE_PREFIX: &str = "synckit"; | |
| 19 | 27 | ||
| 20 | 28 | /// Build the keychain service name: `"synckit:<app_id>"`. | |
| 21 | 29 | /// | |
| 22 | 30 | /// Each SyncKit app gets its own keychain namespace so that keys from | |
| 23 | 31 | /// different apps never collide. | |
| 32 | + | #[cfg(any(feature = "keychain", test))] | |
| 24 | 33 | fn service_name(app_id: Uuid) -> String { | |
| 25 | 34 | format!("{SERVICE_PREFIX}:{app_id}") | |
| 26 | 35 | } | |
| @@ -29,6 +38,7 @@ fn service_name(app_id: Uuid) -> String { | |||
| 29 | 38 | /// | |
| 30 | 39 | /// Combined with `service_name`, this uniquely identifies the keychain entry | |
| 31 | 40 | /// for a given (app, user) pair. | |
| 41 | + | #[cfg(any(feature = "keychain", test))] | |
| 32 | 42 | fn user_key(user_id: Uuid) -> String { | |
| 33 | 43 | user_id.to_string() | |
| 34 | 44 | } |
| @@ -51,6 +51,7 @@ pub mod types; | |||
| 51 | 51 | ||
| 52 | 52 | // Re-exports for convenience | |
| 53 | 53 | pub use client::{validate_api_key, SessionInfo, SyncKitClient, SyncKitConfig, SyncNotifyStream}; | |
| 54 | + | pub use client::{OtaArtifactUpload, OtaManifest, OtaRelease}; | |
| 54 | 55 | pub use client::subscription::{ | |
| 55 | 56 | AccountInfo, AppPricing, BillingInterval, CheckoutResponse, PriceQuote, SubscriptionStatus, | |
| 56 | 57 | }; |