Git hosting and a container registry in one Rust binary (axum + Astro)
Foundation: workspace, schema, auth, permissions, R2 storage, git smart HTTP, push processing, size-limit hook, page shell
59 files changed, +11457 -0
+7-0.gitignore
| @@ -5,3 +5,10 @@ gitea/ | ||
| 5 | 5 | secrets.md |
| 6 | 6 | .env |
| 7 | 7 | .env.* |
| 8 | + | |
| 9 | +# Build output and local state | |
| 10 | +target/ | |
| 11 | +frontend/dist/ | |
| 12 | +frontend/node_modules/ | |
| 13 | +frontend/.astro/ | |
| 14 | +data/ |
+5273-0Cargo.lock
| @@ -0,0 +1,5273 @@ | ||
| 1 | +# This file is automatically @generated by Cargo. | |
| 2 | +# It is not intended for manual editing. | |
| 3 | +version = 4 | |
| 4 | + | |
| 5 | +[[package]] | |
| 6 | +name = "adler2" | |
| 7 | +version = "2.0.1" | |
| 8 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9 | +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" | |
| 10 | + | |
| 11 | +[[package]] | |
| 12 | +name = "aead" | |
| 13 | +version = "0.6.1" | |
| 14 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 15 | +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" | |
| 16 | +dependencies = [ | |
| 17 | + "crypto-common 0.2.2", | |
| 18 | + "inout", | |
| 19 | +] | |
| 20 | + | |
| 21 | +[[package]] | |
| 22 | +name = "aes" | |
| 23 | +version = "0.9.3" | |
| 24 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 25 | +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" | |
| 26 | +dependencies = [ | |
| 27 | + "cipher", | |
| 28 | + "cpubits", | |
| 29 | + "cpufeatures 0.3.1", | |
| 30 | + "zeroize", | |
| 31 | +] | |
| 32 | + | |
| 33 | +[[package]] | |
| 34 | +name = "aes-gcm" | |
| 35 | +version = "0.11.1" | |
| 36 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 37 | +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" | |
| 38 | +dependencies = [ | |
| 39 | + "aead", | |
| 40 | + "aes", | |
| 41 | + "cipher", | |
| 42 | + "ctr", | |
| 43 | + "ctutils", | |
| 44 | + "ghash", | |
| 45 | + "zeroize", | |
| 46 | +] | |
| 47 | + | |
| 48 | +[[package]] | |
| 49 | +name = "aho-corasick" | |
| 50 | +version = "1.1.5" | |
| 51 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 52 | +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" | |
| 53 | +dependencies = [ | |
| 54 | + "memchr", | |
| 55 | +] | |
| 56 | + | |
| 57 | +[[package]] | |
| 58 | +name = "allocator-api2" | |
| 59 | +version = "0.2.21" | |
| 60 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 61 | +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" | |
| 62 | + | |
| 63 | +[[package]] | |
| 64 | +name = "android_system_properties" | |
| 65 | +version = "0.1.6" | |
| 66 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 67 | +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" | |
| 68 | +dependencies = [ | |
| 69 | + "libc", | |
| 70 | +] | |
| 71 | + | |
| 72 | +[[package]] | |
| 73 | +name = "anstream" | |
| 74 | +version = "1.0.0" | |
| 75 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 76 | +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" | |
| 77 | +dependencies = [ | |
| 78 | + "anstyle", | |
| 79 | + "anstyle-parse", | |
| 80 | + "anstyle-query", | |
| 81 | + "anstyle-wincon", | |
| 82 | + "colorchoice", | |
| 83 | + "is_terminal_polyfill", | |
| 84 | + "utf8parse", | |
| 85 | +] | |
| 86 | + | |
| 87 | +[[package]] | |
| 88 | +name = "anstyle" | |
| 89 | +version = "1.0.14" | |
| 90 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 91 | +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" | |
| 92 | + | |
| 93 | +[[package]] | |
| 94 | +name = "anstyle-parse" | |
| 95 | +version = "1.0.0" | |
| 96 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 97 | +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" | |
| 98 | +dependencies = [ | |
| 99 | + "utf8parse", | |
| 100 | +] | |
| 101 | + | |
| 102 | +[[package]] | |
| 103 | +name = "anstyle-query" | |
| 104 | +version = "1.1.5" | |
| 105 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 106 | +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" | |
| 107 | +dependencies = [ | |
| 108 | + "windows-sys 0.61.2", | |
| 109 | +] | |
| 110 | + | |
| 111 | +[[package]] | |
| 112 | +name = "anstyle-wincon" | |
| 113 | +version = "3.0.11" | |
| 114 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 115 | +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" | |
| 116 | +dependencies = [ | |
| 117 | + "anstyle", | |
| 118 | + "once_cell_polyfill", | |
| 119 | + "windows-sys 0.61.2", | |
| 120 | +] | |
| 121 | + | |
| 122 | +[[package]] | |
| 123 | +name = "anyhow" | |
| 124 | +version = "1.0.104" | |
| 125 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 126 | +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" | |
| 127 | + | |
| 128 | +[[package]] | |
| 129 | +name = "argon2" | |
| 130 | +version = "0.6.0" | |
| 131 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 132 | +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" | |
| 133 | +dependencies = [ | |
| 134 | + "base64ct", | |
| 135 | + "blake2", | |
| 136 | + "cpufeatures 0.3.1", | |
| 137 | + "password-hash", | |
| 138 | +] | |
| 139 | + | |
| 140 | +[[package]] | |
| 141 | +name = "async-compression" | |
| 142 | +version = "0.4.50" | |
| 143 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 144 | +checksum = "ee19bd99b43e3691acbad4e840420a4881cea6c0b66a208125a824f8fd53f5a1" | |
| 145 | +dependencies = [ | |
| 146 | + "compression-codecs", | |
| 147 | + "compression-core", | |
| 148 | + "pin-project-lite", | |
| 149 | + "tokio", | |
| 150 | +] | |
| 151 | + | |
| 152 | +[[package]] | |
| 153 | +name = "async-trait" | |
| 154 | +version = "0.1.92" | |
| 155 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 156 | +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" | |
| 157 | +dependencies = [ | |
| 158 | + "proc-macro2", | |
| 159 | + "quote", | |
| 160 | + "syn 3.0.6", | |
| 161 | +] | |
| 162 | + | |
| 163 | +[[package]] | |
| 164 | +name = "atoi" | |
| 165 | +version = "2.0.0" | |
| 166 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 167 | +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" | |
| 168 | +dependencies = [ | |
| 169 | + "num-traits", | |
| 170 | +] | |
| 171 | + | |
| 172 | +[[package]] | |
| 173 | +name = "atomic-waker" | |
| 174 | +version = "1.1.2" | |
| 175 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 176 | +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | |
| 177 | + | |
| 178 | +[[package]] | |
| 179 | +name = "autocfg" | |
| 180 | +version = "1.5.1" | |
| 181 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 182 | +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" | |
| 183 | + | |
| 184 | +[[package]] | |
| 185 | +name = "aws-lc-rs" | |
| 186 | +version = "1.18.1" | |
| 187 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 188 | +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" | |
| 189 | +dependencies = [ | |
| 190 | + "aws-lc-sys", | |
| 191 | + "untrusted 0.7.1", | |
| 192 | + "zeroize", | |
| 193 | +] | |
| 194 | + | |
| 195 | +[[package]] | |
| 196 | +name = "aws-lc-sys" | |
| 197 | +version = "0.45.0" | |
| 198 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 199 | +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" | |
| 200 | +dependencies = [ | |
| 201 | + "cc", | |
| 202 | + "cmake", | |
| 203 | + "dunce", | |
| 204 | + "fs_extra", | |
| 205 | + "pkg-config", | |
| 206 | +] | |
| 207 | + | |
| 208 | +[[package]] | |
| 209 | +name = "axum" | |
| 210 | +version = "0.8.9" | |
| 211 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 212 | +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" | |
| 213 | +dependencies = [ | |
| 214 | + "axum-core", | |
| 215 | + "axum-macros", | |
| 216 | + "base64 0.22.1", | |
| 217 | + "bytes", | |
| 218 | + "form_urlencoded", | |
| 219 | + "futures-util", | |
| 220 | + "http", | |
| 221 | + "http-body", | |
| 222 | + "http-body-util", | |
| 223 | + "hyper", | |
| 224 | + "hyper-util", | |
| 225 | + "itoa", | |
| 226 | + "matchit", | |
| 227 | + "memchr", | |
| 228 | + "mime", | |
| 229 | + "multer", | |
| 230 | + "percent-encoding", | |
| 231 | + "pin-project-lite", | |
| 232 | + "serde_core", | |
| 233 | + "serde_json", | |
| 234 | + "serde_path_to_error", | |
| 235 | + "serde_urlencoded", | |
| 236 | + "sha1 0.10.7", | |
| 237 | + "sync_wrapper", | |
| 238 | + "tokio", | |
| 239 | + "tokio-tungstenite", | |
| 240 | + "tower", | |
| 241 | + "tower-layer", | |
| 242 | + "tower-service", | |
| 243 | + "tracing", | |
| 244 | +] | |
| 245 | + | |
| 246 | +[[package]] | |
| 247 | +name = "axum-core" | |
| 248 | +version = "0.5.6" | |
| 249 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 250 | +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" | |
| 251 | +dependencies = [ | |
| 252 | + "bytes", | |
| 253 | + "futures-core", | |
| 254 | + "http", | |
| 255 | + "http-body", | |
| 256 | + "http-body-util", | |
| 257 | + "mime", | |
| 258 | + "pin-project-lite", | |
| 259 | + "sync_wrapper", | |
| 260 | + "tower-layer", | |
| 261 | + "tower-service", | |
| 262 | + "tracing", | |
| 263 | +] | |
| 264 | + | |
| 265 | +[[package]] | |
| 266 | +name = "axum-extra" | |
| 267 | +version = "0.12.6" | |
| 268 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 269 | +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" | |
| 270 | +dependencies = [ | |
| 271 | + "axum", | |
| 272 | + "axum-core", | |
| 273 | + "bytes", | |
| 274 | + "cookie", | |
| 275 | + "futures-core", | |
| 276 | + "futures-util", | |
| 277 | + "headers", | |
| 278 | + "http", | |
| 279 | + "http-body", | |
| 280 | + "http-body-util", | |
| 281 | + "mime", | |
| 282 | + "pin-project-lite", | |
| 283 | + "tower-layer", | |
| 284 | + "tower-service", | |
| 285 | + "tracing", | |
| 286 | +] | |
| 287 | + | |
| 288 | +[[package]] | |
| 289 | +name = "axum-macros" | |
| 290 | +version = "0.5.1" | |
| 291 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 292 | +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" | |
| 293 | +dependencies = [ | |
| 294 | + "proc-macro2", | |
| 295 | + "quote", | |
| 296 | + "syn 2.0.119", | |
| 297 | +] | |
| 298 | + | |
| 299 | +[[package]] | |
| 300 | +name = "base16ct" | |
| 301 | +version = "1.0.0" | |
| 302 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 303 | +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" | |
| 304 | + | |
| 305 | +[[package]] | |
| 306 | +name = "base64" | |
| 307 | +version = "0.22.1" | |
| 308 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 309 | +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" | |
| 310 | + | |
| 311 | +[[package]] | |
| 312 | +name = "base64" | |
| 313 | +version = "0.23.1" | |
| 314 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 315 | +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" | |
| 316 | + | |
| 317 | +[[package]] | |
| 318 | +name = "base64ct" | |
| 319 | +version = "1.8.3" | |
| 320 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 321 | +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" | |
| 322 | + | |
| 323 | +[[package]] | |
| 324 | +name = "bcrypt-pbkdf" | |
| 325 | +version = "0.11.0" | |
| 326 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 327 | +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" | |
| 328 | +dependencies = [ | |
| 329 | + "blowfish", | |
| 330 | + "pbkdf2", | |
| 331 | + "sha2 0.11.0", | |
| 332 | +] | |
| 333 | + | |
| 334 | +[[package]] | |
| 335 | +name = "bincode" | |
| 336 | +version = "1.3.3" | |
| 337 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 338 | +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" | |
| 339 | +dependencies = [ | |
| 340 | + "serde", | |
| 341 | +] | |
| 342 | + | |
| 343 | +[[package]] | |
| 344 | +name = "bit-set" | |
| 345 | +version = "0.8.0" | |
| 346 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 347 | +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" | |
| 348 | +dependencies = [ | |
| 349 | + "bit-vec", | |
| 350 | +] | |
| 351 | + | |
| 352 | +[[package]] | |
| 353 | +name = "bit-vec" | |
| 354 | +version = "0.8.0" | |
| 355 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 356 | +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" | |
| 357 | + | |
| 358 | +[[package]] | |
| 359 | +name = "bitflags" | |
| 360 | +version = "1.3.2" | |
| 361 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 362 | +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | |
| 363 | + | |
| 364 | +[[package]] | |
| 365 | +name = "bitflags" | |
| 366 | +version = "2.13.2" | |
| 367 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 368 | +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" | |
| 369 | +dependencies = [ | |
| 370 | + "serde_core", | |
| 371 | +] | |
| 372 | + | |
| 373 | +[[package]] | |
| 374 | +name = "blake2" | |
| 375 | +version = "0.11.0" | |
| 376 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 377 | +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" | |
| 378 | +dependencies = [ | |
| 379 | + "digest 0.11.3", | |
| 380 | +] | |
| 381 | + | |
| 382 | +[[package]] | |
| 383 | +name = "block-buffer" | |
| 384 | +version = "0.10.4" | |
| 385 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 386 | +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | |
| 387 | +dependencies = [ | |
| 388 | + "generic-array 0.14.7", | |
| 389 | +] | |
| 390 | + | |
| 391 | +[[package]] | |
| 392 | +name = "block-buffer" | |
| 393 | +version = "0.12.1" | |
| 394 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 395 | +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" | |
| 396 | +dependencies = [ | |
| 397 | + "hybrid-array", | |
| 398 | + "zeroize", | |
| 399 | +] | |
| 400 | + | |
| 401 | +[[package]] | |
| 402 | +name = "block-padding" | |
| 403 | +version = "0.4.2" | |
| 404 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 405 | +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" | |
| 406 | +dependencies = [ | |
| 407 | + "hybrid-array", | |
| 408 | +] | |
| 409 | + | |
| 410 | +[[package]] | |
| 411 | +name = "blowfish" | |
| 412 | +version = "0.10.0" | |
| 413 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 414 | +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" | |
| 415 | +dependencies = [ | |
| 416 | + "byteorder", | |
| 417 | + "cipher", | |
| 418 | +] | |
| 419 | + | |
| 420 | +[[package]] | |
| 421 | +name = "bon" | |
| 422 | +version = "3.10.2" | |
| 423 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 424 | +checksum = "214f5df094ce551a10a30ffb9c70243d61f121a3d985a6495933e181dee6a7d2" | |
| 425 | +dependencies = [ | |
| 426 | + "bon-macros", | |
| 427 | +] | |
| 428 | + | |
| 429 | +[[package]] | |
| 430 | +name = "bon-macros" | |
| 431 | +version = "3.10.2" | |
| 432 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 433 | +checksum = "2706da6c749998cc555d04184909608956a69ad3f8ab1a076fbc867b4a3c3ce5" | |
| 434 | +dependencies = [ | |
| 435 | + "darling", | |
| 436 | + "ident_case", | |
| 437 | + "prettyplease", | |
| 438 | + "proc-macro2", | |
| 439 | + "quote", | |
| 440 | + "syn 3.0.6", | |
| 441 | +] | |
| 442 | + | |
| 443 | +[[package]] | |
| 444 | +name = "bumpalo" | |
| 445 | +version = "3.20.3" | |
| 446 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 447 | +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" | |
| 448 | + | |
| 449 | +[[package]] | |
| 450 | +name = "byteorder" | |
| 451 | +version = "1.5.0" | |
| 452 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 453 | +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | |
| 454 | + | |
| 455 | +[[package]] | |
| 456 | +name = "bytes" | |
| 457 | +version = "1.12.1" | |
| 458 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 459 | +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" | |
| 460 | + | |
| 461 | +[[package]] | |
| 462 | +name = "caseless" | |
| 463 | +version = "0.2.2" | |
| 464 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 465 | +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" | |
| 466 | +dependencies = [ | |
| 467 | + "unicode-normalization", | |
| 468 | +] | |
| 469 | + | |
| 470 | +[[package]] | |
| 471 | +name = "cbc" | |
| 472 | +version = "0.2.1" | |
| 473 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 474 | +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" | |
| 475 | +dependencies = [ | |
| 476 | + "cipher", | |
| 477 | +] | |
| 478 | + | |
| 479 | +[[package]] | |
| 480 | +name = "cc" | |
| 481 | +version = "1.6.0" | |
| 482 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 483 | +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" | |
| 484 | +dependencies = [ | |
| 485 | + "find-msvc-tools", | |
| 486 | + "jobserver", | |
| 487 | + "libc", | |
| 488 | + "shlex", | |
| 489 | +] | |
| 490 | + | |
| 491 | +[[package]] | |
| 492 | +name = "cfg-if" | |
| 493 | +version = "1.0.5" | |
| 494 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 495 | +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" | |
| 496 | + | |
| 497 | +[[package]] | |
| 498 | +name = "cfg_aliases" | |
| 499 | +version = "0.2.2" | |
| 500 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 501 | +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" | |
| 502 | + | |
| 503 | +[[package]] | |
| 504 | +name = "chacha20" | |
| 505 | +version = "0.10.2" | |
| 506 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 507 | +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" | |
| 508 | +dependencies = [ | |
| 509 | + "cfg-if", | |
| 510 | + "cipher", | |
| 511 | + "cpufeatures 0.3.1", | |
| 512 | + "rand_core 0.10.1", | |
| 513 | + "zeroize", | |
| 514 | +] | |
| 515 | + | |
| 516 | +[[package]] | |
| 517 | +name = "chrono" | |
| 518 | +version = "0.4.45" | |
| 519 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 520 | +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" | |
| 521 | +dependencies = [ | |
| 522 | + "iana-time-zone", | |
| 523 | + "js-sys", | |
| 524 | + "num-traits", | |
| 525 | + "serde", | |
| 526 | + "wasm-bindgen", | |
| 527 | + "windows-link", | |
| 528 | +] | |
| 529 | + | |
| 530 | +[[package]] | |
| 531 | +name = "cipher" | |
| 532 | +version = "0.5.2" | |
| 533 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 534 | +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" | |
| 535 | +dependencies = [ | |
| 536 | + "block-buffer 0.12.1", | |
| 537 | + "crypto-common 0.2.2", | |
| 538 | + "inout", | |
| 539 | + "zeroize", | |
| 540 | +] | |
| 541 | + | |
| 542 | +[[package]] | |
| 543 | +name = "clap" | |
| 544 | +version = "4.6.7" | |
| 545 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 546 | +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" | |
| 547 | +dependencies = [ | |
| 548 | + "clap_builder", | |
| 549 | + "clap_derive", | |
| 550 | +] | |
| 551 | + | |
| 552 | +[[package]] | |
| 553 | +name = "clap_builder" | |
| 554 | +version = "4.6.7" | |
| 555 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 556 | +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" | |
| 557 | +dependencies = [ | |
| 558 | + "anstream", | |
| 559 | + "anstyle", | |
| 560 | + "clap_lex", | |
| 561 | + "strsim", | |
| 562 | + "terminal_size", | |
| 563 | +] | |
| 564 | + | |
| 565 | +[[package]] | |
| 566 | +name = "clap_derive" | |
| 567 | +version = "4.6.7" | |
| 568 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 569 | +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" | |
| 570 | +dependencies = [ | |
| 571 | + "heck", | |
| 572 | + "proc-macro2", | |
| 573 | + "quote", | |
| 574 | + "syn 3.0.6", | |
| 575 | +] | |
| 576 | + | |
| 577 | +[[package]] | |
| 578 | +name = "clap_lex" | |
| 579 | +version = "1.1.1" | |
| 580 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 581 | +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" | |
| 582 | + | |
| 583 | +[[package]] | |
| 584 | +name = "cmake" | |
| 585 | +version = "0.1.58" | |
| 586 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 587 | +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" | |
| 588 | +dependencies = [ | |
| 589 | + "cc", | |
| 590 | +] | |
| 591 | + | |
| 592 | +[[package]] | |
| 593 | +name = "cmov" | |
| 594 | +version = "0.5.4" | |
| 595 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 596 | +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" | |
| 597 | + | |
| 598 | +[[package]] | |
| 599 | +name = "colorchoice" | |
| 600 | +version = "1.0.5" | |
| 601 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 602 | +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" | |
| 603 | + | |
| 604 | +[[package]] | |
| 605 | +name = "combine" | |
| 606 | +version = "4.6.8" | |
| 607 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 608 | +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" | |
| 609 | +dependencies = [ | |
| 610 | + "bytes", | |
| 611 | + "memchr", | |
| 612 | +] | |
| 613 | + | |
| 614 | +[[package]] | |
| 615 | +name = "compression-codecs" | |
| 616 | +version = "0.4.45" | |
| 617 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 618 | +checksum = "98fc98460ba0ad5317075d3632b8dfc45d0be8c4a49347c2a38272019717614a" | |
| 619 | +dependencies = [ | |
| 620 | + "compression-core", | |
| 621 | + "flate2", | |
| 622 | + "memchr", | |
| 623 | +] | |
| 624 | + | |
| 625 | +[[package]] | |
| 626 | +name = "compression-core" | |
| 627 | +version = "0.4.33" | |
| 628 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 629 | +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" | |
| 630 | + | |
| 631 | +[[package]] | |
| 632 | +name = "comrak" | |
| 633 | +version = "0.56.0" | |
| 634 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 635 | +checksum = "2544c4d6b6da7d54e95ac2833b464bf3dffa0666237868f2bf4ec5e11c9ba8cc" | |
| 636 | +dependencies = [ | |
| 637 | + "bon", | |
| 638 | + "caseless", | |
| 639 | + "clap", | |
| 640 | + "emojis", | |
| 641 | + "entities", | |
| 642 | + "finl_unicode", | |
| 643 | + "fmt2io", | |
| 644 | + "memchr-n", | |
| 645 | + "phf 0.14.0", | |
| 646 | + "phf_codegen", | |
| 647 | + "rustc-hash", | |
| 648 | + "shell-words", | |
| 649 | + "smallvec", | |
| 650 | + "syntect", | |
| 651 | + "typed-arena", | |
| 652 | + "xdg", | |
| 653 | +] | |
| 654 | + | |
| 655 | +[[package]] | |
| 656 | +name = "const-oid" | |
| 657 | +version = "0.10.2" | |
| 658 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 659 | +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" | |
| 660 | + | |
| 661 | +[[package]] | |
| 662 | +name = "cookie" | |
| 663 | +version = "0.18.2" | |
| 664 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 665 | +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" | |
| 666 | +dependencies = [ | |
| 667 | + "percent-encoding", | |
| 668 | + "time", | |
| 669 | + "version_check", | |
| 670 | +] | |
| 671 | + | |
| 672 | +[[package]] | |
| 673 | +name = "core-foundation" | |
| 674 | +version = "0.10.1" | |
| 675 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 676 | +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" | |
| 677 | +dependencies = [ | |
| 678 | + "core-foundation-sys", | |
| 679 | + "libc", | |
| 680 | +] | |
| 681 | + | |
| 682 | +[[package]] | |
| 683 | +name = "core-foundation-sys" | |
| 684 | +version = "0.8.7" | |
| 685 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 686 | +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" | |
| 687 | + | |
| 688 | +[[package]] | |
| 689 | +name = "core_detect" | |
| 690 | +version = "1.0.0" | |
| 691 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 692 | +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" | |
| 693 | + | |
| 694 | +[[package]] | |
| 695 | +name = "cpubits" | |
| 696 | +version = "0.1.1" | |
| 697 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 698 | +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" | |
| 699 | + | |
| 700 | +[[package]] | |
| 701 | +name = "cpufeatures" | |
| 702 | +version = "0.2.17" | |
| 703 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 704 | +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | |
| 705 | +dependencies = [ | |
| 706 | + "libc", | |
| 707 | +] | |
| 708 | + | |
| 709 | +[[package]] | |
| 710 | +name = "cpufeatures" | |
| 711 | +version = "0.3.1" | |
| 712 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 713 | +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" | |
| 714 | +dependencies = [ | |
| 715 | + "libc", | |
| 716 | +] | |
| 717 | + | |
| 718 | +[[package]] | |
| 719 | +name = "crc" | |
| 720 | +version = "3.4.0" | |
| 721 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 722 | +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" | |
| 723 | +dependencies = [ | |
| 724 | + "crc-catalog", | |
| 725 | +] | |
| 726 | + | |
| 727 | +[[package]] | |
| 728 | +name = "crc-catalog" | |
| 729 | +version = "2.5.0" | |
| 730 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 731 | +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" | |
| 732 | + | |
| 733 | +[[package]] | |
| 734 | +name = "crc32fast" | |
| 735 | +version = "1.5.2" | |
| 736 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 737 | +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" | |
| 738 | +dependencies = [ | |
| 739 | + "cfg-if", | |
| 740 | +] | |
| 741 | + | |
| 742 | +[[package]] | |
| 743 | +name = "crossbeam-channel" | |
| 744 | +version = "0.5.17" | |
| 745 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 746 | +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" | |
| 747 | +dependencies = [ | |
| 748 | + "crossbeam-utils", | |
| 749 | +] | |
| 750 | + | |
| 751 | +[[package]] | |
| 752 | +name = "crossbeam-queue" | |
| 753 | +version = "0.3.14" | |
| 754 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 755 | +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" | |
| 756 | +dependencies = [ | |
| 757 | + "crossbeam-utils", | |
| 758 | +] | |
| 759 | + | |
| 760 | +[[package]] | |
| 761 | +name = "crossbeam-utils" | |
| 762 | +version = "0.8.23" | |
| 763 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 764 | +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" | |
| 765 | + | |
| 766 | +[[package]] | |
| 767 | +name = "crypto-bigint" | |
| 768 | +version = "0.7.5" | |
| 769 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 770 | +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" | |
| 771 | +dependencies = [ | |
| 772 | + "cpubits", | |
| 773 | + "ctutils", | |
| 774 | + "getrandom 0.4.3", | |
| 775 | + "hybrid-array", | |
| 776 | + "num-traits", | |
| 777 | + "rand_core 0.10.1", | |
| 778 | + "serdect", | |
| 779 | + "subtle", | |
| 780 | + "zeroize", | |
| 781 | +] | |
| 782 | + | |
| 783 | +[[package]] | |
| 784 | +name = "crypto-common" | |
| 785 | +version = "0.1.7" | |
| 786 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 787 | +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" | |
| 788 | +dependencies = [ | |
| 789 | + "generic-array 0.14.7", | |
| 790 | + "typenum", | |
| 791 | +] | |
| 792 | + | |
| 793 | +[[package]] | |
| 794 | +name = "crypto-common" | |
| 795 | +version = "0.2.2" | |
| 796 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 797 | +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" | |
| 798 | +dependencies = [ | |
| 799 | + "getrandom 0.4.3", | |
| 800 | + "hybrid-array", | |
| 801 | + "rand_core 0.10.1", | |
| 802 | +] | |
| 803 | + | |
| 804 | +[[package]] | |
| 805 | +name = "crypto-primes" | |
| 806 | +version = "0.7.2" | |
| 807 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 808 | +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" | |
| 809 | +dependencies = [ | |
| 810 | + "crypto-bigint", | |
| 811 | + "rand_core 0.10.1", | |
| 812 | +] | |
| 813 | + | |
| 814 | +[[package]] | |
| 815 | +name = "ctr" | |
| 816 | +version = "0.10.1" | |
| 817 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 818 | +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" | |
| 819 | +dependencies = [ | |
| 820 | + "cipher", | |
| 821 | +] | |
| 822 | + | |
| 823 | +[[package]] | |
| 824 | +name = "ctutils" | |
| 825 | +version = "0.4.3" | |
| 826 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 827 | +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" | |
| 828 | +dependencies = [ | |
| 829 | + "cmov", | |
| 830 | + "subtle", | |
| 831 | +] | |
| 832 | + | |
| 833 | +[[package]] | |
| 834 | +name = "curve25519-dalek" | |
| 835 | +version = "5.0.0" | |
| 836 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 837 | +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" | |
| 838 | +dependencies = [ | |
| 839 | + "cfg-if", | |
| 840 | + "cpufeatures 0.3.1", | |
| 841 | + "curve25519-dalek-derive", | |
| 842 | + "digest 0.11.3", | |
| 843 | + "fiat-crypto", | |
| 844 | + "rand_core 0.10.1", | |
| 845 | + "rustc_version", | |
| 846 | + "subtle", | |
| 847 | + "zeroize", | |
| 848 | +] | |
| 849 | + | |
| 850 | +[[package]] | |
| 851 | +name = "curve25519-dalek-derive" | |
| 852 | +version = "0.1.1" | |
| 853 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 854 | +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" | |
| 855 | +dependencies = [ | |
| 856 | + "proc-macro2", | |
| 857 | + "quote", | |
| 858 | + "syn 2.0.119", | |
| 859 | +] | |
| 860 | + | |
| 861 | +[[package]] | |
| 862 | +name = "darling" | |
| 863 | +version = "0.24.1" | |
| 864 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 865 | +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" | |
| 866 | +dependencies = [ | |
| 867 | + "darling_core", | |
| 868 | + "darling_macro", | |
| 869 | +] | |
| 870 | + | |
| 871 | +[[package]] | |
| 872 | +name = "darling_core" | |
| 873 | +version = "0.24.1" | |
| 874 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 875 | +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" | |
| 876 | +dependencies = [ | |
| 877 | + "ident_case", | |
| 878 | + "proc-macro2", | |
| 879 | + "quote", | |
| 880 | + "strsim", | |
| 881 | + "syn 3.0.6", | |
| 882 | +] | |
| 883 | + | |
| 884 | +[[package]] | |
| 885 | +name = "darling_macro" | |
| 886 | +version = "0.24.1" | |
| 887 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 888 | +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" | |
| 889 | +dependencies = [ | |
| 890 | + "darling_core", | |
| 891 | + "quote", | |
| 892 | + "syn 3.0.6", | |
| 893 | +] | |
| 894 | + | |
| 895 | +[[package]] | |
| 896 | +name = "data-encoding" | |
| 897 | +version = "2.11.1" | |
| 898 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 899 | +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" | |
| 900 | + | |
| 901 | +[[package]] | |
| 902 | +name = "defmt" | |
| 903 | +version = "1.1.1" | |
| 904 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 905 | +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" | |
| 906 | +dependencies = [ | |
| 907 | + "bitflags 1.3.2", | |
| 908 | + "defmt-macros", | |
| 909 | +] | |
| 910 | + | |
| 911 | +[[package]] | |
| 912 | +name = "defmt-macros" | |
| 913 | +version = "1.1.1" | |
| 914 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 915 | +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" | |
| 916 | +dependencies = [ | |
| 917 | + "defmt-parser", | |
| 918 | + "proc-macro2", | |
| 919 | + "quote", | |
| 920 | + "syn 2.0.119", | |
| 921 | +] | |
| 922 | + | |
| 923 | +[[package]] | |
| 924 | +name = "defmt-parser" | |
| 925 | +version = "1.0.0" | |
| 926 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 927 | +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" | |
| 928 | +dependencies = [ | |
| 929 | + "thiserror", | |
| 930 | +] | |
| 931 | + | |
| 932 | +[[package]] | |
| 933 | +name = "delegate" | |
| 934 | +version = "0.13.5" | |
| 935 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 936 | +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" | |
| 937 | +dependencies = [ | |
| 938 | + "proc-macro2", | |
| 939 | + "quote", | |
| 940 | + "syn 2.0.119", | |
| 941 | +] | |
| 942 | + | |
| 943 | +[[package]] | |
| 944 | +name = "der" | |
| 945 | +version = "0.8.2" | |
| 946 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 947 | +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" | |
| 948 | +dependencies = [ | |
| 949 | + "const-oid", | |
| 950 | + "pem-rfc7468", | |
| 951 | + "zeroize", | |
| 952 | +] | |
| 953 | + | |
| 954 | +[[package]] | |
| 955 | +name = "deranged" | |
| 956 | +version = "0.5.8" | |
| 957 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 958 | +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" | |
| 959 | + | |
| 960 | +[[package]] | |
| 961 | +name = "des" | |
| 962 | +version = "0.9.0" | |
| 963 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 964 | +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" | |
| 965 | +dependencies = [ | |
| 966 | + "cipher", | |
| 967 | +] | |
| 968 | + | |
| 969 | +[[package]] | |
| 970 | +name = "digest" | |
| 971 | +version = "0.10.7" | |
| 972 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 973 | +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | |
| 974 | +dependencies = [ | |
| 975 | + "block-buffer 0.10.4", | |
| 976 | + "crypto-common 0.1.7", | |
| 977 | +] | |
| 978 | + | |
| 979 | +[[package]] | |
| 980 | +name = "digest" | |
| 981 | +version = "0.11.3" | |
| 982 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 983 | +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" | |
| 984 | +dependencies = [ | |
| 985 | + "block-buffer 0.12.1", | |
| 986 | + "const-oid", | |
| 987 | + "crypto-common 0.2.2", | |
| 988 | + "ctutils", | |
| 989 | +] | |
| 990 | + | |
| 991 | +[[package]] | |
| 992 | +name = "dirs" | |
| 993 | +version = "7.0.0" | |
| 994 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 995 | +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" | |
| 996 | +dependencies = [ | |
| 997 | + "dirs-sys", | |
| 998 | +] | |
| 999 | + | |
| 1000 | +[[package]] | |
| 1001 | +name = "dirs-sys" | |
| 1002 | +version = "0.5.0" | |
| 1003 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1004 | +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" | |
| 1005 | +dependencies = [ | |
| 1006 | + "libc", | |
| 1007 | + "option-ext", | |
| 1008 | + "redox_users", | |
| 1009 | + "windows-sys 0.61.2", | |
| 1010 | +] | |
| 1011 | + | |
| 1012 | +[[package]] | |
| 1013 | +name = "displaydoc" | |
| 1014 | +version = "0.2.7" | |
| 1015 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1016 | +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" | |
| 1017 | +dependencies = [ | |
| 1018 | + "proc-macro2", | |
| 1019 | + "quote", | |
| 1020 | + "syn 3.0.6", | |
| 1021 | +] | |
| 1022 | + | |
| 1023 | +[[package]] | |
| 1024 | +name = "dotenvy" | |
| 1025 | +version = "0.15.7" | |
| 1026 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1027 | +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" | |
| 1028 | + | |
| 1029 | +[[package]] | |
| 1030 | +name = "dunce" | |
| 1031 | +version = "1.0.5" | |
| 1032 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1033 | +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" | |
| 1034 | + | |
| 1035 | +[[package]] | |
| 1036 | +name = "ecdsa" | |
| 1037 | +version = "0.17.0" | |
| 1038 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1039 | +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" | |
| 1040 | +dependencies = [ | |
| 1041 | + "der", | |
| 1042 | + "digest 0.11.3", | |
| 1043 | + "elliptic-curve", | |
| 1044 | + "rfc6979", | |
| 1045 | + "signature", | |
| 1046 | + "spki", | |
| 1047 | + "zeroize", | |
| 1048 | +] | |
| 1049 | + | |
| 1050 | +[[package]] | |
| 1051 | +name = "ed25519" | |
| 1052 | +version = "3.0.0" | |
| 1053 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1054 | +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" | |
| 1055 | +dependencies = [ | |
| 1056 | + "pkcs8", | |
| 1057 | + "signature", | |
| 1058 | +] | |
| 1059 | + | |
| 1060 | +[[package]] | |
| 1061 | +name = "ed25519-dalek" | |
| 1062 | +version = "3.0.0" | |
| 1063 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1064 | +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" | |
| 1065 | +dependencies = [ | |
| 1066 | + "curve25519-dalek", | |
| 1067 | + "ed25519", | |
| 1068 | + "rand_core 0.10.1", | |
| 1069 | + "serde", | |
| 1070 | + "sha2 0.11.0", | |
| 1071 | + "signature", | |
| 1072 | + "subtle", | |
| 1073 | + "zeroize", | |
| 1074 | +] | |
| 1075 | + | |
| 1076 | +[[package]] | |
| 1077 | +name = "either" | |
| 1078 | +version = "1.19.0" | |
| 1079 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1080 | +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" | |
| 1081 | +dependencies = [ | |
| 1082 | + "serde", | |
| 1083 | +] | |
| 1084 | + | |
| 1085 | +[[package]] | |
| 1086 | +name = "elliptic-curve" | |
| 1087 | +version = "0.14.1" | |
| 1088 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1089 | +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" | |
| 1090 | +dependencies = [ | |
| 1091 | + "base16ct", | |
| 1092 | + "crypto-bigint", | |
| 1093 | + "crypto-common 0.2.2", | |
| 1094 | + "digest 0.11.3", | |
| 1095 | + "ff", | |
| 1096 | + "group", | |
| 1097 | + "hkdf", | |
| 1098 | + "hybrid-array", | |
| 1099 | + "pem-rfc7468", | |
| 1100 | + "pkcs8", | |
| 1101 | + "rand_core 0.10.1", | |
| 1102 | + "sec1", | |
| 1103 | + "subtle", | |
| 1104 | + "zeroize", | |
| 1105 | +] | |
| 1106 | + | |
| 1107 | +[[package]] | |
| 1108 | +name = "email-encoding" | |
| 1109 | +version = "0.4.2" | |
| 1110 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1111 | +checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd" | |
| 1112 | +dependencies = [ | |
| 1113 | + "base64 0.23.1", | |
| 1114 | + "memchr", | |
| 1115 | +] | |
| 1116 | + | |
| 1117 | +[[package]] | |
| 1118 | +name = "email_address" | |
| 1119 | +version = "0.2.9" | |
| 1120 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1121 | +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" | |
| 1122 | + | |
| 1123 | +[[package]] | |
| 1124 | +name = "emojis" | |
| 1125 | +version = "0.9.0" | |
| 1126 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1127 | +checksum = "0b1514ced566c94991ed9563258ecf61e311fd773bf0a3f20606830386bf66e3" | |
| 1128 | +dependencies = [ | |
| 1129 | + "phf 0.13.1", | |
| 1130 | +] | |
| 1131 | + | |
| 1132 | +[[package]] | |
| 1133 | +name = "encoding_rs" | |
| 1134 | +version = "0.8.42" | |
| 1135 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1136 | +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" | |
| 1137 | +dependencies = [ | |
| 1138 | + "cfg-if", | |
| 1139 | + "core_detect", | |
| 1140 | + "multiversion_no_op", | |
| 1141 | + "rustversion", | |
| 1142 | + "scopeguard", | |
| 1143 | + "simdutf8", | |
| 1144 | +] | |
| 1145 | + | |
| 1146 | +[[package]] | |
| 1147 | +name = "entities" | |
| 1148 | +version = "1.0.1" | |
| 1149 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1150 | +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" | |
| 1151 | + | |
| 1152 | +[[package]] | |
| 1153 | +name = "enum_dispatch" | |
| 1154 | +version = "0.3.13" | |
| 1155 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1156 | +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" | |
| 1157 | +dependencies = [ | |
| 1158 | + "once_cell", | |
| 1159 | + "proc-macro2", | |
| 1160 | + "quote", | |
| 1161 | + "syn 2.0.119", | |
| 1162 | +] | |
| 1163 | + | |
| 1164 | +[[package]] | |
| 1165 | +name = "equivalent" | |
| 1166 | +version = "1.0.2" | |
| 1167 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1168 | +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | |
| 1169 | + | |
| 1170 | +[[package]] | |
| 1171 | +name = "errno" | |
| 1172 | +version = "0.3.14" | |
| 1173 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1174 | +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 1175 | +dependencies = [ | |
| 1176 | + "libc", | |
| 1177 | + "windows-sys 0.61.2", | |
| 1178 | +] | |
| 1179 | + | |
| 1180 | +[[package]] | |
| 1181 | +name = "etcetera" | |
| 1182 | +version = "0.11.0" | |
| 1183 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1184 | +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" | |
| 1185 | +dependencies = [ | |
| 1186 | + "cfg-if", | |
| 1187 | + "windows-sys 0.61.2", | |
| 1188 | +] | |
| 1189 | + | |
| 1190 | +[[package]] | |
| 1191 | +name = "event-listener" | |
| 1192 | +version = "5.4.2" | |
| 1193 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1194 | +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" | |
| 1195 | +dependencies = [ | |
| 1196 | + "parking", | |
| 1197 | + "pin-project-lite", | |
| 1198 | +] | |
| 1199 | + | |
| 1200 | +[[package]] | |
| 1201 | +name = "fancy-regex" | |
| 1202 | +version = "0.16.2" | |
| 1203 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1204 | +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" | |
| 1205 | +dependencies = [ | |
| 1206 | + "bit-set", | |
| 1207 | + "regex-automata", | |
| 1208 | + "regex-syntax", | |
| 1209 | +] | |
| 1210 | + | |
| 1211 | +[[package]] | |
| 1212 | +name = "fastrand" | |
| 1213 | +version = "2.5.0" | |
| 1214 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1215 | +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" | |
| 1216 | + | |
| 1217 | +[[package]] | |
| 1218 | +name = "fearless_simd" | |
| 1219 | +version = "1.1.0" | |
| 1220 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1221 | +checksum = "3296eb97d869113eaf148a6bdf01884e90b3828ad7ae2340b0f93362fc158611" | |
| 1222 | + | |
| 1223 | +[[package]] | |
| 1224 | +name = "fearless_simd_macros" | |
| 1225 | +version = "0.1.0" | |
| 1226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1227 | +checksum = "3e91d94c6885390b81782d7ee60fd6583b33907333a8b340de25ccf468353315" | |
| 1228 | +dependencies = [ | |
| 1229 | + "proc-macro2", | |
| 1230 | + "quote", | |
| 1231 | + "syn 3.0.6", | |
| 1232 | +] | |
| 1233 | + | |
| 1234 | +[[package]] | |
| 1235 | +name = "ff" | |
| 1236 | +version = "0.14.0" | |
| 1237 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1238 | +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" | |
| 1239 | +dependencies = [ | |
| 1240 | + "rand_core 0.10.1", | |
| 1241 | + "subtle", | |
| 1242 | +] | |
| 1243 | + | |
| 1244 | +[[package]] | |
| 1245 | +name = "fiat-crypto" | |
| 1246 | +version = "0.3.0" | |
| 1247 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1248 | +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" | |
| 1249 | + | |
| 1250 | +[[package]] | |
| 1251 | +name = "find-msvc-tools" | |
| 1252 | +version = "0.1.14" | |
| 1253 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1254 | +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" | |
| 1255 | + | |
| 1256 | +[[package]] | |
| 1257 | +name = "finl_unicode" | |
| 1258 | +version = "1.5.0" | |
| 1259 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1260 | +checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" | |
| 1261 | + | |
| 1262 | +[[package]] | |
| 1263 | +name = "flate2" | |
| 1264 | +version = "1.1.10" | |
| 1265 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1266 | +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" | |
| 1267 | +dependencies = [ | |
| 1268 | + "crc32fast", | |
| 1269 | + "miniz_oxide", | |
| 1270 | + "zlib-rs", | |
| 1271 | +] | |
| 1272 | + | |
| 1273 | +[[package]] | |
| 1274 | +name = "flume" | |
| 1275 | +version = "0.12.0" | |
| 1276 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1277 | +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" | |
| 1278 | +dependencies = [ | |
| 1279 | + "futures-core", | |
| 1280 | + "futures-sink", | |
| 1281 | + "spin", | |
| 1282 | +] | |
| 1283 | + | |
| 1284 | +[[package]] | |
| 1285 | +name = "fmt2io" | |
| 1286 | +version = "1.0.0" | |
| 1287 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1288 | +checksum = "6b6129284da9f7e5296cc22183a63f24300e945e297705dcc0672f7df01d62c8" | |
| 1289 | + | |
| 1290 | +[[package]] | |
| 1291 | +name = "fnv" | |
| 1292 | +version = "1.0.7" | |
| 1293 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1294 | +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | |
| 1295 | + | |
| 1296 | +[[package]] | |
| 1297 | +name = "foldhash" | |
| 1298 | +version = "0.2.0" | |
| 1299 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1300 | +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" | |
| 1301 | + | |
| 1302 | +[[package]] | |
| 1303 | +name = "form_urlencoded" | |
| 1304 | +version = "1.2.2" | |
| 1305 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1306 | +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" | |
| 1307 | +dependencies = [ | |
| 1308 | + "percent-encoding", | |
| 1309 | +] | |
| 1310 | + | |
| 1311 | +[[package]] | |
| 1312 | +name = "fs_extra" | |
| 1313 | +version = "1.3.0" | |
| 1314 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1315 | +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" | |
| 1316 | + | |
| 1317 | +[[package]] | |
| 1318 | +name = "fsevent-sys" | |
| 1319 | +version = "4.1.0" | |
| 1320 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1321 | +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" | |
| 1322 | +dependencies = [ | |
| 1323 | + "libc", | |
| 1324 | +] | |
| 1325 | + | |
| 1326 | +[[package]] | |
| 1327 | +name = "futures" | |
| 1328 | +version = "0.3.34" | |
| 1329 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1330 | +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" | |
| 1331 | +dependencies = [ | |
| 1332 | + "futures-channel", | |
| 1333 | + "futures-core", | |
| 1334 | + "futures-executor", | |
| 1335 | + "futures-io", | |
| 1336 | + "futures-sink", | |
| 1337 | + "futures-task", | |
| 1338 | + "futures-util", | |
| 1339 | +] | |
| 1340 | + | |
| 1341 | +[[package]] | |
| 1342 | +name = "futures-channel" | |
| 1343 | +version = "0.3.34" | |
| 1344 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1345 | +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" | |
| 1346 | +dependencies = [ | |
| 1347 | + "futures-core", | |
| 1348 | + "futures-sink", | |
| 1349 | +] | |
| 1350 | + | |
| 1351 | +[[package]] | |
| 1352 | +name = "futures-core" | |
| 1353 | +version = "0.3.34" | |
| 1354 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1355 | +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" | |
| 1356 | + | |
| 1357 | +[[package]] | |
| 1358 | +name = "futures-executor" | |
| 1359 | +version = "0.3.34" | |
| 1360 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1361 | +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" | |
| 1362 | +dependencies = [ | |
| 1363 | + "futures-core", | |
| 1364 | + "futures-task", | |
| 1365 | + "futures-util", | |
| 1366 | +] | |
| 1367 | + | |
| 1368 | +[[package]] | |
| 1369 | +name = "futures-intrusive" | |
| 1370 | +version = "0.5.0" | |
| 1371 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1372 | +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" | |
| 1373 | +dependencies = [ | |
| 1374 | + "futures-core", | |
| 1375 | + "lock_api", | |
| 1376 | + "parking_lot", | |
| 1377 | +] | |
| 1378 | + | |
| 1379 | +[[package]] | |
| 1380 | +name = "futures-io" | |
| 1381 | +version = "0.3.34" | |
| 1382 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1383 | +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" | |
| 1384 | + | |
| 1385 | +[[package]] | |
| 1386 | +name = "futures-macro" | |
| 1387 | +version = "0.3.34" | |
| 1388 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1389 | +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" | |
| 1390 | +dependencies = [ | |
| 1391 | + "proc-macro2", | |
| 1392 | + "quote", | |
| 1393 | + "syn 3.0.6", | |
| 1394 | +] | |
| 1395 | + | |
| 1396 | +[[package]] | |
| 1397 | +name = "futures-sink" | |
| 1398 | +version = "0.3.34" | |
| 1399 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1400 | +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" | |
| 1401 | + | |
| 1402 | +[[package]] | |
| 1403 | +name = "futures-task" | |
| 1404 | +version = "0.3.34" | |
| 1405 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1406 | +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" | |
| 1407 | + | |
| 1408 | +[[package]] | |
| 1409 | +name = "futures-util" | |
| 1410 | +version = "0.3.34" | |
| 1411 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1412 | +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" | |
| 1413 | +dependencies = [ | |
| 1414 | + "futures-channel", | |
| 1415 | + "futures-core", | |
| 1416 | + "futures-io", | |
| 1417 | + "futures-macro", | |
| 1418 | + "futures-sink", | |
| 1419 | + "futures-task", | |
| 1420 | + "memchr", | |
| 1421 | + "pin-project-lite", | |
| 1422 | + "slab", | |
| 1423 | +] | |
| 1424 | + | |
| 1425 | +[[package]] | |
| 1426 | +name = "generic-array" | |
| 1427 | +version = "0.14.7" | |
| 1428 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1429 | +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | |
| 1430 | +dependencies = [ | |
| 1431 | + "typenum", | |
| 1432 | + "version_check", | |
| 1433 | +] | |
| 1434 | + | |
| 1435 | +[[package]] | |
| 1436 | +name = "generic-array" | |
| 1437 | +version = "1.4.5" | |
| 1438 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1439 | +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" | |
| 1440 | +dependencies = [ | |
| 1441 | + "generic-array 0.14.7", | |
| 1442 | + "rustversion", | |
| 1443 | + "typenum", | |
| 1444 | +] | |
| 1445 | + | |
| 1446 | +[[package]] | |
| 1447 | +name = "getrandom" | |
| 1448 | +version = "0.2.17" | |
| 1449 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1450 | +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" | |
| 1451 | +dependencies = [ | |
| 1452 | + "cfg-if", | |
| 1453 | + "js-sys", | |
| 1454 | + "libc", | |
| 1455 | + "wasi", | |
| 1456 | + "wasm-bindgen", | |
| 1457 | +] | |
| 1458 | + | |
| 1459 | +[[package]] | |
| 1460 | +name = "getrandom" | |
| 1461 | +version = "0.3.4" | |
| 1462 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1463 | +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" | |
| 1464 | +dependencies = [ | |
| 1465 | + "cfg-if", | |
| 1466 | + "libc", | |
| 1467 | + "r-efi 5.3.0", | |
| 1468 | + "wasip2", | |
| 1469 | +] | |
| 1470 | + | |
| 1471 | +[[package]] | |
| 1472 | +name = "getrandom" | |
| 1473 | +version = "0.4.3" | |
| 1474 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1475 | +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" | |
| 1476 | +dependencies = [ | |
| 1477 | + "cfg-if", | |
| 1478 | + "js-sys", | |
| 1479 | + "libc", | |
| 1480 | + "r-efi 6.0.0", | |
| 1481 | + "rand_core 0.10.1", | |
| 1482 | + "wasm-bindgen", | |
| 1483 | +] | |
| 1484 | + | |
| 1485 | +[[package]] | |
| 1486 | +name = "ghash" | |
| 1487 | +version = "0.6.0" | |
| 1488 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1489 | +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" | |
| 1490 | +dependencies = [ | |
| 1491 | + "polyval", | |
| 1492 | + "zeroize", | |
| 1493 | +] | |
| 1494 | + | |
| 1495 | +[[package]] | |
| 1496 | +name = "group" | |
| 1497 | +version = "0.14.0" | |
| 1498 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1499 | +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" | |
| 1500 | +dependencies = [ | |
| 1501 | + "ff", | |
| 1502 | + "rand_core 0.10.1", | |
| 1503 | + "subtle", | |
| 1504 | +] | |
| 1505 | + | |
| 1506 | +[[package]] | |
| 1507 | +name = "h2" | |
| 1508 | +version = "0.4.20" | |
| 1509 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1510 | +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" | |
| 1511 | +dependencies = [ | |
| 1512 | + "atomic-waker", | |
| 1513 | + "bytes", | |
| 1514 | + "fnv", | |
| 1515 | + "futures-core", | |
| 1516 | + "futures-sink", | |
| 1517 | + "http", | |
| 1518 | + "indexmap", | |
| 1519 | + "slab", | |
| 1520 | + "tokio", | |
| 1521 | + "tokio-util", | |
| 1522 | + "tracing", | |
| 1523 | +] | |
| 1524 | + | |
| 1525 | +[[package]] | |
| 1526 | +name = "hashbrown" | |
| 1527 | +version = "0.16.1" | |
| 1528 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1529 | +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" | |
| 1530 | +dependencies = [ | |
| 1531 | + "allocator-api2", | |
| 1532 | + "equivalent", | |
| 1533 | + "foldhash", | |
| 1534 | +] | |
| 1535 | + | |
| 1536 | +[[package]] | |
| 1537 | +name = "hashbrown" | |
| 1538 | +version = "0.17.1" | |
| 1539 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1540 | +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" | |
| 1541 | + | |
| 1542 | +[[package]] | |
| 1543 | +name = "hashlink" | |
| 1544 | +version = "0.11.1" | |
| 1545 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1546 | +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" | |
| 1547 | +dependencies = [ | |
| 1548 | + "hashbrown 0.16.1", | |
| 1549 | +] | |
| 1550 | + | |
| 1551 | +[[package]] | |
| 1552 | +name = "headers" | |
| 1553 | +version = "0.4.2" | |
| 1554 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1555 | +checksum = "fc2700e3fe555c3310aa7286cac6167449f2c87e0eb58769c9208a1c58a1d106" | |
| 1556 | +dependencies = [ | |
| 1557 | + "base64 0.22.1", | |
| 1558 | + "bytes", | |
| 1559 | + "headers-core", | |
| 1560 | + "http", | |
| 1561 | + "httpdate", | |
| 1562 | + "mime", | |
| 1563 | + "sha1 0.10.7", | |
| 1564 | +] | |
| 1565 | + | |
| 1566 | +[[package]] | |
| 1567 | +name = "headers-core" | |
| 1568 | +version = "0.3.0" | |
| 1569 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1570 | +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" | |
| 1571 | +dependencies = [ | |
| 1572 | + "http", | |
| 1573 | +] | |
| 1574 | + | |
| 1575 | +[[package]] | |
| 1576 | +name = "heck" | |
| 1577 | +version = "0.5.0" | |
| 1578 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1579 | +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | |
| 1580 | + | |
| 1581 | +[[package]] | |
| 1582 | +name = "hex" | |
| 1583 | +version = "0.4.3" | |
| 1584 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1585 | +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" | |
| 1586 | + | |
| 1587 | +[[package]] | |
| 1588 | +name = "hex-literal" | |
| 1589 | +version = "1.1.0" | |
| 1590 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1591 | +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" | |
| 1592 | + | |
| 1593 | +[[package]] | |
| 1594 | +name = "hkdf" | |
| 1595 | +version = "0.13.0" | |
| 1596 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1597 | +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" | |
| 1598 | +dependencies = [ | |
| 1599 | + "hmac", | |
| 1600 | +] | |
| 1601 | + | |
| 1602 | +[[package]] | |
| 1603 | +name = "hmac" | |
| 1604 | +version = "0.13.0" | |
| 1605 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1606 | +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" | |
| 1607 | +dependencies = [ | |
| 1608 | + "digest 0.11.3", | |
| 1609 | +] | |
| 1610 | + | |
| 1611 | +[[package]] | |
| 1612 | +name = "hostname" | |
| 1613 | +version = "0.4.2" | |
| 1614 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1615 | +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" | |
| 1616 | +dependencies = [ | |
| 1617 | + "cfg-if", | |
| 1618 | + "libc", | |
| 1619 | + "windows-link", | |
| 1620 | +] | |
| 1621 | + | |
| 1622 | +[[package]] | |
| 1623 | +name = "http" | |
| 1624 | +version = "1.5.0" | |
| 1625 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1626 | +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" | |
| 1627 | +dependencies = [ | |
| 1628 | + "bytes", | |
| 1629 | + "itoa", | |
| 1630 | +] | |
| 1631 | + | |
| 1632 | +[[package]] | |
| 1633 | +name = "http-body" | |
| 1634 | +version = "1.1.0" | |
| 1635 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1636 | +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" | |
| 1637 | +dependencies = [ | |
| 1638 | + "bytes", | |
| 1639 | + "http", | |
| 1640 | +] | |
| 1641 | + | |
| 1642 | +[[package]] | |
| 1643 | +name = "http-body-util" | |
| 1644 | +version = "0.1.5" | |
| 1645 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1646 | +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" | |
| 1647 | +dependencies = [ | |
| 1648 | + "bytes", | |
| 1649 | + "futures-core", | |
| 1650 | + "http", | |
| 1651 | + "http-body", | |
| 1652 | + "pin-project-lite", | |
| 1653 | +] | |
| 1654 | + | |
| 1655 | +[[package]] | |
| 1656 | +name = "httparse" | |
| 1657 | +version = "1.10.1" | |
| 1658 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1659 | +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" | |
| 1660 | + | |
| 1661 | +[[package]] | |
| 1662 | +name = "httpdate" | |
| 1663 | +version = "1.0.3" | |
| 1664 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1665 | +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" | |
| 1666 | + | |
| 1667 | +[[package]] | |
| 1668 | +name = "hybrid-array" | |
| 1669 | +version = "0.4.15" | |
| 1670 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1671 | +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" | |
| 1672 | +dependencies = [ | |
| 1673 | + "ctutils", | |
| 1674 | + "subtle", | |
| 1675 | + "typenum", | |
| 1676 | + "zeroize", | |
| 1677 | +] | |
| 1678 | + | |
| 1679 | +[[package]] | |
| 1680 | +name = "hyper" | |
| 1681 | +version = "1.12.0" | |
| 1682 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1683 | +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" | |
| 1684 | +dependencies = [ | |
| 1685 | + "atomic-waker", | |
| 1686 | + "bytes", | |
| 1687 | + "futures-channel", | |
| 1688 | + "futures-core", | |
| 1689 | + "h2", | |
| 1690 | + "http", | |
| 1691 | + "http-body", | |
| 1692 | + "httparse", | |
| 1693 | + "httpdate", | |
| 1694 | + "itoa", | |
| 1695 | + "pin-project-lite", | |
| 1696 | + "smallvec", | |
| 1697 | + "tokio", | |
| 1698 | + "want", | |
| 1699 | +] | |
| 1700 | + | |
| 1701 | +[[package]] | |
| 1702 | +name = "hyper-rustls" | |
| 1703 | +version = "0.27.10" | |
| 1704 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1705 | +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" | |
| 1706 | +dependencies = [ | |
| 1707 | + "http", | |
| 1708 | + "hyper", | |
| 1709 | + "hyper-util", | |
| 1710 | + "rustls", | |
| 1711 | + "tokio", | |
| 1712 | + "tokio-rustls", | |
| 1713 | + "tower-service", | |
| 1714 | +] | |
| 1715 | + | |
| 1716 | +[[package]] | |
| 1717 | +name = "hyper-util" | |
| 1718 | +version = "0.1.21" | |
| 1719 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1720 | +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" | |
| 1721 | +dependencies = [ | |
| 1722 | + "base64 0.23.1", | |
| 1723 | + "bytes", | |
| 1724 | + "futures-channel", | |
| 1725 | + "futures-util", | |
| 1726 | + "http", | |
| 1727 | + "http-body", | |
| 1728 | + "httparse", | |
| 1729 | + "hyper", | |
| 1730 | + "ipnet", | |
| 1731 | + "libc", | |
| 1732 | + "percent-encoding", | |
| 1733 | + "pin-project-lite", | |
| 1734 | + "socket2", | |
| 1735 | + "tokio", | |
| 1736 | + "tower-service", | |
| 1737 | + "tracing", | |
| 1738 | +] | |
| 1739 | + | |
| 1740 | +[[package]] | |
| 1741 | +name = "iana-time-zone" | |
| 1742 | +version = "0.1.65" | |
| 1743 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1744 | +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" | |
| 1745 | +dependencies = [ | |
| 1746 | + "android_system_properties", | |
| 1747 | + "core-foundation-sys", | |
| 1748 | + "iana-time-zone-haiku", | |
| 1749 | + "js-sys", | |
| 1750 | + "log", | |
| 1751 | + "wasm-bindgen", | |
| 1752 | + "windows-core", | |
| 1753 | +] | |
| 1754 | + | |
| 1755 | +[[package]] | |
| 1756 | +name = "iana-time-zone-haiku" | |
| 1757 | +version = "0.1.2" | |
| 1758 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1759 | +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" | |
| 1760 | +dependencies = [ | |
| 1761 | + "cc", | |
| 1762 | +] | |
| 1763 | + | |
| 1764 | +[[package]] | |
| 1765 | +name = "icu_collections" | |
| 1766 | +version = "2.3.0" | |
| 1767 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1768 | +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" | |
| 1769 | +dependencies = [ | |
| 1770 | + "displaydoc", | |
| 1771 | + "potential_utf", | |
| 1772 | + "utf8_iter", | |
| 1773 | + "yoke", | |
| 1774 | + "zerofrom", | |
| 1775 | + "zerovec", | |
| 1776 | +] | |
| 1777 | + | |
| 1778 | +[[package]] | |
| 1779 | +name = "icu_locale_core" | |
| 1780 | +version = "2.3.0" | |
| 1781 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1782 | +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" | |
| 1783 | +dependencies = [ | |
| 1784 | + "displaydoc", | |
| 1785 | + "litemap", | |
| 1786 | + "tinystr", | |
| 1787 | + "writeable", | |
| 1788 | + "zerovec", | |
| 1789 | +] | |
| 1790 | + | |
| 1791 | +[[package]] | |
| 1792 | +name = "icu_normalizer" | |
| 1793 | +version = "2.3.0" | |
| 1794 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1795 | +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" | |
| 1796 | +dependencies = [ | |
| 1797 | + "icu_collections", | |
| 1798 | + "icu_normalizer_data", | |
| 1799 | + "icu_properties", | |
| 1800 | + "icu_provider", | |
| 1801 | + "smallvec", | |
| 1802 | + "zerovec", | |
| 1803 | +] | |
| 1804 | + | |
| 1805 | +[[package]] | |
| 1806 | +name = "icu_normalizer_data" | |
| 1807 | +version = "2.3.0" | |
| 1808 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1809 | +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" | |
| 1810 | + | |
| 1811 | +[[package]] | |
| 1812 | +name = "icu_properties" | |
| 1813 | +version = "2.3.0" | |
| 1814 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1815 | +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" | |
| 1816 | +dependencies = [ | |
| 1817 | + "displaydoc", | |
| 1818 | + "icu_collections", | |
| 1819 | + "icu_locale_core", | |
| 1820 | + "icu_properties_data", | |
| 1821 | + "icu_provider", | |
| 1822 | + "zerotrie", | |
| 1823 | + "zerovec", | |
| 1824 | +] | |
| 1825 | + | |
| 1826 | +[[package]] | |
| 1827 | +name = "icu_properties_data" | |
| 1828 | +version = "2.3.0" | |
| 1829 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1830 | +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" | |
| 1831 | + | |
| 1832 | +[[package]] | |
| 1833 | +name = "icu_provider" | |
| 1834 | +version = "2.3.1" | |
| 1835 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1836 | +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" | |
| 1837 | +dependencies = [ | |
| 1838 | + "displaydoc", | |
| 1839 | + "icu_locale_core", | |
| 1840 | + "writeable", | |
| 1841 | + "yoke", | |
| 1842 | + "zerofrom", | |
| 1843 | + "zerotrie", | |
| 1844 | + "zerovec", | |
| 1845 | +] | |
| 1846 | + | |
| 1847 | +[[package]] | |
| 1848 | +name = "ident_case" | |
| 1849 | +version = "1.0.1" | |
| 1850 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1851 | +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" | |
| 1852 | + | |
| 1853 | +[[package]] | |
| 1854 | +name = "idna" | |
| 1855 | +version = "1.1.0" | |
| 1856 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1857 | +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" | |
| 1858 | +dependencies = [ | |
| 1859 | + "idna_adapter", | |
| 1860 | + "smallvec", | |
| 1861 | + "utf8_iter", | |
| 1862 | +] | |
| 1863 | + | |
| 1864 | +[[package]] | |
| 1865 | +name = "idna_adapter" | |
| 1866 | +version = "1.2.2" | |
| 1867 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1868 | +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" | |
| 1869 | +dependencies = [ | |
| 1870 | + "icu_normalizer", | |
| 1871 | + "icu_properties", | |
| 1872 | +] | |
| 1873 | + | |
| 1874 | +[[package]] | |
| 1875 | +name = "ig" | |
| 1876 | +version = "0.1.0" | |
| 1877 | +dependencies = [ | |
| 1878 | + "anyhow", | |
| 1879 | + "clap", | |
| 1880 | + "dirs", | |
| 1881 | + "hex", | |
| 1882 | + "irongit-shared", | |
| 1883 | + "reqwest", | |
| 1884 | + "serde", | |
| 1885 | + "serde_json", | |
| 1886 | + "sha2 0.11.0", | |
| 1887 | + "toml", | |
| 1888 | +] | |
| 1889 | + | |
| 1890 | +[[package]] | |
| 1891 | +name = "indexmap" | |
| 1892 | +version = "2.14.2" | |
| 1893 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1894 | +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" | |
| 1895 | +dependencies = [ | |
| 1896 | + "equivalent", | |
| 1897 | + "hashbrown 0.17.1", | |
| 1898 | +] | |
| 1899 | + | |
| 1900 | +[[package]] | |
| 1901 | +name = "inotify" | |
| 1902 | +version = "0.11.5" | |
| 1903 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1904 | +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" | |
| 1905 | +dependencies = [ | |
| 1906 | + "bitflags 2.13.2", | |
| 1907 | + "inotify-sys", | |
| 1908 | + "libc", | |
| 1909 | +] | |
| 1910 | + | |
| 1911 | +[[package]] | |
| 1912 | +name = "inotify-sys" | |
| 1913 | +version = "0.1.8" | |
| 1914 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1915 | +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" | |
| 1916 | +dependencies = [ | |
| 1917 | + "libc", | |
| 1918 | +] | |
| 1919 | + | |
| 1920 | +[[package]] | |
| 1921 | +name = "inout" | |
| 1922 | +version = "0.2.2" | |
| 1923 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1924 | +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" | |
| 1925 | +dependencies = [ | |
| 1926 | + "block-padding", | |
| 1927 | + "hybrid-array", | |
| 1928 | +] | |
| 1929 | + | |
| 1930 | +[[package]] | |
| 1931 | +name = "instant-xml" | |
| 1932 | +version = "0.7.6" | |
| 1933 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1934 | +checksum = "0a2cad967c3b727c000ebfdcd14974539e8c6f59e1d044c8034179df2c6fe250" | |
| 1935 | +dependencies = [ | |
| 1936 | + "instant-xml-macros", | |
| 1937 | + "thiserror", | |
| 1938 | + "xmlparser", | |
| 1939 | +] | |
| 1940 | + | |
| 1941 | +[[package]] | |
| 1942 | +name = "instant-xml-macros" | |
| 1943 | +version = "0.7.2" | |
| 1944 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1945 | +checksum = "ac3c2e6cbd6e0579ee53a8290fc0015a438cfff0139c8359d5891cae130815d7" | |
| 1946 | +dependencies = [ | |
| 1947 | + "heck", | |
| 1948 | + "proc-macro2", | |
| 1949 | + "quote", | |
| 1950 | + "syn 3.0.6", | |
| 1951 | +] | |
| 1952 | + | |
| 1953 | +[[package]] | |
| 1954 | +name = "ipnet" | |
| 1955 | +version = "2.12.2" | |
| 1956 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1957 | +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" | |
| 1958 | + | |
| 1959 | +[[package]] | |
| 1960 | +name = "irongit" | |
| 1961 | +version = "0.1.0" | |
| 1962 | +dependencies = [ | |
| 1963 | + "anyhow", | |
| 1964 | + "argon2", | |
| 1965 | + "async-compression", | |
| 1966 | + "axum", | |
| 1967 | + "axum-extra", | |
| 1968 | + "base64 0.23.1", | |
| 1969 | + "bytes", | |
| 1970 | + "chrono", | |
| 1971 | + "clap", | |
| 1972 | + "comrak", | |
| 1973 | + "dotenvy", | |
| 1974 | + "flate2", | |
| 1975 | + "futures", | |
| 1976 | + "hex", | |
| 1977 | + "hmac", | |
| 1978 | + "irongit-shared", | |
| 1979 | + "lettre", | |
| 1980 | + "maud", | |
| 1981 | + "mime_guess", | |
| 1982 | + "notify", | |
| 1983 | + "once_cell", | |
| 1984 | + "rand 0.10.3", | |
| 1985 | + "regex", | |
| 1986 | + "reqwest", | |
| 1987 | + "russh", | |
| 1988 | + "rust-embed", | |
| 1989 | + "rusty-s3", | |
| 1990 | + "serde", | |
| 1991 | + "serde_json", | |
| 1992 | + "sha2 0.11.0", | |
| 1993 | + "sqlx", | |
| 1994 | + "syntect", | |
| 1995 | + "time", | |
| 1996 | + "tokio", | |
| 1997 | + "tokio-util", | |
| 1998 | + "tower-http 0.7.1", | |
| 1999 | + "tracing", | |
| 2000 | + "tracing-appender", | |
| 2001 | + "tracing-subscriber", | |
| 2002 | + "url", | |
| 2003 | + "uuid", | |
| 2004 | +] | |
| 2005 | + | |
| 2006 | +[[package]] | |
| 2007 | +name = "irongit-shared" | |
| 2008 | +version = "0.1.0" | |
| 2009 | +dependencies = [ | |
| 2010 | + "chrono", | |
| 2011 | + "serde", | |
| 2012 | + "serde_json", | |
| 2013 | +] | |
| 2014 | + | |
| 2015 | +[[package]] | |
| 2016 | +name = "is_terminal_polyfill" | |
| 2017 | +version = "1.70.2" | |
| 2018 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2019 | +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" | |
| 2020 | + | |
| 2021 | +[[package]] | |
| 2022 | +name = "itoa" | |
| 2023 | +version = "1.0.18" | |
| 2024 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2025 | +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" | |
| 2026 | + | |
| 2027 | +[[package]] | |
| 2028 | +name = "jiff" | |
| 2029 | +version = "0.2.38" | |
| 2030 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2031 | +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" | |
| 2032 | +dependencies = [ | |
| 2033 | + "defmt", | |
| 2034 | + "jiff-core", | |
| 2035 | + "jiff-static", | |
| 2036 | + "log", | |
| 2037 | + "portable-atomic", | |
| 2038 | + "portable-atomic-util", | |
| 2039 | + "serde_core", | |
| 2040 | +] | |
| 2041 | + | |
| 2042 | +[[package]] | |
| 2043 | +name = "jiff-core" | |
| 2044 | +version = "0.1.1" | |
| 2045 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2046 | +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" | |
| 2047 | +dependencies = [ | |
| 2048 | + "defmt", | |
| 2049 | + "log", | |
| 2050 | +] | |
| 2051 | + | |
| 2052 | +[[package]] | |
| 2053 | +name = "jiff-static" | |
| 2054 | +version = "0.2.38" | |
| 2055 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2056 | +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" | |
| 2057 | +dependencies = [ | |
| 2058 | + "jiff-core", | |
| 2059 | + "proc-macro2", | |
| 2060 | + "quote", | |
| 2061 | + "syn 2.0.119", | |
| 2062 | +] | |
| 2063 | + | |
| 2064 | +[[package]] | |
| 2065 | +name = "jni" | |
| 2066 | +version = "0.22.4" | |
| 2067 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2068 | +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" | |
| 2069 | +dependencies = [ | |
| 2070 | + "cfg-if", | |
| 2071 | + "combine", | |
| 2072 | + "jni-macros", | |
| 2073 | + "jni-sys", | |
| 2074 | + "log", | |
| 2075 | + "simd_cesu8", | |
| 2076 | + "thiserror", | |
| 2077 | + "walkdir", | |
| 2078 | + "windows-link", | |
| 2079 | +] | |
| 2080 | + | |
| 2081 | +[[package]] | |
| 2082 | +name = "jni-macros" | |
| 2083 | +version = "0.22.4" | |
| 2084 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2085 | +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" | |
| 2086 | +dependencies = [ | |
| 2087 | + "proc-macro2", | |
| 2088 | + "quote", | |
| 2089 | + "rustc_version", | |
| 2090 | + "simd_cesu8", | |
| 2091 | + "syn 2.0.119", | |
| 2092 | +] | |
| 2093 | + | |
| 2094 | +[[package]] | |
| 2095 | +name = "jni-sys" | |
| 2096 | +version = "0.4.1" | |
| 2097 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2098 | +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" | |
| 2099 | +dependencies = [ | |
| 2100 | + "jni-sys-macros", | |
| 2101 | +] | |
| 2102 | + | |
| 2103 | +[[package]] | |
| 2104 | +name = "jni-sys-macros" | |
| 2105 | +version = "0.4.1" | |
| 2106 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2107 | +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" | |
| 2108 | +dependencies = [ | |
| 2109 | + "quote", | |
| 2110 | + "syn 2.0.119", | |
| 2111 | +] | |
| 2112 | + | |
| 2113 | +[[package]] | |
| 2114 | +name = "jobserver" | |
| 2115 | +version = "0.1.35" | |
| 2116 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2117 | +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" | |
| 2118 | +dependencies = [ | |
| 2119 | + "getrandom 0.4.3", | |
| 2120 | + "libc", | |
| 2121 | +] | |
| 2122 | + | |
| 2123 | +[[package]] | |
| 2124 | +name = "js-sys" | |
| 2125 | +version = "0.3.106" | |
| 2126 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2127 | +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" | |
| 2128 | +dependencies = [ | |
| 2129 | + "cfg-if", | |
| 2130 | + "futures-util", | |
| 2131 | + "wasm-bindgen", | |
| 2132 | +] | |
| 2133 | + | |
| 2134 | +[[package]] | |
| 2135 | +name = "keccak" | |
| 2136 | +version = "0.2.2" | |
| 2137 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2138 | +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" | |
| 2139 | +dependencies = [ | |
| 2140 | + "cfg-if", | |
| 2141 | + "cpufeatures 0.3.1", | |
| 2142 | +] | |
| 2143 | + | |
| 2144 | +[[package]] | |
| 2145 | +name = "kem" | |
| 2146 | +version = "0.3.0" | |
| 2147 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2148 | +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" | |
| 2149 | +dependencies = [ | |
| 2150 | + "crypto-common 0.2.2", | |
| 2151 | + "rand_core 0.10.1", | |
| 2152 | +] | |
| 2153 | + | |
| 2154 | +[[package]] | |
| 2155 | +name = "kqueue" | |
| 2156 | +version = "1.2.1" | |
| 2157 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2158 | +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" | |
| 2159 | +dependencies = [ | |
| 2160 | + "kqueue-sys", | |
| 2161 | + "libc", | |
| 2162 | +] | |
| 2163 | + | |
| 2164 | +[[package]] | |
| 2165 | +name = "kqueue-sys" | |
| 2166 | +version = "1.1.2" | |
| 2167 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2168 | +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" | |
| 2169 | +dependencies = [ | |
| 2170 | + "bitflags 2.13.2", | |
| 2171 | + "libc", | |
| 2172 | +] | |
| 2173 | + | |
| 2174 | +[[package]] | |
| 2175 | +name = "lazy_static" | |
| 2176 | +version = "1.5.1" | |
| 2177 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2178 | +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" | |
| 2179 | + | |
| 2180 | +[[package]] | |
| 2181 | +name = "lettre" | |
| 2182 | +version = "0.11.23" | |
| 2183 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2184 | +checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae" | |
| 2185 | +dependencies = [ | |
| 2186 | + "async-trait", | |
| 2187 | + "base64 0.23.1", | |
| 2188 | + "email-encoding", | |
| 2189 | + "email_address", | |
| 2190 | + "fastrand", | |
| 2191 | + "futures-io", | |
| 2192 | + "futures-util", | |
| 2193 | + "hostname", | |
| 2194 | + "httpdate", | |
| 2195 | + "idna", | |
| 2196 | + "mime", | |
| 2197 | + "nom", | |
| 2198 | + "percent-encoding", | |
| 2199 | + "quoted_printable", | |
| 2200 | + "rustls", | |
| 2201 | + "socket2", | |
| 2202 | + "tokio", | |
| 2203 | + "tokio-rustls", | |
| 2204 | + "url", | |
| 2205 | + "webpki-roots", | |
| 2206 | +] | |
| 2207 | + | |
| 2208 | +[[package]] | |
| 2209 | +name = "libc" | |
| 2210 | +version = "0.2.190" | |
| 2211 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2212 | +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" | |
| 2213 | + | |
| 2214 | +[[package]] | |
| 2215 | +name = "libredox" | |
| 2216 | +version = "0.1.25" | |
| 2217 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2218 | +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" | |
| 2219 | +dependencies = [ | |
| 2220 | + "libc", | |
| 2221 | +] | |
| 2222 | + | |
| 2223 | +[[package]] | |
| 2224 | +name = "libsqlite3-sys" | |
| 2225 | +version = "0.37.0" | |
| 2226 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2227 | +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" | |
| 2228 | +dependencies = [ | |
| 2229 | + "pkg-config", | |
| 2230 | + "vcpkg", | |
| 2231 | +] | |
| 2232 | + | |
| 2233 | +[[package]] | |
| 2234 | +name = "linked-hash-map" | |
| 2235 | +version = "0.5.6" | |
| 2236 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2237 | +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" | |
| 2238 | + | |
| 2239 | +[[package]] | |
| 2240 | +name = "linux-raw-sys" | |
| 2241 | +version = "0.12.1" | |
| 2242 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2243 | +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" | |
| 2244 | + | |
| 2245 | +[[package]] | |
| 2246 | +name = "litemap" | |
| 2247 | +version = "0.8.3" | |
| 2248 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2249 | +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" | |
| 2250 | + | |
| 2251 | +[[package]] | |
| 2252 | +name = "lock_api" | |
| 2253 | +version = "0.4.14" | |
| 2254 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2255 | +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" | |
| 2256 | +dependencies = [ | |
| 2257 | + "scopeguard", | |
| 2258 | +] | |
| 2259 | + | |
| 2260 | +[[package]] | |
| 2261 | +name = "log" | |
| 2262 | +version = "0.4.34" | |
| 2263 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2264 | +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" | |
| 2265 | + | |
| 2266 | +[[package]] | |
| 2267 | +name = "lru-slab" | |
| 2268 | +version = "0.1.3" | |
| 2269 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2270 | +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" | |
| 2271 | + | |
| 2272 | +[[package]] | |
| 2273 | +name = "matchers" | |
| 2274 | +version = "0.2.0" | |
| 2275 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2276 | +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" | |
| 2277 | +dependencies = [ | |
| 2278 | + "regex-automata", | |
| 2279 | +] | |
| 2280 | + | |
| 2281 | +[[package]] | |
| 2282 | +name = "matchit" | |
| 2283 | +version = "0.8.4" | |
| 2284 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2285 | +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" | |
| 2286 | + | |
| 2287 | +[[package]] | |
| 2288 | +name = "maud" | |
| 2289 | +version = "0.27.0" | |
| 2290 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2291 | +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" | |
| 2292 | +dependencies = [ | |
| 2293 | + "axum-core", | |
| 2294 | + "http", | |
| 2295 | + "itoa", | |
| 2296 | + "maud_macros", | |
| 2297 | +] | |
| 2298 | + | |
| 2299 | +[[package]] | |
| 2300 | +name = "maud_macros" | |
| 2301 | +version = "0.27.0" | |
| 2302 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2303 | +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" | |
| 2304 | +dependencies = [ | |
| 2305 | + "proc-macro2", | |
| 2306 | + "proc-macro2-diagnostics", | |
| 2307 | + "quote", | |
| 2308 | + "syn 2.0.119", | |
| 2309 | +] | |
| 2310 | + | |
| 2311 | +[[package]] | |
| 2312 | +name = "md-5" | |
| 2313 | +version = "0.11.0" | |
| 2314 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2315 | +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" | |
| 2316 | +dependencies = [ | |
| 2317 | + "cfg-if", | |
| 2318 | + "digest 0.11.3", | |
| 2319 | +] | |
| 2320 | + | |
| 2321 | +[[package]] | |
| 2322 | +name = "md5" | |
| 2323 | +version = "0.8.1" | |
| 2324 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2325 | +checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" | |
| 2326 | + | |
| 2327 | +[[package]] | |
| 2328 | +name = "memchr" | |
| 2329 | +version = "2.8.3" | |
| 2330 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2331 | +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" | |
| 2332 | + | |
| 2333 | +[[package]] | |
| 2334 | +name = "memchr-n" | |
| 2335 | +version = "0.1.9" | |
| 2336 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2337 | +checksum = "1805571b8e7b1284ad43f27c88477840c1c0e5807c150021ac83d2228806540e" | |
| 2338 | +dependencies = [ | |
| 2339 | + "fearless_simd", | |
| 2340 | + "fearless_simd_macros", | |
| 2341 | +] | |
| 2342 | + | |
| 2343 | +[[package]] | |
| 2344 | +name = "mime" | |
| 2345 | +version = "0.3.17" | |
| 2346 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2347 | +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" | |
| 2348 | + | |
| 2349 | +[[package]] | |
| 2350 | +name = "mime_guess" | |
| 2351 | +version = "2.0.5" | |
| 2352 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2353 | +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" | |
| 2354 | +dependencies = [ | |
| 2355 | + "mime", | |
| 2356 | + "unicase", | |
| 2357 | +] | |
| 2358 | + | |
| 2359 | +[[package]] | |
| 2360 | +name = "miniz_oxide" | |
| 2361 | +version = "0.9.1" | |
| 2362 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2363 | +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" | |
| 2364 | +dependencies = [ | |
| 2365 | + "adler2", | |
| 2366 | + "simd-adler32", | |
| 2367 | +] | |
| 2368 | + | |
| 2369 | +[[package]] | |
| 2370 | +name = "mio" | |
| 2371 | +version = "1.2.4" | |
| 2372 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2373 | +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" | |
| 2374 | +dependencies = [ | |
| 2375 | + "libc", | |
| 2376 | + "log", | |
| 2377 | + "wasi", | |
| 2378 | + "windows-sys 0.61.2", | |
| 2379 | +] | |
| 2380 | + | |
| 2381 | +[[package]] | |
| 2382 | +name = "ml-kem" | |
| 2383 | +version = "0.3.2" | |
| 2384 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2385 | +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" | |
| 2386 | +dependencies = [ | |
| 2387 | + "hybrid-array", | |
| 2388 | + "kem", | |
| 2389 | + "module-lattice", | |
| 2390 | + "pkcs8", | |
| 2391 | + "rand_core 0.10.1", | |
| 2392 | + "sha3 0.11.0", | |
| 2393 | +] | |
| 2394 | + | |
| 2395 | +[[package]] | |
| 2396 | +name = "module-lattice" | |
| 2397 | +version = "0.2.3" | |
| 2398 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2399 | +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" | |
| 2400 | +dependencies = [ | |
| 2401 | + "ctutils", | |
| 2402 | + "hybrid-array", | |
| 2403 | + "num-traits", | |
| 2404 | +] | |
| 2405 | + | |
| 2406 | +[[package]] | |
| 2407 | +name = "multer" | |
| 2408 | +version = "3.1.0" | |
| 2409 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2410 | +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" | |
| 2411 | +dependencies = [ | |
| 2412 | + "bytes", | |
| 2413 | + "encoding_rs", | |
| 2414 | + "futures-util", | |
| 2415 | + "http", | |
| 2416 | + "httparse", | |
| 2417 | + "memchr", | |
| 2418 | + "mime", | |
| 2419 | + "spin", | |
| 2420 | + "version_check", | |
| 2421 | +] | |
| 2422 | + | |
| 2423 | +[[package]] | |
| 2424 | +name = "multiversion_no_op" | |
| 2425 | +version = "1.0.0" | |
| 2426 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2427 | +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" | |
| 2428 | + | |
| 2429 | +[[package]] | |
| 2430 | +name = "nix" | |
| 2431 | +version = "0.31.3" | |
| 2432 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2433 | +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" | |
| 2434 | +dependencies = [ | |
| 2435 | + "bitflags 2.13.2", | |
| 2436 | + "cfg-if", | |
| 2437 | + "cfg_aliases", | |
| 2438 | + "libc", | |
| 2439 | +] | |
| 2440 | + | |
| 2441 | +[[package]] | |
| 2442 | +name = "nom" | |
| 2443 | +version = "8.0.0" | |
| 2444 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2445 | +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" | |
| 2446 | +dependencies = [ | |
| 2447 | + "memchr", | |
| 2448 | +] | |
| 2449 | + | |
| 2450 | +[[package]] | |
| 2451 | +name = "notify" | |
| 2452 | +version = "8.2.0" | |
| 2453 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2454 | +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" | |
| 2455 | +dependencies = [ | |
| 2456 | + "bitflags 2.13.2", | |
| 2457 | + "fsevent-sys", | |
| 2458 | + "inotify", | |
| 2459 | + "kqueue", | |
| 2460 | + "libc", | |
| 2461 | + "log", | |
| 2462 | + "mio", | |
| 2463 | + "notify-types", | |
| 2464 | + "walkdir", | |
| 2465 | + "windows-sys 0.60.2", | |
| 2466 | +] | |
| 2467 | + | |
| 2468 | +[[package]] | |
| 2469 | +name = "notify-types" | |
| 2470 | +version = "2.1.0" | |
| 2471 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2472 | +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" | |
| 2473 | +dependencies = [ | |
| 2474 | + "bitflags 2.13.2", | |
| 2475 | +] | |
| 2476 | + | |
| 2477 | +[[package]] | |
| 2478 | +name = "nu-ansi-term" | |
| 2479 | +version = "0.50.3" | |
| 2480 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2481 | +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 2482 | +dependencies = [ | |
| 2483 | + "windows-sys 0.61.2", | |
| 2484 | +] | |
| 2485 | + | |
| 2486 | +[[package]] | |
| 2487 | +name = "num-bigint" | |
| 2488 | +version = "0.5.1" | |
| 2489 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2490 | +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" | |
| 2491 | +dependencies = [ | |
| 2492 | + "num-integer", | |
| 2493 | + "num-traits", | |
| 2494 | + "rand 0.10.3", | |
| 2495 | + "rand_core 0.10.1", | |
| 2496 | +] | |
| 2497 | + | |
| 2498 | +[[package]] | |
| 2499 | +name = "num-conv" | |
| 2500 | +version = "0.2.2" | |
| 2501 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2502 | +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" | |
| 2503 | + | |
| 2504 | +[[package]] | |
| 2505 | +name = "num-integer" | |
| 2506 | +version = "0.1.47" | |
| 2507 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2508 | +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" | |
| 2509 | +dependencies = [ | |
| 2510 | + "num-traits", | |
| 2511 | +] | |
| 2512 | + | |
| 2513 | +[[package]] | |
| 2514 | +name = "num-traits" | |
| 2515 | +version = "0.2.19" | |
| 2516 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2517 | +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" | |
| 2518 | +dependencies = [ | |
| 2519 | + "autocfg", | |
| 2520 | +] | |
| 2521 | + | |
| 2522 | +[[package]] | |
| 2523 | +name = "once_cell" | |
| 2524 | +version = "1.21.4" | |
| 2525 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2526 | +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" | |
| 2527 | + | |
| 2528 | +[[package]] | |
| 2529 | +name = "once_cell_polyfill" | |
| 2530 | +version = "1.70.2" | |
| 2531 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2532 | +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" | |
| 2533 | + | |
| 2534 | +[[package]] | |
| 2535 | +name = "onig" | |
| 2536 | +version = "6.5.3" | |
| 2537 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2538 | +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" | |
| 2539 | +dependencies = [ | |
| 2540 | + "bitflags 2.13.2", | |
| 2541 | + "libc", | |
| 2542 | + "once_cell", | |
| 2543 | + "onig_sys", | |
| 2544 | +] | |
| 2545 | + | |
| 2546 | +[[package]] | |
| 2547 | +name = "onig_sys" | |
| 2548 | +version = "69.9.3" | |
| 2549 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2550 | +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" | |
| 2551 | +dependencies = [ | |
| 2552 | + "cc", | |
| 2553 | + "pkg-config", | |
| 2554 | +] | |
| 2555 | + | |
| 2556 | +[[package]] | |
| 2557 | +name = "openssl-probe" | |
| 2558 | +version = "0.2.1" | |
| 2559 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2560 | +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" | |
| 2561 | + | |
| 2562 | +[[package]] | |
| 2563 | +name = "option-ext" | |
| 2564 | +version = "0.2.0" | |
| 2565 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2566 | +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" | |
| 2567 | + | |
| 2568 | +[[package]] | |
| 2569 | +name = "p256" | |
| 2570 | +version = "0.14.0" | |
| 2571 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2572 | +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" | |
| 2573 | +dependencies = [ | |
| 2574 | + "ecdsa", | |
| 2575 | + "elliptic-curve", | |
| 2576 | + "primefield", | |
| 2577 | + "primeorder", | |
| 2578 | + "sha2 0.11.0", | |
| 2579 | +] | |
| 2580 | + | |
| 2581 | +[[package]] | |
| 2582 | +name = "p384" | |
| 2583 | +version = "0.14.0" | |
| 2584 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2585 | +checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" | |
| 2586 | +dependencies = [ | |
| 2587 | + "ecdsa", | |
| 2588 | + "elliptic-curve", | |
| 2589 | + "fiat-crypto", | |
| 2590 | + "primefield", | |
| 2591 | + "primeorder", | |
| 2592 | + "sha2 0.11.0", | |
| 2593 | +] | |
| 2594 | + | |
| 2595 | +[[package]] | |
| 2596 | +name = "p521" | |
| 2597 | +version = "0.14.0" | |
| 2598 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2599 | +checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" | |
| 2600 | +dependencies = [ | |
| 2601 | + "base16ct", | |
| 2602 | + "ecdsa", | |
| 2603 | + "elliptic-curve", | |
| 2604 | + "primefield", | |
| 2605 | + "primeorder", | |
| 2606 | + "sha2 0.11.0", | |
| 2607 | +] | |
| 2608 | + | |
| 2609 | +[[package]] | |
| 2610 | +name = "pageant" | |
| 2611 | +version = "0.2.4" | |
| 2612 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2613 | +checksum = "f0786409b5aaa884655c9b4065681db0835db0cc185a6fb19fb17a5d38656866" | |
| 2614 | +dependencies = [ | |
| 2615 | + "base16ct", | |
| 2616 | + "byteorder", | |
| 2617 | + "bytes", | |
| 2618 | + "delegate", | |
| 2619 | + "futures", | |
| 2620 | + "log", | |
| 2621 | + "rand 0.10.3", | |
| 2622 | + "sha2 0.11.0", | |
| 2623 | + "thiserror", | |
| 2624 | + "tokio", | |
| 2625 | + "windows", | |
| 2626 | + "windows-strings", | |
| 2627 | +] | |
| 2628 | + | |
| 2629 | +[[package]] | |
| 2630 | +name = "parking" | |
| 2631 | +version = "2.2.1" | |
| 2632 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2633 | +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" | |
| 2634 | + | |
| 2635 | +[[package]] | |
| 2636 | +name = "parking_lot" | |
| 2637 | +version = "0.12.5" | |
| 2638 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2639 | +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" | |
| 2640 | +dependencies = [ | |
| 2641 | + "lock_api", | |
| 2642 | + "parking_lot_core", | |
| 2643 | +] | |
| 2644 | + | |
| 2645 | +[[package]] | |
| 2646 | +name = "parking_lot_core" | |
| 2647 | +version = "0.9.12" | |
| 2648 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2649 | +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" | |
| 2650 | +dependencies = [ | |
| 2651 | + "cfg-if", | |
| 2652 | + "libc", | |
| 2653 | + "redox_syscall", | |
| 2654 | + "smallvec", | |
| 2655 | + "windows-link", | |
| 2656 | +] | |
| 2657 | + | |
| 2658 | +[[package]] | |
| 2659 | +name = "password-hash" | |
| 2660 | +version = "0.6.1" | |
| 2661 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2662 | +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" | |
| 2663 | +dependencies = [ | |
| 2664 | + "getrandom 0.4.3", | |
| 2665 | + "phc", | |
| 2666 | +] | |
| 2667 | + | |
| 2668 | +[[package]] | |
| 2669 | +name = "pbkdf2" | |
| 2670 | +version = "0.13.0" | |
| 2671 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2672 | +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" | |
| 2673 | +dependencies = [ | |
| 2674 | + "digest 0.11.3", | |
| 2675 | + "hmac", | |
| 2676 | +] | |
| 2677 | + | |
| 2678 | +[[package]] | |
| 2679 | +name = "pem-rfc7468" | |
| 2680 | +version = "1.0.0" | |
| 2681 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2682 | +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" | |
| 2683 | +dependencies = [ | |
| 2684 | + "base64ct", | |
| 2685 | +] | |
| 2686 | + | |
| 2687 | +[[package]] | |
| 2688 | +name = "percent-encoding" | |
| 2689 | +version = "2.3.2" | |
| 2690 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2691 | +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" | |
| 2692 | + | |
| 2693 | +[[package]] | |
| 2694 | +name = "phc" | |
| 2695 | +version = "0.6.1" | |
| 2696 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2697 | +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" | |
| 2698 | +dependencies = [ | |
| 2699 | + "base64ct", | |
| 2700 | + "ctutils", | |
| 2701 | + "getrandom 0.4.3", | |
| 2702 | +] | |
| 2703 | + | |
| 2704 | +[[package]] | |
| 2705 | +name = "phf" | |
| 2706 | +version = "0.13.1" | |
| 2707 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2708 | +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" | |
| 2709 | +dependencies = [ | |
| 2710 | + "phf_shared 0.13.1", | |
| 2711 | +] | |
| 2712 | + | |
| 2713 | +[[package]] | |
| 2714 | +name = "phf" | |
| 2715 | +version = "0.14.0" | |
| 2716 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2717 | +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" | |
| 2718 | +dependencies = [ | |
| 2719 | + "phf_shared 0.14.0", | |
| 2720 | + "serde", | |
| 2721 | +] | |
| 2722 | + | |
| 2723 | +[[package]] | |
| 2724 | +name = "phf_codegen" | |
| 2725 | +version = "0.14.0" | |
| 2726 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2727 | +checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" | |
| 2728 | +dependencies = [ | |
| 2729 | + "phf_generator", | |
| 2730 | + "phf_shared 0.14.0", | |
| 2731 | +] | |
| 2732 | + | |
| 2733 | +[[package]] | |
| 2734 | +name = "phf_generator" | |
| 2735 | +version = "0.14.0" | |
| 2736 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2737 | +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" | |
| 2738 | +dependencies = [ | |
| 2739 | + "fastrand", | |
| 2740 | + "phf_shared 0.14.0", | |
| 2741 | +] | |
| 2742 | + | |
| 2743 | +[[package]] | |
| 2744 | +name = "phf_shared" | |
| 2745 | +version = "0.13.1" | |
| 2746 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2747 | +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" | |
| 2748 | +dependencies = [ | |
| 2749 | + "siphasher", | |
| 2750 | +] | |
| 2751 | + | |
| 2752 | +[[package]] | |
| 2753 | +name = "phf_shared" | |
| 2754 | +version = "0.14.0" | |
| 2755 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2756 | +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" | |
| 2757 | +dependencies = [ | |
| 2758 | + "siphasher", | |
| 2759 | +] | |
| 2760 | + | |
| 2761 | +[[package]] | |
| 2762 | +name = "pin-project-lite" | |
| 2763 | +version = "0.2.17" | |
| 2764 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2765 | +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" | |
| 2766 | + | |
| 2767 | +[[package]] | |
| 2768 | +name = "pkcs1" | |
| 2769 | +version = "0.8.0-rc.4" | |
| 2770 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2771 | +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" | |
| 2772 | +dependencies = [ | |
| 2773 | + "der", | |
| 2774 | + "spki", | |
| 2775 | +] | |
| 2776 | + | |
| 2777 | +[[package]] | |
| 2778 | +name = "pkcs5" | |
| 2779 | +version = "0.8.1" | |
| 2780 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2781 | +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" | |
| 2782 | +dependencies = [ | |
| 2783 | + "aes", | |
| 2784 | + "aes-gcm", | |
| 2785 | + "cbc", | |
| 2786 | + "der", | |
| 2787 | + "pbkdf2", | |
| 2788 | + "rand_core 0.10.1", | |
| 2789 | + "scrypt", | |
| 2790 | + "sha2 0.11.0", | |
| 2791 | + "spki", | |
| 2792 | +] | |
| 2793 | + | |
| 2794 | +[[package]] | |
| 2795 | +name = "pkcs8" | |
| 2796 | +version = "0.11.0" | |
| 2797 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2798 | +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" | |
| 2799 | +dependencies = [ | |
| 2800 | + "der", | |
| 2801 | + "pkcs5", | |
| 2802 | + "rand_core 0.10.1", | |
| 2803 | + "spki", | |
| 2804 | +] | |
| 2805 | + | |
| 2806 | +[[package]] | |
| 2807 | +name = "pkg-config" | |
| 2808 | +version = "0.3.34" | |
| 2809 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2810 | +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" | |
| 2811 | + | |
| 2812 | +[[package]] | |
| 2813 | +name = "plist" | |
| 2814 | +version = "1.10.1" | |
| 2815 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2816 | +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" | |
| 2817 | +dependencies = [ | |
| 2818 | + "base64 0.23.1", | |
| 2819 | + "indexmap", | |
| 2820 | + "quick-xml", | |
| 2821 | + "serde", | |
| 2822 | + "time", | |
| 2823 | +] | |
| 2824 | + | |
| 2825 | +[[package]] | |
| 2826 | +name = "poly1305" | |
| 2827 | +version = "0.9.1" | |
| 2828 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2829 | +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" | |
| 2830 | +dependencies = [ | |
| 2831 | + "cpufeatures 0.3.1", | |
| 2832 | + "universal-hash", | |
| 2833 | + "zeroize", | |
| 2834 | +] | |
| 2835 | + | |
| 2836 | +[[package]] | |
| 2837 | +name = "polyval" | |
| 2838 | +version = "0.7.3" | |
| 2839 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2840 | +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" | |
| 2841 | +dependencies = [ | |
| 2842 | + "cpubits", | |
| 2843 | + "cpufeatures 0.3.1", | |
| 2844 | + "universal-hash", | |
| 2845 | + "zeroize", | |
| 2846 | +] | |
| 2847 | + | |
| 2848 | +[[package]] | |
| 2849 | +name = "portable-atomic" | |
| 2850 | +version = "1.15.0" | |
| 2851 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2852 | +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" | |
| 2853 | + | |
| 2854 | +[[package]] | |
| 2855 | +name = "portable-atomic-util" | |
| 2856 | +version = "0.2.8" | |
| 2857 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2858 | +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" | |
| 2859 | +dependencies = [ | |
| 2860 | + "portable-atomic", | |
| 2861 | +] | |
| 2862 | + | |
| 2863 | +[[package]] | |
| 2864 | +name = "potential_utf" | |
| 2865 | +version = "0.1.6" | |
| 2866 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2867 | +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" | |
| 2868 | +dependencies = [ | |
| 2869 | + "zerovec", | |
| 2870 | +] | |
| 2871 | + | |
| 2872 | +[[package]] | |
| 2873 | +name = "powerfmt" | |
| 2874 | +version = "0.2.1" | |
| 2875 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2876 | +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" | |
| 2877 | + | |
| 2878 | +[[package]] | |
| 2879 | +name = "ppv-lite86" | |
| 2880 | +version = "0.2.21" | |
| 2881 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2882 | +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | |
| 2883 | +dependencies = [ | |
| 2884 | + "zerocopy", | |
| 2885 | +] | |
| 2886 | + | |
| 2887 | +[[package]] | |
| 2888 | +name = "prettyplease" | |
| 2889 | +version = "0.3.0" | |
| 2890 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2891 | +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" | |
| 2892 | +dependencies = [ | |
| 2893 | + "proc-macro2", | |
| 2894 | + "syn 3.0.6", | |
| 2895 | +] | |
| 2896 | + | |
| 2897 | +[[package]] | |
| 2898 | +name = "primefield" | |
| 2899 | +version = "0.14.0" | |
| 2900 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2901 | +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" | |
| 2902 | +dependencies = [ | |
| 2903 | + "crypto-bigint", | |
| 2904 | + "crypto-common 0.2.2", | |
| 2905 | + "ff", | |
| 2906 | + "rand_core 0.10.1", | |
| 2907 | + "subtle", | |
| 2908 | + "zeroize", | |
| 2909 | +] | |
| 2910 | + | |
| 2911 | +[[package]] | |
| 2912 | +name = "primeorder" | |
| 2913 | +version = "0.14.0" | |
| 2914 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2915 | +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" | |
| 2916 | +dependencies = [ | |
| 2917 | + "elliptic-curve", | |
| 2918 | + "once_cell", | |
| 2919 | + "primefield", | |
| 2920 | + "serdect", | |
| 2921 | + "wnaf", | |
| 2922 | +] | |
| 2923 | + | |
| 2924 | +[[package]] | |
| 2925 | +name = "proc-macro2" | |
| 2926 | +version = "1.0.107" | |
| 2927 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2928 | +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" | |
| 2929 | +dependencies = [ | |
| 2930 | + "unicode-ident", | |
| 2931 | +] | |
| 2932 | + | |
| 2933 | +[[package]] | |
| 2934 | +name = "proc-macro2-diagnostics" | |
| 2935 | +version = "0.10.1" | |
| 2936 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2937 | +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" | |
| 2938 | +dependencies = [ | |
| 2939 | + "proc-macro2", | |
| 2940 | + "quote", | |
| 2941 | + "syn 2.0.119", | |
| 2942 | + "version_check", | |
| 2943 | +] | |
| 2944 | + | |
| 2945 | +[[package]] | |
| 2946 | +name = "quick-xml" | |
| 2947 | +version = "0.42.0" | |
| 2948 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2949 | +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" | |
| 2950 | +dependencies = [ | |
| 2951 | + "memchr", | |
| 2952 | +] | |
| 2953 | + | |
| 2954 | +[[package]] | |
| 2955 | +name = "quinn" | |
| 2956 | +version = "0.11.12" | |
| 2957 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2958 | +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" | |
| 2959 | +dependencies = [ | |
| 2960 | + "bytes", | |
| 2961 | + "cfg_aliases", | |
| 2962 | + "pin-project-lite", | |
| 2963 | + "quinn-proto", | |
| 2964 | + "quinn-udp", | |
| 2965 | + "rustc-hash", | |
| 2966 | + "rustls", | |
| 2967 | + "socket2", | |
| 2968 | + "thiserror", | |
| 2969 | + "tokio", | |
| 2970 | + "tracing", | |
| 2971 | + "web-time", | |
| 2972 | +] | |
| 2973 | + | |
| 2974 | +[[package]] | |
| 2975 | +name = "quinn-proto" | |
| 2976 | +version = "0.11.19" | |
| 2977 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2978 | +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" | |
| 2979 | +dependencies = [ | |
| 2980 | + "aws-lc-rs", | |
| 2981 | + "bytes", | |
| 2982 | + "getrandom 0.4.3", | |
| 2983 | + "lru-slab", | |
| 2984 | + "rand 0.10.3", | |
| 2985 | + "rand_pcg", | |
| 2986 | + "ring", | |
| 2987 | + "rustc-hash", | |
| 2988 | + "rustls", | |
| 2989 | + "rustls-pki-types", | |
| 2990 | + "slab", | |
| 2991 | + "thiserror", | |
| 2992 | + "tinyvec", | |
| 2993 | + "tracing", | |
| 2994 | + "web-time", | |
| 2995 | +] | |
| 2996 | + | |
| 2997 | +[[package]] | |
| 2998 | +name = "quinn-udp" | |
| 2999 | +version = "0.5.16" | |
| 3000 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3001 | +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" | |
| 3002 | +dependencies = [ | |
| 3003 | + "cfg_aliases", | |
| 3004 | + "libc", | |
| 3005 | + "once_cell", | |
| 3006 | + "socket2", | |
| 3007 | + "tracing", | |
| 3008 | + "windows-sys 0.61.2", | |
| 3009 | +] | |
| 3010 | + | |
| 3011 | +[[package]] | |
| 3012 | +name = "quote" | |
| 3013 | +version = "1.0.47" | |
| 3014 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3015 | +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" | |
| 3016 | +dependencies = [ | |
| 3017 | + "proc-macro2", | |
| 3018 | +] | |
| 3019 | + | |
| 3020 | +[[package]] | |
| 3021 | +name = "quoted_printable" | |
| 3022 | +version = "0.5.2" | |
| 3023 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3024 | +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" | |
| 3025 | + | |
| 3026 | +[[package]] | |
| 3027 | +name = "r-efi" | |
| 3028 | +version = "5.3.0" | |
| 3029 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3030 | +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" | |
| 3031 | + | |
| 3032 | +[[package]] | |
| 3033 | +name = "r-efi" | |
| 3034 | +version = "6.0.0" | |
| 3035 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3036 | +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" | |
| 3037 | + | |
| 3038 | +[[package]] | |
| 3039 | +name = "rand" | |
| 3040 | +version = "0.9.5" | |
| 3041 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3042 | +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" | |
| 3043 | +dependencies = [ | |
| 3044 | + "rand_chacha", | |
| 3045 | + "rand_core 0.9.5", | |
| 3046 | +] | |
| 3047 | + | |
| 3048 | +[[package]] | |
| 3049 | +name = "rand" | |
| 3050 | +version = "0.10.3" | |
| 3051 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3052 | +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" | |
| 3053 | +dependencies = [ | |
| 3054 | + "chacha20", | |
| 3055 | + "getrandom 0.4.3", | |
| 3056 | + "rand_core 0.10.1", | |
| 3057 | +] | |
| 3058 | + | |
| 3059 | +[[package]] | |
| 3060 | +name = "rand_chacha" | |
| 3061 | +version = "0.9.0" | |
| 3062 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3063 | +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" | |
| 3064 | +dependencies = [ | |
| 3065 | + "ppv-lite86", | |
| 3066 | + "rand_core 0.9.5", | |
| 3067 | +] | |
| 3068 | + | |
| 3069 | +[[package]] | |
| 3070 | +name = "rand_core" | |
| 3071 | +version = "0.9.5" | |
| 3072 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3073 | +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" | |
| 3074 | +dependencies = [ | |
| 3075 | + "getrandom 0.3.4", | |
| 3076 | +] | |
| 3077 | + | |
| 3078 | +[[package]] | |
| 3079 | +name = "rand_core" | |
| 3080 | +version = "0.10.1" | |
| 3081 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3082 | +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" | |
| 3083 | + | |
| 3084 | +[[package]] | |
| 3085 | +name = "rand_pcg" | |
| 3086 | +version = "0.10.2" | |
| 3087 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3088 | +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" | |
| 3089 | +dependencies = [ | |
| 3090 | + "rand_core 0.10.1", | |
| 3091 | +] | |
| 3092 | + | |
| 3093 | +[[package]] | |
| 3094 | +name = "redox_syscall" | |
| 3095 | +version = "0.5.18" | |
| 3096 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3097 | +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" | |
| 3098 | +dependencies = [ | |
| 3099 | + "bitflags 2.13.2", | |
| 3100 | +] | |
| 3101 | + | |
| 3102 | +[[package]] | |
| 3103 | +name = "redox_users" | |
| 3104 | +version = "0.5.3" | |
| 3105 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3106 | +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" | |
| 3107 | +dependencies = [ | |
| 3108 | + "libredox", | |
| 3109 | + "thiserror", | |
| 3110 | +] | |
| 3111 | + | |
| 3112 | +[[package]] | |
| 3113 | +name = "regex" | |
| 3114 | +version = "1.13.1" | |
| 3115 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3116 | +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" | |
| 3117 | +dependencies = [ | |
| 3118 | + "aho-corasick", | |
| 3119 | + "memchr", | |
| 3120 | + "regex-automata", | |
| 3121 | + "regex-syntax", | |
| 3122 | +] | |
| 3123 | + | |
| 3124 | +[[package]] | |
| 3125 | +name = "regex-automata" | |
| 3126 | +version = "0.4.18" | |
| 3127 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3128 | +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" | |
| 3129 | +dependencies = [ | |
| 3130 | + "aho-corasick", | |
| 3131 | + "memchr", | |
| 3132 | + "regex-syntax", | |
| 3133 | +] | |
| 3134 | + | |
| 3135 | +[[package]] | |
| 3136 | +name = "regex-syntax" | |
| 3137 | +version = "0.8.11" | |
| 3138 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3139 | +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" | |
| 3140 | + | |
| 3141 | +[[package]] | |
| 3142 | +name = "reqwest" | |
| 3143 | +version = "0.13.5" | |
| 3144 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3145 | +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" | |
| 3146 | +dependencies = [ | |
| 3147 | + "base64 0.23.1", | |
| 3148 | + "bytes", | |
| 3149 | + "futures-channel", | |
| 3150 | + "futures-core", | |
| 3151 | + "futures-util", | |
| 3152 | + "h2", | |
| 3153 | + "http", | |
| 3154 | + "http-body", | |
| 3155 | + "http-body-util", | |
| 3156 | + "hyper", | |
| 3157 | + "hyper-rustls", | |
| 3158 | + "hyper-util", | |
| 3159 | + "js-sys", | |
| 3160 | + "log", | |
| 3161 | + "percent-encoding", | |
| 3162 | + "pin-project-lite", | |
| 3163 | + "quinn", | |
| 3164 | + "rustls", | |
| 3165 | + "rustls-pki-types", | |
| 3166 | + "rustls-platform-verifier", | |
| 3167 | + "serde", | |
| 3168 | + "serde_json", | |
| 3169 | + "sync_wrapper", | |
| 3170 | + "tokio", | |
| 3171 | + "tokio-rustls", | |
| 3172 | + "tokio-util", | |
| 3173 | + "tower", | |
| 3174 | + "tower-http 0.6.11", | |
| 3175 | + "tower-service", | |
| 3176 | + "url", | |
| 3177 | + "wasm-bindgen", | |
| 3178 | + "wasm-bindgen-futures", | |
| 3179 | + "wasm-streams", | |
| 3180 | + "web-sys", | |
| 3181 | +] | |
| 3182 | + | |
| 3183 | +[[package]] | |
| 3184 | +name = "rfc6979" | |
| 3185 | +version = "0.6.0" | |
| 3186 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3187 | +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" | |
| 3188 | +dependencies = [ | |
| 3189 | + "crypto-bigint", | |
| 3190 | + "hmac", | |
| 3191 | +] | |
| 3192 | + | |
| 3193 | +[[package]] | |
| 3194 | +name = "ring" | |
| 3195 | +version = "0.17.14" | |
| 3196 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3197 | +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" | |
| 3198 | +dependencies = [ | |
| 3199 | + "cc", | |
| 3200 | + "cfg-if", | |
| 3201 | + "getrandom 0.2.17", | |
| 3202 | + "libc", | |
| 3203 | + "untrusted 0.9.0", | |
| 3204 | + "windows-sys 0.52.0", | |
| 3205 | +] | |
| 3206 | + | |
| 3207 | +[[package]] | |
| 3208 | +name = "rsa" | |
| 3209 | +version = "0.10.0-rc.18" | |
| 3210 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3211 | +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" | |
| 3212 | +dependencies = [ | |
| 3213 | + "const-oid", | |
| 3214 | + "crypto-bigint", | |
| 3215 | + "crypto-primes", | |
| 3216 | + "digest 0.11.3", | |
| 3217 | + "pkcs1", | |
| 3218 | + "pkcs8", | |
| 3219 | + "rand_core 0.10.1", | |
| 3220 | + "sha2 0.11.0", | |
| 3221 | + "signature", | |
| 3222 | + "spki", | |
| 3223 | + "zeroize", | |
| 3224 | +] | |
| 3225 | + | |
| 3226 | +[[package]] | |
| 3227 | +name = "russh" | |
| 3228 | +version = "0.64.1" | |
| 3229 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3230 | +checksum = "ba61e87b9ec9a39a59a6bbed4c0b8ff7fb07073405b24a767a1f2c8560ac8f58" | |
| 3231 | +dependencies = [ | |
| 3232 | + "aes", | |
| 3233 | + "aws-lc-rs", | |
| 3234 | + "bitflags 2.13.2", | |
| 3235 | + "block-padding", | |
| 3236 | + "byteorder", | |
| 3237 | + "bytes", | |
| 3238 | + "cbc", | |
| 3239 | + "cipher", | |
| 3240 | + "crypto-bigint", | |
| 3241 | + "ctr", | |
| 3242 | + "curve25519-dalek", | |
| 3243 | + "data-encoding", | |
| 3244 | + "delegate", | |
| 3245 | + "der", | |
| 3246 | + "digest 0.11.3", | |
| 3247 | + "ecdsa", | |
| 3248 | + "ed25519-dalek", | |
| 3249 | + "elliptic-curve", | |
| 3250 | + "enum_dispatch", | |
| 3251 | + "flate2", | |
| 3252 | + "futures", | |
| 3253 | + "generic-array 1.4.5", | |
| 3254 | + "getrandom 0.4.3", | |
| 3255 | + "ghash", | |
| 3256 | + "hex-literal", | |
| 3257 | + "hmac", | |
| 3258 | + "inout", | |
| 3259 | + "keccak", | |
| 3260 | + "log", | |
| 3261 | + "md5", | |
| 3262 | + "ml-kem", | |
| 3263 | + "module-lattice", | |
| 3264 | + "num-bigint", | |
| 3265 | + "p256", | |
| 3266 | + "p384", | |
| 3267 | + "p521", | |
| 3268 | + "pageant", | |
| 3269 | + "pbkdf2", | |
| 3270 | + "pkcs1", | |
| 3271 | + "pkcs5", | |
| 3272 | + "pkcs8", | |
| 3273 | + "polyval", | |
| 3274 | + "rand 0.10.3", | |
| 3275 | + "rand_core 0.10.1", | |
| 3276 | + "rsa", | |
| 3277 | + "russh-cryptovec", | |
| 3278 | + "russh-util", | |
| 3279 | + "salsa20", | |
| 3280 | + "scrypt", | |
| 3281 | + "sec1", | |
| 3282 | + "sha1 0.11.0", | |
| 3283 | + "sha2 0.11.0", | |
| 3284 | + "sha3 0.12.0", | |
| 3285 | + "signature", | |
| 3286 | + "spki", | |
| 3287 | + "ssh-encoding", | |
| 3288 | + "ssh-key", | |
| 3289 | + "subtle", | |
| 3290 | + "thiserror", | |
| 3291 | + "tokio", | |
| 3292 | + "typenum", | |
| 3293 | + "universal-hash", | |
| 3294 | + "zeroize", | |
| 3295 | +] | |
| 3296 | + | |
| 3297 | +[[package]] | |
| 3298 | +name = "russh-cryptovec" | |
| 3299 | +version = "0.62.0" | |
| 3300 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3301 | +checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438" | |
| 3302 | +dependencies = [ | |
| 3303 | + "log", | |
| 3304 | + "nix", | |
| 3305 | + "ssh-encoding", | |
| 3306 | + "windows-sys 0.61.2", | |
| 3307 | +] | |
| 3308 | + | |
| 3309 | +[[package]] | |
| 3310 | +name = "russh-util" | |
| 3311 | +version = "0.52.0" | |
| 3312 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3313 | +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" | |
| 3314 | +dependencies = [ | |
| 3315 | + "chrono", | |
| 3316 | + "tokio", | |
| 3317 | + "wasm-bindgen", | |
| 3318 | + "wasm-bindgen-futures", | |
| 3319 | +] | |
| 3320 | + | |
| 3321 | +[[package]] | |
| 3322 | +name = "rust-embed" | |
| 3323 | +version = "8.13.0" | |
| 3324 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3325 | +checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" | |
| 3326 | +dependencies = [ | |
| 3327 | + "rust-embed-impl", | |
| 3328 | + "rust-embed-utils", | |
| 3329 | + "walkdir", | |
| 3330 | +] | |
| 3331 | + | |
| 3332 | +[[package]] | |
| 3333 | +name = "rust-embed-impl" | |
| 3334 | +version = "8.13.0" | |
| 3335 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3336 | +checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" | |
| 3337 | +dependencies = [ | |
| 3338 | + "mime_guess", | |
| 3339 | + "proc-macro2", | |
| 3340 | + "quote", | |
| 3341 | + "rust-embed-utils", | |
| 3342 | + "syn 2.0.119", | |
| 3343 | + "walkdir", | |
| 3344 | +] | |
| 3345 | + | |
| 3346 | +[[package]] | |
| 3347 | +name = "rust-embed-utils" | |
| 3348 | +version = "8.13.0" | |
| 3349 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3350 | +checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" | |
| 3351 | +dependencies = [ | |
| 3352 | + "sha2 0.11.0", | |
| 3353 | + "walkdir", | |
| 3354 | +] | |
| 3355 | + | |
| 3356 | +[[package]] | |
| 3357 | +name = "rustc-hash" | |
| 3358 | +version = "2.1.3" | |
| 3359 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3360 | +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" | |
| 3361 | + | |
| 3362 | +[[package]] | |
| 3363 | +name = "rustc_version" | |
| 3364 | +version = "0.4.1" | |
| 3365 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3366 | +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" | |
| 3367 | +dependencies = [ | |
| 3368 | + "semver", | |
| 3369 | +] | |
| 3370 | + | |
| 3371 | +[[package]] | |
| 3372 | +name = "rustix" | |
| 3373 | +version = "1.1.5" | |
| 3374 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3375 | +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" | |
| 3376 | +dependencies = [ | |
| 3377 | + "bitflags 2.13.2", | |
| 3378 | + "errno", | |
| 3379 | + "libc", | |
| 3380 | + "linux-raw-sys", | |
| 3381 | + "windows-sys 0.61.2", | |
| 3382 | +] | |
| 3383 | + | |
| 3384 | +[[package]] | |
| 3385 | +name = "rustls" | |
| 3386 | +version = "0.23.45" | |
| 3387 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3388 | +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" | |
| 3389 | +dependencies = [ | |
| 3390 | + "aws-lc-rs", | |
| 3391 | + "log", | |
| 3392 | + "once_cell", | |
| 3393 | + "ring", | |
| 3394 | + "rustls-pki-types", | |
| 3395 | + "rustls-webpki", | |
| 3396 | + "subtle", | |
| 3397 | + "zeroize", | |
| 3398 | +] | |
| 3399 | + | |
| 3400 | +[[package]] | |
| 3401 | +name = "rustls-native-certs" | |
| 3402 | +version = "0.8.4" | |
| 3403 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3404 | +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" | |
| 3405 | +dependencies = [ | |
| 3406 | + "openssl-probe", | |
| 3407 | + "rustls-pki-types", | |
| 3408 | + "schannel", | |
| 3409 | + "security-framework", | |
| 3410 | +] | |
| 3411 | + | |
| 3412 | +[[package]] | |
| 3413 | +name = "rustls-pki-types" | |
| 3414 | +version = "1.15.1" | |
| 3415 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3416 | +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" | |
| 3417 | +dependencies = [ | |
| 3418 | + "web-time", | |
| 3419 | + "zeroize", | |
| 3420 | +] | |
| 3421 | + | |
| 3422 | +[[package]] | |
| 3423 | +name = "rustls-platform-verifier" | |
| 3424 | +version = "0.7.1" | |
| 3425 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3426 | +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" | |
| 3427 | +dependencies = [ | |
| 3428 | + "core-foundation", | |
| 3429 | + "core-foundation-sys", | |
| 3430 | + "jni", | |
| 3431 | + "log", | |
| 3432 | + "once_cell", | |
| 3433 | + "rustls", | |
| 3434 | + "rustls-native-certs", | |
| 3435 | + "rustls-platform-verifier-android", | |
| 3436 | + "rustls-webpki", | |
| 3437 | + "security-framework", | |
| 3438 | + "security-framework-sys", | |
| 3439 | + "webpki-root-certs", | |
| 3440 | + "windows-sys 0.61.2", | |
| 3441 | +] | |
| 3442 | + | |
| 3443 | +[[package]] | |
| 3444 | +name = "rustls-platform-verifier-android" | |
| 3445 | +version = "0.2.0" | |
| 3446 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3447 | +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" | |
| 3448 | + | |
| 3449 | +[[package]] | |
| 3450 | +name = "rustls-webpki" | |
| 3451 | +version = "0.103.15" | |
| 3452 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3453 | +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" | |
| 3454 | +dependencies = [ | |
| 3455 | + "aws-lc-rs", | |
| 3456 | + "ring", | |
| 3457 | + "rustls-pki-types", | |
| 3458 | + "untrusted 0.9.0", | |
| 3459 | +] | |
| 3460 | + | |
| 3461 | +[[package]] | |
| 3462 | +name = "rustversion" | |
| 3463 | +version = "1.0.23" | |
| 3464 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3465 | +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" | |
| 3466 | + | |
| 3467 | +[[package]] | |
| 3468 | +name = "rusty-s3" | |
| 3469 | +version = "0.10.2" | |
| 3470 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3471 | +checksum = "2412ec048709e6a8ee4ee1505313bce003f6b8b27fc41a97e30615223c7ed592" | |
| 3472 | +dependencies = [ | |
| 3473 | + "base64 0.23.1", | |
| 3474 | + "hmac", | |
| 3475 | + "instant-xml", | |
| 3476 | + "jiff", | |
| 3477 | + "md-5", | |
| 3478 | + "percent-encoding", | |
| 3479 | + "serde", | |
| 3480 | + "serde_json", | |
| 3481 | + "sha2 0.11.0", | |
| 3482 | + "url", | |
| 3483 | + "zeroize", | |
| 3484 | +] | |
| 3485 | + | |
| 3486 | +[[package]] | |
| 3487 | +name = "ryu" | |
| 3488 | +version = "1.0.23" | |
| 3489 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3490 | +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" | |
| 3491 | + | |
| 3492 | +[[package]] | |
| 3493 | +name = "salsa20" | |
| 3494 | +version = "0.11.0" | |
| 3495 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3496 | +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" | |
| 3497 | +dependencies = [ | |
| 3498 | + "cfg-if", | |
| 3499 | + "cipher", | |
| 3500 | +] | |
| 3501 | + | |
| 3502 | +[[package]] | |
| 3503 | +name = "same-file" | |
| 3504 | +version = "1.0.6" | |
| 3505 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3506 | +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" | |
| 3507 | +dependencies = [ | |
| 3508 | + "winapi-util", | |
| 3509 | +] | |
| 3510 | + | |
| 3511 | +[[package]] | |
| 3512 | +name = "schannel" | |
| 3513 | +version = "0.1.29" | |
| 3514 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3515 | +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" | |
| 3516 | +dependencies = [ | |
| 3517 | + "windows-sys 0.61.2", | |
| 3518 | +] | |
| 3519 | + | |
| 3520 | +[[package]] | |
| 3521 | +name = "scopeguard" | |
| 3522 | +version = "1.2.0" | |
| 3523 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3524 | +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" | |
| 3525 | + | |
| 3526 | +[[package]] | |
| 3527 | +name = "scrypt" | |
| 3528 | +version = "0.12.0" | |
| 3529 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3530 | +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" | |
| 3531 | +dependencies = [ | |
| 3532 | + "cfg-if", | |
| 3533 | + "pbkdf2", | |
| 3534 | + "salsa20", | |
| 3535 | + "sha2 0.11.0", | |
| 3536 | +] | |
| 3537 | + | |
| 3538 | +[[package]] | |
| 3539 | +name = "sec1" | |
| 3540 | +version = "0.8.1" | |
| 3541 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3542 | +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" | |
| 3543 | +dependencies = [ | |
| 3544 | + "base16ct", | |
| 3545 | + "ctutils", | |
| 3546 | + "der", | |
| 3547 | + "hybrid-array", | |
| 3548 | + "subtle", | |
| 3549 | + "zeroize", | |
| 3550 | +] | |
| 3551 | + | |
| 3552 | +[[package]] | |
| 3553 | +name = "security-framework" | |
| 3554 | +version = "3.7.0" | |
| 3555 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3556 | +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" | |
| 3557 | +dependencies = [ | |
| 3558 | + "bitflags 2.13.2", | |
| 3559 | + "core-foundation", | |
| 3560 | + "core-foundation-sys", | |
| 3561 | + "libc", | |
| 3562 | + "security-framework-sys", | |
| 3563 | +] | |
| 3564 | + | |
| 3565 | +[[package]] | |
| 3566 | +name = "security-framework-sys" | |
| 3567 | +version = "2.17.0" | |
| 3568 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3569 | +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" | |
| 3570 | +dependencies = [ | |
| 3571 | + "core-foundation-sys", | |
| 3572 | + "libc", | |
| 3573 | +] | |
| 3574 | + | |
| 3575 | +[[package]] | |
| 3576 | +name = "semver" | |
| 3577 | +version = "1.0.28" | |
| 3578 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3579 | +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" | |
| 3580 | + | |
| 3581 | +[[package]] | |
| 3582 | +name = "serde" | |
| 3583 | +version = "1.0.229" | |
| 3584 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3585 | +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" | |
| 3586 | +dependencies = [ | |
| 3587 | + "serde_core", | |
| 3588 | + "serde_derive", | |
| 3589 | +] | |
| 3590 | + | |
| 3591 | +[[package]] | |
| 3592 | +name = "serde_core" | |
| 3593 | +version = "1.0.229" | |
| 3594 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3595 | +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" | |
| 3596 | +dependencies = [ | |
| 3597 | + "serde_derive", | |
| 3598 | +] | |
| 3599 | + | |
| 3600 | +[[package]] | |
| 3601 | +name = "serde_derive" | |
| 3602 | +version = "1.0.229" | |
| 3603 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3604 | +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" | |
| 3605 | +dependencies = [ | |
| 3606 | + "proc-macro2", | |
| 3607 | + "quote", | |
| 3608 | + "syn 3.0.6", | |
| 3609 | +] | |
| 3610 | + | |
| 3611 | +[[package]] | |
| 3612 | +name = "serde_json" | |
| 3613 | +version = "1.0.151" | |
| 3614 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3615 | +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" | |
| 3616 | +dependencies = [ | |
| 3617 | + "itoa", | |
| 3618 | + "memchr", | |
| 3619 | + "serde", | |
| 3620 | + "serde_core", | |
| 3621 | + "zmij", | |
| 3622 | +] | |
| 3623 | + | |
| 3624 | +[[package]] | |
| 3625 | +name = "serde_path_to_error" | |
| 3626 | +version = "0.1.20" | |
| 3627 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3628 | +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" | |
| 3629 | +dependencies = [ | |
| 3630 | + "itoa", | |
| 3631 | + "serde", | |
| 3632 | + "serde_core", | |
| 3633 | +] | |
| 3634 | + | |
| 3635 | +[[package]] | |
| 3636 | +name = "serde_spanned" | |
| 3637 | +version = "1.1.2" | |
| 3638 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3639 | +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" | |
| 3640 | +dependencies = [ | |
| 3641 | + "serde_core", | |
| 3642 | +] | |
| 3643 | + | |
| 3644 | +[[package]] | |
| 3645 | +name = "serde_urlencoded" | |
| 3646 | +version = "0.7.1" | |
| 3647 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3648 | +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" | |
| 3649 | +dependencies = [ | |
| 3650 | + "form_urlencoded", | |
| 3651 | + "itoa", | |
| 3652 | + "ryu", | |
| 3653 | + "serde", | |
| 3654 | +] | |
| 3655 | + | |
| 3656 | +[[package]] | |
| 3657 | +name = "serdect" | |
| 3658 | +version = "0.4.3" | |
| 3659 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3660 | +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" | |
| 3661 | +dependencies = [ | |
| 3662 | + "base16ct", | |
| 3663 | + "serde", | |
| 3664 | +] | |
| 3665 | + | |
| 3666 | +[[package]] | |
| 3667 | +name = "sha1" | |
| 3668 | +version = "0.10.7" | |
| 3669 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3670 | +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" | |
| 3671 | +dependencies = [ | |
| 3672 | + "cfg-if", | |
| 3673 | + "cpufeatures 0.2.17", | |
| 3674 | + "digest 0.10.7", | |
| 3675 | +] | |
| 3676 | + | |
| 3677 | +[[package]] | |
| 3678 | +name = "sha1" | |
| 3679 | +version = "0.11.0" | |
| 3680 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3681 | +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" | |
| 3682 | +dependencies = [ | |
| 3683 | + "cfg-if", | |
| 3684 | + "cpufeatures 0.3.1", | |
| 3685 | + "digest 0.11.3", | |
| 3686 | +] | |
| 3687 | + | |
| 3688 | +[[package]] | |
| 3689 | +name = "sha2" | |
| 3690 | +version = "0.10.9" | |
| 3691 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3692 | +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | |
| 3693 | +dependencies = [ | |
| 3694 | + "cfg-if", | |
| 3695 | + "cpufeatures 0.2.17", | |
| 3696 | + "digest 0.10.7", | |
| 3697 | +] | |
| 3698 | + | |
| 3699 | +[[package]] | |
| 3700 | +name = "sha2" | |
| 3701 | +version = "0.11.0" | |
| 3702 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3703 | +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" | |
| 3704 | +dependencies = [ | |
| 3705 | + "cfg-if", | |
| 3706 | + "cpufeatures 0.3.1", | |
| 3707 | + "digest 0.11.3", | |
| 3708 | +] | |
| 3709 | + | |
| 3710 | +[[package]] | |
| 3711 | +name = "sha3" | |
| 3712 | +version = "0.11.0" | |
| 3713 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3714 | +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" | |
| 3715 | +dependencies = [ | |
| 3716 | + "digest 0.11.3", | |
| 3717 | + "keccak", | |
| 3718 | +] | |
| 3719 | + | |
| 3720 | +[[package]] | |
| 3721 | +name = "sha3" | |
| 3722 | +version = "0.12.0" | |
| 3723 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3724 | +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" | |
| 3725 | +dependencies = [ | |
| 3726 | + "digest 0.11.3", | |
| 3727 | + "keccak", | |
| 3728 | + "sponge-cursor", | |
| 3729 | +] | |
| 3730 | + | |
| 3731 | +[[package]] | |
| 3732 | +name = "sharded-slab" | |
| 3733 | +version = "0.1.7" | |
| 3734 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3735 | +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" | |
| 3736 | +dependencies = [ | |
| 3737 | + "lazy_static", | |
| 3738 | +] | |
| 3739 | + | |
| 3740 | +[[package]] | |
| 3741 | +name = "shell-words" | |
| 3742 | +version = "1.1.1" | |
| 3743 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3744 | +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" | |
| 3745 | + | |
| 3746 | +[[package]] | |
| 3747 | +name = "shlex" | |
| 3748 | +version = "2.0.1" | |
| 3749 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3750 | +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" | |
| 3751 | + | |
| 3752 | +[[package]] | |
| 3753 | +name = "signal-hook-registry" | |
| 3754 | +version = "1.4.8" | |
| 3755 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3756 | +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" | |
| 3757 | +dependencies = [ | |
| 3758 | + "errno", | |
| 3759 | + "libc", | |
| 3760 | +] | |
| 3761 | + | |
| 3762 | +[[package]] | |
| 3763 | +name = "signature" | |
| 3764 | +version = "3.0.0" | |
| 3765 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3766 | +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" | |
| 3767 | +dependencies = [ | |
| 3768 | + "digest 0.11.3", | |
| 3769 | + "rand_core 0.10.1", | |
| 3770 | +] | |
| 3771 | + | |
| 3772 | +[[package]] | |
| 3773 | +name = "simd-adler32" | |
| 3774 | +version = "0.3.10" | |
| 3775 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3776 | +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" | |
| 3777 | + | |
| 3778 | +[[package]] | |
| 3779 | +name = "simd_cesu8" | |
| 3780 | +version = "1.2.0" | |
| 3781 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3782 | +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" | |
| 3783 | +dependencies = [ | |
| 3784 | + "rustc_version", | |
| 3785 | + "simdutf8", | |
| 3786 | +] | |
| 3787 | + | |
| 3788 | +[[package]] | |
| 3789 | +name = "simdutf8" | |
| 3790 | +version = "0.1.5" | |
| 3791 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3792 | +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" | |
| 3793 | + | |
| 3794 | +[[package]] | |
| 3795 | +name = "siphasher" | |
| 3796 | +version = "1.0.4" | |
| 3797 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3798 | +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" | |
| 3799 | + | |
| 3800 | +[[package]] | |
| 3801 | +name = "slab" | |
| 3802 | +version = "0.4.12" | |
| 3803 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3804 | +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" | |
| 3805 | + | |
| 3806 | +[[package]] | |
| 3807 | +name = "smallvec" | |
| 3808 | +version = "1.16.2" | |
| 3809 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3810 | +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" | |
| 3811 | +dependencies = [ | |
| 3812 | + "serde", | |
| 3813 | +] | |
| 3814 | + | |
| 3815 | +[[package]] | |
| 3816 | +name = "socket2" | |
| 3817 | +version = "0.6.5" | |
| 3818 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3819 | +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" | |
| 3820 | +dependencies = [ | |
| 3821 | + "libc", | |
| 3822 | + "windows-sys 0.61.2", | |
| 3823 | +] | |
| 3824 | + | |
| 3825 | +[[package]] | |
| 3826 | +name = "spin" | |
| 3827 | +version = "0.9.9" | |
| 3828 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3829 | +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" | |
| 3830 | +dependencies = [ | |
| 3831 | + "lock_api", | |
| 3832 | +] | |
| 3833 | + | |
| 3834 | +[[package]] | |
| 3835 | +name = "spki" | |
| 3836 | +version = "0.8.1" | |
| 3837 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3838 | +checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" | |
| 3839 | +dependencies = [ | |
| 3840 | + "base64ct", | |
| 3841 | + "der", | |
| 3842 | +] | |
| 3843 | + | |
| 3844 | +[[package]] | |
| 3845 | +name = "sponge-cursor" | |
| 3846 | +version = "0.1.0" | |
| 3847 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3848 | +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" | |
| 3849 | + | |
| 3850 | +[[package]] | |
| 3851 | +name = "sqlx" | |
| 3852 | +version = "0.9.0" | |
| 3853 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3854 | +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" | |
| 3855 | +dependencies = [ | |
| 3856 | + "sqlx-core", | |
| 3857 | + "sqlx-macros", | |
| 3858 | + "sqlx-mysql", | |
| 3859 | + "sqlx-postgres", | |
| 3860 | + "sqlx-sqlite", | |
| 3861 | +] | |
| 3862 | + | |
| 3863 | +[[package]] | |
| 3864 | +name = "sqlx-core" | |
| 3865 | +version = "0.9.0" | |
| 3866 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3867 | +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" | |
| 3868 | +dependencies = [ | |
| 3869 | + "base64 0.22.1", | |
| 3870 | + "bytes", | |
| 3871 | + "cfg-if", | |
| 3872 | + "chrono", | |
| 3873 | + "crc", | |
| 3874 | + "crossbeam-queue", | |
| 3875 | + "either", | |
| 3876 | + "event-listener", | |
| 3877 | + "futures-core", | |
| 3878 | + "futures-intrusive", | |
| 3879 | + "futures-io", | |
| 3880 | + "futures-util", | |
| 3881 | + "hashbrown 0.16.1", | |
| 3882 | + "hashlink", | |
| 3883 | + "indexmap", | |
| 3884 | + "log", | |
| 3885 | + "memchr", | |
| 3886 | + "percent-encoding", | |
| 3887 | + "rustls", | |
| 3888 | + "serde", | |
| 3889 | + "serde_json", | |
| 3890 | + "sha2 0.10.9", | |
| 3891 | + "smallvec", | |
| 3892 | + "thiserror", | |
| 3893 | + "tokio", | |
| 3894 | + "tokio-stream", | |
| 3895 | + "tracing", | |
| 3896 | + "url", | |
| 3897 | + "uuid", | |
| 3898 | + "webpki-roots", | |
| 3899 | +] | |
| 3900 | + | |
| 3901 | +[[package]] | |
| 3902 | +name = "sqlx-macros" | |
| 3903 | +version = "0.9.0" | |
| 3904 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3905 | +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" | |
| 3906 | +dependencies = [ | |
| 3907 | + "proc-macro2", | |
| 3908 | + "quote", | |
| 3909 | + "sqlx-core", | |
| 3910 | + "sqlx-macros-core", | |
| 3911 | + "syn 2.0.119", | |
| 3912 | +] | |
| 3913 | + | |
| 3914 | +[[package]] | |
| 3915 | +name = "sqlx-macros-core" | |
| 3916 | +version = "0.9.0" | |
| 3917 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3918 | +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" | |
| 3919 | +dependencies = [ | |
| 3920 | + "cfg-if", | |
| 3921 | + "dotenvy", | |
| 3922 | + "either", | |
| 3923 | + "heck", | |
| 3924 | + "hex", | |
| 3925 | + "proc-macro2", | |
| 3926 | + "quote", | |
| 3927 | + "serde", | |
| 3928 | + "serde_json", | |
| 3929 | + "sha2 0.10.9", | |
| 3930 | + "sqlx-core", | |
| 3931 | + "sqlx-mysql", | |
| 3932 | + "sqlx-postgres", | |
| 3933 | + "sqlx-sqlite", | |
| 3934 | + "syn 2.0.119", | |
| 3935 | + "thiserror", | |
| 3936 | + "tokio", | |
| 3937 | + "url", | |
| 3938 | +] | |
| 3939 | + | |
| 3940 | +[[package]] | |
| 3941 | +name = "sqlx-mysql" | |
| 3942 | +version = "0.9.0" | |
| 3943 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3944 | +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" | |
| 3945 | +dependencies = [ | |
| 3946 | + "bitflags 2.13.2", | |
| 3947 | + "byteorder", | |
| 3948 | + "bytes", | |
| 3949 | + "chrono", | |
| 3950 | + "crc", | |
| 3951 | + "digest 0.11.3", | |
| 3952 | + "dotenvy", | |
| 3953 | + "either", | |
| 3954 | + "futures-core", | |
| 3955 | + "futures-util", | |
| 3956 | + "generic-array 0.14.7", | |
| 3957 | + "log", | |
| 3958 | + "percent-encoding", | |
| 3959 | + "serde", | |
| 3960 | + "sha1 0.11.0", | |
| 3961 | + "sha2 0.11.0", | |
| 3962 | + "sqlx-core", | |
| 3963 | + "thiserror", | |
| 3964 | + "tracing", | |
| 3965 | + "uuid", | |
| 3966 | +] | |
| 3967 | + | |
| 3968 | +[[package]] | |
| 3969 | +name = "sqlx-postgres" | |
| 3970 | +version = "0.9.0" | |
| 3971 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3972 | +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" | |
| 3973 | +dependencies = [ | |
| 3974 | + "atoi", | |
| 3975 | + "base64 0.22.1", | |
| 3976 | + "bitflags 2.13.2", | |
| 3977 | + "byteorder", | |
| 3978 | + "chrono", | |
| 3979 | + "crc", | |
| 3980 | + "dotenvy", | |
| 3981 | + "etcetera", | |
| 3982 | + "futures-channel", | |
| 3983 | + "futures-core", | |
| 3984 | + "futures-util", | |
| 3985 | + "hex", | |
| 3986 | + "hkdf", | |
| 3987 | + "hmac", | |
| 3988 | + "itoa", | |
| 3989 | + "log", | |
| 3990 | + "md-5", | |
| 3991 | + "memchr", | |
| 3992 | + "rand 0.10.3", | |
| 3993 | + "serde", | |
| 3994 | + "serde_json", | |
| 3995 | + "sha2 0.11.0", | |
| 3996 | + "smallvec", | |
| 3997 | + "sqlx-core", | |
| 3998 | + "stringprep", | |
| 3999 | + "thiserror", | |
| 4000 | + "tracing", | |
| 4001 | + "uuid", | |
| 4002 | + "whoami", | |
| 4003 | +] | |
| 4004 | + | |
| 4005 | +[[package]] | |
| 4006 | +name = "sqlx-sqlite" | |
| 4007 | +version = "0.9.0" | |
| 4008 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4009 | +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" | |
| 4010 | +dependencies = [ | |
| 4011 | + "atoi", | |
| 4012 | + "chrono", | |
| 4013 | + "flume", | |
| 4014 | + "form_urlencoded", | |
| 4015 | + "futures-channel", | |
| 4016 | + "futures-core", | |
| 4017 | + "futures-executor", | |
| 4018 | + "futures-intrusive", | |
| 4019 | + "futures-util", | |
| 4020 | + "libsqlite3-sys", | |
| 4021 | + "log", | |
| 4022 | + "percent-encoding", | |
| 4023 | + "serde", | |
| 4024 | + "sqlx-core", | |
| 4025 | + "thiserror", | |
| 4026 | + "tracing", | |
| 4027 | + "url", | |
| 4028 | + "uuid", | |
| 4029 | +] | |
| 4030 | + | |
| 4031 | +[[package]] | |
| 4032 | +name = "ssh-cipher" | |
| 4033 | +version = "0.3.0" | |
| 4034 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4035 | +checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597" | |
| 4036 | +dependencies = [ | |
| 4037 | + "aead", | |
| 4038 | + "aes", | |
| 4039 | + "aes-gcm", | |
| 4040 | + "chacha20", | |
| 4041 | + "cipher", | |
| 4042 | + "ctutils", | |
| 4043 | + "des", | |
| 4044 | + "poly1305", | |
| 4045 | + "ssh-encoding", | |
| 4046 | + "zeroize", | |
| 4047 | +] | |
| 4048 | + | |
| 4049 | +[[package]] | |
| 4050 | +name = "ssh-encoding" | |
| 4051 | +version = "0.3.0" | |
| 4052 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4053 | +checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" | |
| 4054 | +dependencies = [ | |
| 4055 | + "base64ct", | |
| 4056 | + "bytes", | |
| 4057 | + "crypto-bigint", | |
| 4058 | + "ctutils", | |
| 4059 | + "digest 0.11.3", | |
| 4060 | + "pem-rfc7468", | |
| 4061 | + "zeroize", | |
| 4062 | +] | |
| 4063 | + | |
| 4064 | +[[package]] | |
| 4065 | +name = "ssh-key" | |
| 4066 | +version = "0.7.0-rc.11" | |
| 4067 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4068 | +checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3" | |
| 4069 | +dependencies = [ | |
| 4070 | + "argon2", | |
| 4071 | + "bcrypt-pbkdf", | |
| 4072 | + "ctutils", | |
| 4073 | + "ed25519-dalek", | |
| 4074 | + "hex", | |
| 4075 | + "hmac", | |
| 4076 | + "p256", | |
| 4077 | + "p384", | |
| 4078 | + "p521", | |
| 4079 | + "rand_core 0.10.1", | |
| 4080 | + "rsa", | |
| 4081 | + "sec1", | |
| 4082 | + "sha1 0.11.0", | |
| 4083 | + "sha2 0.11.0", | |
| 4084 | + "signature", | |
| 4085 | + "ssh-cipher", | |
| 4086 | + "ssh-encoding", | |
| 4087 | + "zeroize", | |
| 4088 | +] | |
| 4089 | + | |
| 4090 | +[[package]] | |
| 4091 | +name = "stable_deref_trait" | |
| 4092 | +version = "1.2.1" | |
| 4093 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4094 | +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" | |
| 4095 | + | |
| 4096 | +[[package]] | |
| 4097 | +name = "stringprep" | |
| 4098 | +version = "0.1.5" | |
| 4099 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4100 | +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" | |
| 4101 | +dependencies = [ | |
| 4102 | + "unicode-bidi", | |
| 4103 | + "unicode-normalization", | |
| 4104 | + "unicode-properties", | |
| 4105 | +] | |
| 4106 | + | |
| 4107 | +[[package]] | |
| 4108 | +name = "strsim" | |
| 4109 | +version = "0.11.1" | |
| 4110 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4111 | +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |
| 4112 | + | |
| 4113 | +[[package]] | |
| 4114 | +name = "subtle" | |
| 4115 | +version = "2.6.1" | |
| 4116 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4117 | +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | |
| 4118 | + | |
| 4119 | +[[package]] | |
| 4120 | +name = "symlink" | |
| 4121 | +version = "0.1.0" | |
| 4122 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4123 | +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" | |
| 4124 | + | |
| 4125 | +[[package]] | |
| 4126 | +name = "syn" | |
| 4127 | +version = "2.0.119" | |
| 4128 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4129 | +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" | |
| 4130 | +dependencies = [ | |
| 4131 | + "proc-macro2", | |
| 4132 | + "quote", | |
| 4133 | + "unicode-ident", | |
| 4134 | +] | |
| 4135 | + | |
| 4136 | +[[package]] | |
| 4137 | +name = "syn" | |
| 4138 | +version = "3.0.6" | |
| 4139 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4140 | +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" | |
| 4141 | +dependencies = [ | |
| 4142 | + "proc-macro2", | |
| 4143 | + "quote", | |
| 4144 | + "unicode-ident", | |
| 4145 | +] | |
| 4146 | + | |
| 4147 | +[[package]] | |
| 4148 | +name = "sync_wrapper" | |
| 4149 | +version = "1.0.2" | |
| 4150 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4151 | +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" | |
| 4152 | +dependencies = [ | |
| 4153 | + "futures-core", | |
| 4154 | +] | |
| 4155 | + | |
| 4156 | +[[package]] | |
| 4157 | +name = "synstructure" | |
| 4158 | +version = "0.14.0" | |
| 4159 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4160 | +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" | |
| 4161 | +dependencies = [ | |
| 4162 | + "proc-macro2", | |
| 4163 | + "quote", | |
| 4164 | + "syn 3.0.6", | |
| 4165 | +] | |
| 4166 | + | |
| 4167 | +[[package]] | |
| 4168 | +name = "syntect" | |
| 4169 | +version = "5.3.0" | |
| 4170 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4171 | +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" | |
| 4172 | +dependencies = [ | |
| 4173 | + "bincode", | |
| 4174 | + "fancy-regex", | |
| 4175 | + "flate2", | |
| 4176 | + "fnv", | |
| 4177 | + "once_cell", | |
| 4178 | + "onig", | |
| 4179 | + "plist", | |
| 4180 | + "regex-syntax", | |
| 4181 | + "serde", | |
| 4182 | + "serde_derive", | |
| 4183 | + "serde_json", | |
| 4184 | + "thiserror", | |
| 4185 | + "walkdir", | |
| 4186 | + "yaml-rust", | |
| 4187 | +] | |
| 4188 | + | |
| 4189 | +[[package]] | |
| 4190 | +name = "terminal_size" | |
| 4191 | +version = "0.4.4" | |
| 4192 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4193 | +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" | |
| 4194 | +dependencies = [ | |
| 4195 | + "rustix", | |
| 4196 | + "windows-sys 0.61.2", | |
| 4197 | +] | |
| 4198 | + | |
| 4199 | +[[package]] | |
| 4200 | +name = "thiserror" | |
| 4201 | +version = "2.0.21" | |
| 4202 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4203 | +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" | |
| 4204 | +dependencies = [ | |
| 4205 | + "thiserror-impl", | |
| 4206 | +] | |
| 4207 | + | |
| 4208 | +[[package]] | |
| 4209 | +name = "thiserror-impl" | |
| 4210 | +version = "2.0.21" | |
| 4211 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4212 | +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" | |
| 4213 | +dependencies = [ | |
| 4214 | + "proc-macro2", | |
| 4215 | + "quote", | |
| 4216 | + "syn 3.0.6", | |
| 4217 | +] | |
| 4218 | + | |
| 4219 | +[[package]] | |
| 4220 | +name = "thread_local" | |
| 4221 | +version = "1.1.10" | |
| 4222 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4223 | +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" | |
| 4224 | +dependencies = [ | |
| 4225 | + "cfg-if", | |
| 4226 | +] | |
| 4227 | + | |
| 4228 | +[[package]] | |
| 4229 | +name = "time" | |
| 4230 | +version = "0.3.55" | |
| 4231 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4232 | +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" | |
| 4233 | +dependencies = [ | |
| 4234 | + "deranged", | |
| 4235 | + "num-conv", | |
| 4236 | + "powerfmt", | |
| 4237 | + "serde_core", | |
| 4238 | + "time-core", | |
| 4239 | + "time-macros", | |
| 4240 | +] | |
| 4241 | + | |
| 4242 | +[[package]] | |
| 4243 | +name = "time-core" | |
| 4244 | +version = "0.1.9" | |
| 4245 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4246 | +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" | |
| 4247 | + | |
| 4248 | +[[package]] | |
| 4249 | +name = "time-macros" | |
| 4250 | +version = "0.2.32" | |
| 4251 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4252 | +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" | |
| 4253 | +dependencies = [ | |
| 4254 | + "num-conv", | |
| 4255 | + "time-core", | |
| 4256 | +] | |
| 4257 | + | |
| 4258 | +[[package]] | |
| 4259 | +name = "tinystr" | |
| 4260 | +version = "0.8.4" | |
| 4261 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4262 | +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" | |
| 4263 | +dependencies = [ | |
| 4264 | + "displaydoc", | |
| 4265 | + "zerovec", | |
| 4266 | +] | |
| 4267 | + | |
| 4268 | +[[package]] | |
| 4269 | +name = "tinyvec" | |
| 4270 | +version = "1.13.3" | |
| 4271 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4272 | +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" | |
| 4273 | + | |
| 4274 | +[[package]] | |
| 4275 | +name = "tokio" | |
| 4276 | +version = "1.53.2" | |
| 4277 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4278 | +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" | |
| 4279 | +dependencies = [ | |
| 4280 | + "bytes", | |
| 4281 | + "libc", | |
| 4282 | + "mio", | |
| 4283 | + "parking_lot", | |
| 4284 | + "pin-project-lite", | |
| 4285 | + "signal-hook-registry", | |
| 4286 | + "socket2", | |
| 4287 | + "tokio-macros", | |
| 4288 | + "windows-sys 0.61.2", | |
| 4289 | +] | |
| 4290 | + | |
| 4291 | +[[package]] | |
| 4292 | +name = "tokio-macros" | |
| 4293 | +version = "2.7.2" | |
| 4294 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4295 | +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" | |
| 4296 | +dependencies = [ | |
| 4297 | + "proc-macro2", | |
| 4298 | + "quote", | |
| 4299 | + "syn 3.0.6", | |
| 4300 | +] | |
| 4301 | + | |
| 4302 | +[[package]] | |
| 4303 | +name = "tokio-rustls" | |
| 4304 | +version = "0.26.6" | |
| 4305 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4306 | +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" | |
| 4307 | +dependencies = [ | |
| 4308 | + "rustls", | |
| 4309 | + "tokio", | |
| 4310 | +] | |
| 4311 | + | |
| 4312 | +[[package]] | |
| 4313 | +name = "tokio-stream" | |
| 4314 | +version = "0.1.19" | |
| 4315 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4316 | +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" | |
| 4317 | +dependencies = [ | |
| 4318 | + "futures-core", | |
| 4319 | + "pin-project-lite", | |
| 4320 | + "tokio", | |
| 4321 | +] | |
| 4322 | + | |
| 4323 | +[[package]] | |
| 4324 | +name = "tokio-tungstenite" | |
| 4325 | +version = "0.29.0" | |
| 4326 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4327 | +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" | |
| 4328 | +dependencies = [ | |
| 4329 | + "futures-util", | |
| 4330 | + "log", | |
| 4331 | + "tokio", | |
| 4332 | + "tungstenite", | |
| 4333 | +] | |
| 4334 | + | |
| 4335 | +[[package]] | |
| 4336 | +name = "tokio-util" | |
| 4337 | +version = "0.7.19" | |
| 4338 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4339 | +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" | |
| 4340 | +dependencies = [ | |
| 4341 | + "bytes", | |
| 4342 | + "futures-core", | |
| 4343 | + "futures-sink", | |
| 4344 | + "libc", | |
| 4345 | + "pin-project-lite", | |
| 4346 | + "tokio", | |
| 4347 | +] | |
| 4348 | + | |
| 4349 | +[[package]] | |
| 4350 | +name = "toml" | |
| 4351 | +version = "1.1.7+spec-1.1.0" | |
| 4352 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4353 | +checksum = "7874cd34dd99040fc24e30923aa64a60a81ff3ddfe2b8129ccc84ba24949deed" | |
| 4354 | +dependencies = [ | |
| 4355 | + "indexmap", | |
| 4356 | + "serde_core", | |
| 4357 | + "serde_spanned", | |
| 4358 | + "toml_datetime", | |
| 4359 | + "toml_parser", | |
| 4360 | + "toml_writer", | |
| 4361 | + "winnow", | |
| 4362 | +] | |
| 4363 | + | |
| 4364 | +[[package]] | |
| 4365 | +name = "toml_datetime" | |
| 4366 | +version = "1.1.2+spec-1.1.0" | |
| 4367 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4368 | +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" | |
| 4369 | +dependencies = [ | |
| 4370 | + "serde_core", | |
| 4371 | +] | |
| 4372 | + | |
| 4373 | +[[package]] | |
| 4374 | +name = "toml_parser" | |
| 4375 | +version = "1.1.4+spec-1.1.0" | |
| 4376 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4377 | +checksum = "bed810ee007d8e7f9cf20b4774e7198142fa27186e0f805871da7f3c39eaf333" | |
| 4378 | +dependencies = [ | |
| 4379 | + "winnow", | |
| 4380 | +] | |
| 4381 | + | |
| 4382 | +[[package]] | |
| 4383 | +name = "toml_writer" | |
| 4384 | +version = "1.1.3+spec-1.1.0" | |
| 4385 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4386 | +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" | |
| 4387 | + | |
| 4388 | +[[package]] | |
| 4389 | +name = "tower" | |
| 4390 | +version = "0.5.3" | |
| 4391 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4392 | +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" | |
| 4393 | +dependencies = [ | |
| 4394 | + "futures-core", | |
| 4395 | + "futures-util", | |
| 4396 | + "pin-project-lite", | |
| 4397 | + "sync_wrapper", | |
| 4398 | + "tokio", | |
| 4399 | + "tower-layer", | |
| 4400 | + "tower-service", | |
| 4401 | + "tracing", | |
| 4402 | +] | |
| 4403 | + | |
| 4404 | +[[package]] | |
| 4405 | +name = "tower-http" | |
| 4406 | +version = "0.6.11" | |
| 4407 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4408 | +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" | |
| 4409 | +dependencies = [ | |
| 4410 | + "bitflags 2.13.2", | |
| 4411 | + "bytes", | |
| 4412 | + "futures-util", | |
| 4413 | + "http", | |
| 4414 | + "http-body", | |
| 4415 | + "pin-project-lite", | |
| 4416 | + "tower", | |
| 4417 | + "tower-layer", | |
| 4418 | + "tower-service", | |
| 4419 | + "url", | |
| 4420 | +] | |
| 4421 | + | |
| 4422 | +[[package]] | |
| 4423 | +name = "tower-http" | |
| 4424 | +version = "0.7.1" | |
| 4425 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4426 | +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" | |
| 4427 | +dependencies = [ | |
| 4428 | + "async-compression", | |
| 4429 | + "bitflags 2.13.2", | |
| 4430 | + "bytes", | |
| 4431 | + "futures-core", | |
| 4432 | + "http", | |
| 4433 | + "http-body", | |
| 4434 | + "http-body-util", | |
| 4435 | + "percent-encoding", | |
| 4436 | + "pin-project-lite", | |
| 4437 | + "tokio", | |
| 4438 | + "tokio-util", | |
| 4439 | + "tower-layer", | |
| 4440 | + "tower-service", | |
| 4441 | + "tracing", | |
| 4442 | +] | |
| 4443 | + | |
| 4444 | +[[package]] | |
| 4445 | +name = "tower-layer" | |
| 4446 | +version = "0.3.3" | |
| 4447 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4448 | +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" | |
| 4449 | + | |
| 4450 | +[[package]] | |
| 4451 | +name = "tower-service" | |
| 4452 | +version = "0.3.3" | |
| 4453 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4454 | +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" | |
| 4455 | + | |
| 4456 | +[[package]] | |
| 4457 | +name = "tracing" | |
| 4458 | +version = "0.1.44" | |
| 4459 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4460 | +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" | |
| 4461 | +dependencies = [ | |
| 4462 | + "log", | |
| 4463 | + "pin-project-lite", | |
| 4464 | + "tracing-attributes", | |
| 4465 | + "tracing-core", | |
| 4466 | +] | |
| 4467 | + | |
| 4468 | +[[package]] | |
| 4469 | +name = "tracing-appender" | |
| 4470 | +version = "0.2.5" | |
| 4471 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4472 | +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" | |
| 4473 | +dependencies = [ | |
| 4474 | + "crossbeam-channel", | |
| 4475 | + "symlink", | |
| 4476 | + "thiserror", | |
| 4477 | + "time", | |
| 4478 | + "tracing-subscriber", | |
| 4479 | +] | |
| 4480 | + | |
| 4481 | +[[package]] | |
| 4482 | +name = "tracing-attributes" | |
| 4483 | +version = "0.1.31" | |
| 4484 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4485 | +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" | |
| 4486 | +dependencies = [ | |
| 4487 | + "proc-macro2", | |
| 4488 | + "quote", | |
| 4489 | + "syn 2.0.119", | |
| 4490 | +] | |
| 4491 | + | |
| 4492 | +[[package]] | |
| 4493 | +name = "tracing-core" | |
| 4494 | +version = "0.1.36" | |
| 4495 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4496 | +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" | |
| 4497 | +dependencies = [ | |
| 4498 | + "once_cell", | |
| 4499 | + "valuable", | |
| 4500 | +] | |
| 4501 | + | |
| 4502 | +[[package]] | |
| 4503 | +name = "tracing-log" | |
| 4504 | +version = "0.2.0" | |
| 4505 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4506 | +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" | |
| 4507 | +dependencies = [ | |
| 4508 | + "log", | |
| 4509 | + "once_cell", | |
| 4510 | + "tracing-core", | |
| 4511 | +] | |
| 4512 | + | |
| 4513 | +[[package]] | |
| 4514 | +name = "tracing-serde" | |
| 4515 | +version = "0.2.0" | |
| 4516 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4517 | +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" | |
| 4518 | +dependencies = [ | |
| 4519 | + "serde", | |
| 4520 | + "tracing-core", | |
| 4521 | +] | |
| 4522 | + | |
| 4523 | +[[package]] | |
| 4524 | +name = "tracing-subscriber" | |
| 4525 | +version = "0.3.23" | |
| 4526 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4527 | +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" | |
| 4528 | +dependencies = [ | |
| 4529 | + "matchers", | |
| 4530 | + "nu-ansi-term", | |
| 4531 | + "once_cell", | |
| 4532 | + "regex-automata", | |
| 4533 | + "serde", | |
| 4534 | + "serde_json", | |
| 4535 | + "sharded-slab", | |
| 4536 | + "smallvec", | |
| 4537 | + "thread_local", | |
| 4538 | + "tracing", | |
| 4539 | + "tracing-core", | |
| 4540 | + "tracing-log", | |
| 4541 | + "tracing-serde", | |
| 4542 | +] | |
| 4543 | + | |
| 4544 | +[[package]] | |
| 4545 | +name = "try-lock" | |
| 4546 | +version = "0.2.5" | |
| 4547 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4548 | +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | |
| 4549 | + | |
| 4550 | +[[package]] | |
| 4551 | +name = "tungstenite" | |
| 4552 | +version = "0.29.0" | |
| 4553 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4554 | +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" | |
| 4555 | +dependencies = [ | |
| 4556 | + "bytes", | |
| 4557 | + "data-encoding", | |
| 4558 | + "http", | |
| 4559 | + "httparse", | |
| 4560 | + "log", | |
| 4561 | + "rand 0.9.5", | |
| 4562 | + "sha1 0.10.7", | |
| 4563 | + "thiserror", | |
| 4564 | +] | |
| 4565 | + | |
| 4566 | +[[package]] | |
| 4567 | +name = "typed-arena" | |
| 4568 | +version = "2.0.2" | |
| 4569 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4570 | +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" | |
| 4571 | + | |
| 4572 | +[[package]] | |
| 4573 | +name = "typenum" | |
| 4574 | +version = "1.20.1" | |
| 4575 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4576 | +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" | |
| 4577 | + | |
| 4578 | +[[package]] | |
| 4579 | +name = "unicase" | |
| 4580 | +version = "2.10.0" | |
| 4581 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4582 | +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" | |
| 4583 | + | |
| 4584 | +[[package]] | |
| 4585 | +name = "unicode-bidi" | |
| 4586 | +version = "0.3.18" | |
| 4587 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4588 | +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" | |
| 4589 | + | |
| 4590 | +[[package]] | |
| 4591 | +name = "unicode-ident" | |
| 4592 | +version = "1.0.26" | |
| 4593 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4594 | +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" | |
| 4595 | + | |
| 4596 | +[[package]] | |
| 4597 | +name = "unicode-normalization" | |
| 4598 | +version = "0.1.25" | |
| 4599 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4600 | +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" | |
| 4601 | +dependencies = [ | |
| 4602 | + "tinyvec", | |
| 4603 | +] | |
| 4604 | + | |
| 4605 | +[[package]] | |
| 4606 | +name = "unicode-properties" | |
| 4607 | +version = "0.1.4" | |
| 4608 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4609 | +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" | |
| 4610 | + | |
| 4611 | +[[package]] | |
| 4612 | +name = "universal-hash" | |
| 4613 | +version = "0.6.1" | |
| 4614 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4615 | +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" | |
| 4616 | +dependencies = [ | |
| 4617 | + "crypto-common 0.2.2", | |
| 4618 | + "ctutils", | |
| 4619 | +] | |
| 4620 | + | |
| 4621 | +[[package]] | |
| 4622 | +name = "untrusted" | |
| 4623 | +version = "0.7.1" | |
| 4624 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4625 | +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" | |
| 4626 | + | |
| 4627 | +[[package]] | |
| 4628 | +name = "untrusted" | |
| 4629 | +version = "0.9.0" | |
| 4630 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4631 | +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | |
| 4632 | + | |
| 4633 | +[[package]] | |
| 4634 | +name = "url" | |
| 4635 | +version = "2.5.8" | |
| 4636 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4637 | +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" | |
| 4638 | +dependencies = [ | |
| 4639 | + "form_urlencoded", | |
| 4640 | + "idna", | |
| 4641 | + "percent-encoding", | |
| 4642 | + "serde", | |
| 4643 | +] | |
| 4644 | + | |
| 4645 | +[[package]] | |
| 4646 | +name = "utf8_iter" | |
| 4647 | +version = "1.0.4" | |
| 4648 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4649 | +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" | |
| 4650 | + | |
| 4651 | +[[package]] | |
| 4652 | +name = "utf8parse" | |
| 4653 | +version = "0.2.2" | |
| 4654 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4655 | +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | |
| 4656 | + | |
| 4657 | +[[package]] | |
| 4658 | +name = "uuid" | |
| 4659 | +version = "1.27.0" | |
| 4660 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4661 | +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" | |
| 4662 | +dependencies = [ | |
| 4663 | + "getrandom 0.4.3", | |
| 4664 | + "js-sys", | |
| 4665 | + "serde_core", | |
| 4666 | + "wasm-bindgen", | |
| 4667 | +] | |
| 4668 | + | |
| 4669 | +[[package]] | |
| 4670 | +name = "valuable" | |
| 4671 | +version = "0.1.1" | |
| 4672 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4673 | +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" | |
| 4674 | + | |
| 4675 | +[[package]] | |
| 4676 | +name = "vcpkg" | |
| 4677 | +version = "0.2.15" | |
| 4678 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4679 | +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | |
| 4680 | + | |
| 4681 | +[[package]] | |
| 4682 | +name = "version_check" | |
| 4683 | +version = "0.9.5" | |
| 4684 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4685 | +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | |
| 4686 | + | |
| 4687 | +[[package]] | |
| 4688 | +name = "walkdir" | |
| 4689 | +version = "2.5.0" | |
| 4690 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4691 | +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" | |
| 4692 | +dependencies = [ | |
| 4693 | + "same-file", | |
| 4694 | + "winapi-util", | |
| 4695 | +] | |
| 4696 | + | |
| 4697 | +[[package]] | |
| 4698 | +name = "want" | |
| 4699 | +version = "0.3.2" | |
| 4700 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4701 | +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" | |
| 4702 | +dependencies = [ | |
| 4703 | + "try-lock", | |
| 4704 | +] | |
| 4705 | + | |
| 4706 | +[[package]] | |
| 4707 | +name = "wasi" | |
| 4708 | +version = "0.11.1+wasi-snapshot-preview1" | |
| 4709 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4710 | +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" | |
| 4711 | + | |
| 4712 | +[[package]] | |
| 4713 | +name = "wasip2" | |
| 4714 | +version = "1.0.4+wasi-0.2.12" | |
| 4715 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4716 | +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" | |
| 4717 | +dependencies = [ | |
| 4718 | + "wit-bindgen", | |
| 4719 | +] | |
| 4720 | + | |
| 4721 | +[[package]] | |
| 4722 | +name = "wasm-bindgen" | |
| 4723 | +version = "0.2.129" | |
| 4724 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4725 | +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" | |
| 4726 | +dependencies = [ | |
| 4727 | + "cfg-if", | |
| 4728 | + "once_cell", | |
| 4729 | + "rustversion", | |
| 4730 | + "wasm-bindgen-macro", | |
| 4731 | + "wasm-bindgen-shared", | |
| 4732 | +] | |
| 4733 | + | |
| 4734 | +[[package]] | |
| 4735 | +name = "wasm-bindgen-futures" | |
| 4736 | +version = "0.4.79" | |
| 4737 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4738 | +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" | |
| 4739 | +dependencies = [ | |
| 4740 | + "js-sys", | |
| 4741 | + "tokio", | |
| 4742 | + "wasm-bindgen", | |
| 4743 | +] | |
| 4744 | + | |
| 4745 | +[[package]] | |
| 4746 | +name = "wasm-bindgen-macro" | |
| 4747 | +version = "0.2.129" | |
| 4748 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4749 | +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" | |
| 4750 | +dependencies = [ | |
| 4751 | + "quote", | |
| 4752 | + "wasm-bindgen-macro-support", | |
| 4753 | +] | |
| 4754 | + | |
| 4755 | +[[package]] | |
| 4756 | +name = "wasm-bindgen-macro-support" | |
| 4757 | +version = "0.2.129" | |
| 4758 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4759 | +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" | |
| 4760 | +dependencies = [ | |
| 4761 | + "bumpalo", | |
| 4762 | + "proc-macro2", | |
| 4763 | + "quote", | |
| 4764 | + "syn 3.0.6", | |
| 4765 | + "wasm-bindgen-shared", | |
| 4766 | +] | |
| 4767 | + | |
| 4768 | +[[package]] | |
| 4769 | +name = "wasm-bindgen-shared" | |
| 4770 | +version = "0.2.129" | |
| 4771 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4772 | +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" | |
| 4773 | +dependencies = [ | |
| 4774 | + "unicode-ident", | |
| 4775 | +] | |
| 4776 | + | |
| 4777 | +[[package]] | |
| 4778 | +name = "wasm-streams" | |
| 4779 | +version = "0.5.0" | |
| 4780 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4781 | +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" | |
| 4782 | +dependencies = [ | |
| 4783 | + "futures-util", | |
| 4784 | + "js-sys", | |
| 4785 | + "wasm-bindgen", | |
| 4786 | + "wasm-bindgen-futures", | |
| 4787 | + "web-sys", | |
| 4788 | +] | |
| 4789 | + | |
| 4790 | +[[package]] | |
| 4791 | +name = "web-sys" | |
| 4792 | +version = "0.3.106" | |
| 4793 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4794 | +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" | |
| 4795 | +dependencies = [ | |
| 4796 | + "js-sys", | |
| 4797 | + "wasm-bindgen", | |
| 4798 | +] | |
| 4799 | + | |
| 4800 | +[[package]] | |
| 4801 | +name = "web-time" | |
| 4802 | +version = "1.1.0" | |
| 4803 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4804 | +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | |
| 4805 | +dependencies = [ | |
| 4806 | + "js-sys", | |
| 4807 | + "wasm-bindgen", | |
| 4808 | +] | |
| 4809 | + | |
| 4810 | +[[package]] | |
| 4811 | +name = "webpki-root-certs" | |
| 4812 | +version = "1.0.9" | |
| 4813 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4814 | +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" | |
| 4815 | +dependencies = [ | |
| 4816 | + "rustls-pki-types", | |
| 4817 | +] | |
| 4818 | + | |
| 4819 | +[[package]] | |
| 4820 | +name = "webpki-roots" | |
| 4821 | +version = "1.0.9" | |
| 4822 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4823 | +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" | |
| 4824 | +dependencies = [ | |
| 4825 | + "rustls-pki-types", | |
| 4826 | +] | |
| 4827 | + | |
| 4828 | +[[package]] | |
| 4829 | +name = "whoami" | |
| 4830 | +version = "2.1.3" | |
| 4831 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4832 | +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" | |
| 4833 | + | |
| 4834 | +[[package]] | |
| 4835 | +name = "winapi-util" | |
| 4836 | +version = "0.1.11" | |
| 4837 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4838 | +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 4839 | +dependencies = [ | |
| 4840 | + "windows-sys 0.61.2", | |
| 4841 | +] | |
| 4842 | + | |
| 4843 | +[[package]] | |
| 4844 | +name = "windows" | |
| 4845 | +version = "0.62.2" | |
| 4846 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4847 | +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" | |
| 4848 | +dependencies = [ | |
| 4849 | + "windows-collections", | |
| 4850 | + "windows-core", | |
| 4851 | + "windows-future", | |
| 4852 | + "windows-numerics", | |
| 4853 | +] | |
| 4854 | + | |
| 4855 | +[[package]] | |
| 4856 | +name = "windows-collections" | |
| 4857 | +version = "0.3.2" | |
| 4858 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4859 | +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" | |
| 4860 | +dependencies = [ | |
| 4861 | + "windows-core", | |
| 4862 | +] | |
| 4863 | + | |
| 4864 | +[[package]] | |
| 4865 | +name = "windows-core" | |
| 4866 | +version = "0.62.2" | |
| 4867 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4868 | +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" | |
| 4869 | +dependencies = [ | |
| 4870 | + "windows-implement", | |
| 4871 | + "windows-interface", | |
| 4872 | + "windows-link", | |
| 4873 | + "windows-result", | |
| 4874 | + "windows-strings", | |
| 4875 | +] | |
| 4876 | + | |
| 4877 | +[[package]] | |
| 4878 | +name = "windows-future" | |
| 4879 | +version = "0.3.2" | |
| 4880 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4881 | +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" | |
| 4882 | +dependencies = [ | |
| 4883 | + "windows-core", | |
| 4884 | + "windows-link", | |
| 4885 | + "windows-threading", | |
| 4886 | +] | |
| 4887 | + | |
| 4888 | +[[package]] | |
| 4889 | +name = "windows-implement" | |
| 4890 | +version = "0.60.2" | |
| 4891 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4892 | +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" | |
| 4893 | +dependencies = [ | |
| 4894 | + "proc-macro2", | |
| 4895 | + "quote", | |
| 4896 | + "syn 2.0.119", | |
| 4897 | +] | |
| 4898 | + | |
| 4899 | +[[package]] | |
| 4900 | +name = "windows-interface" | |
| 4901 | +version = "0.59.3" | |
| 4902 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4903 | +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" | |
| 4904 | +dependencies = [ | |
| 4905 | + "proc-macro2", | |
| 4906 | + "quote", | |
| 4907 | + "syn 2.0.119", | |
| 4908 | +] | |
| 4909 | + | |
| 4910 | +[[package]] | |
| 4911 | +name = "windows-link" | |
| 4912 | +version = "0.2.1" | |
| 4913 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4914 | +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | |
| 4915 | + | |
| 4916 | +[[package]] | |
| 4917 | +name = "windows-numerics" | |
| 4918 | +version = "0.3.1" | |
| 4919 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4920 | +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" | |
| 4921 | +dependencies = [ | |
| 4922 | + "windows-core", | |
| 4923 | + "windows-link", | |
| 4924 | +] | |
| 4925 | + | |
| 4926 | +[[package]] | |
| 4927 | +name = "windows-result" | |
| 4928 | +version = "0.4.1" | |
| 4929 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4930 | +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" | |
| 4931 | +dependencies = [ | |
| 4932 | + "windows-link", | |
| 4933 | +] | |
| 4934 | + | |
| 4935 | +[[package]] | |
| 4936 | +name = "windows-strings" | |
| 4937 | +version = "0.5.1" | |
| 4938 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4939 | +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" | |
| 4940 | +dependencies = [ | |
| 4941 | + "windows-link", | |
| 4942 | +] | |
| 4943 | + | |
| 4944 | +[[package]] | |
| 4945 | +name = "windows-sys" | |
| 4946 | +version = "0.52.0" | |
| 4947 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4948 | +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" | |
| 4949 | +dependencies = [ | |
| 4950 | + "windows-targets 0.52.6", | |
| 4951 | +] | |
| 4952 | + | |
| 4953 | +[[package]] | |
| 4954 | +name = "windows-sys" | |
| 4955 | +version = "0.60.2" | |
| 4956 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4957 | +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | |
| 4958 | +dependencies = [ | |
| 4959 | + "windows-targets 0.53.5", | |
| 4960 | +] | |
| 4961 | + | |
| 4962 | +[[package]] | |
| 4963 | +name = "windows-sys" | |
| 4964 | +version = "0.61.2" | |
| 4965 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4966 | +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" | |
| 4967 | +dependencies = [ | |
| 4968 | + "windows-link", | |
| 4969 | +] | |
| 4970 | + | |
| 4971 | +[[package]] | |
| 4972 | +name = "windows-targets" | |
| 4973 | +version = "0.52.6" | |
| 4974 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4975 | +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" | |
| 4976 | +dependencies = [ | |
| 4977 | + "windows_aarch64_gnullvm 0.52.6", | |
| 4978 | + "windows_aarch64_msvc 0.52.6", | |
| 4979 | + "windows_i686_gnu 0.52.6", | |
| 4980 | + "windows_i686_gnullvm 0.52.6", | |
| 4981 | + "windows_i686_msvc 0.52.6", | |
| 4982 | + "windows_x86_64_gnu 0.52.6", | |
| 4983 | + "windows_x86_64_gnullvm 0.52.6", | |
| 4984 | + "windows_x86_64_msvc 0.52.6", | |
| 4985 | +] | |
| 4986 | + | |
| 4987 | +[[package]] | |
| 4988 | +name = "windows-targets" | |
| 4989 | +version = "0.53.5" | |
| 4990 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4991 | +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" | |
| 4992 | +dependencies = [ | |
| 4993 | + "windows-link", | |
| 4994 | + "windows_aarch64_gnullvm 0.53.1", | |
| 4995 | + "windows_aarch64_msvc 0.53.1", | |
| 4996 | + "windows_i686_gnu 0.53.1", | |
| 4997 | + "windows_i686_gnullvm 0.53.1", | |
| 4998 | + "windows_i686_msvc 0.53.1", | |
| 4999 | + "windows_x86_64_gnu 0.53.1", | |
| 5000 | + "windows_x86_64_gnullvm 0.53.1", | |
| 5001 | + "windows_x86_64_msvc 0.53.1", | |
| 5002 | +] | |
| 5003 | + | |
| 5004 | +[[package]] | |
| 5005 | +name = "windows-threading" | |
| 5006 | +version = "0.2.1" | |
| 5007 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5008 | +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" | |
| 5009 | +dependencies = [ | |
| 5010 | + "windows-link", | |
| 5011 | +] | |
| 5012 | + | |
| 5013 | +[[package]] | |
| 5014 | +name = "windows_aarch64_gnullvm" | |
| 5015 | +version = "0.52.6" | |
| 5016 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5017 | +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" | |
| 5018 | + | |
| 5019 | +[[package]] | |
| 5020 | +name = "windows_aarch64_gnullvm" | |
| 5021 | +version = "0.53.1" | |
| 5022 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5023 | +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" | |
| 5024 | + | |
| 5025 | +[[package]] | |
| 5026 | +name = "windows_aarch64_msvc" | |
| 5027 | +version = "0.52.6" | |
| 5028 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5029 | +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" | |
| 5030 | + | |
| 5031 | +[[package]] | |
| 5032 | +name = "windows_aarch64_msvc" | |
| 5033 | +version = "0.53.1" | |
| 5034 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5035 | +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" | |
| 5036 | + | |
| 5037 | +[[package]] | |
| 5038 | +name = "windows_i686_gnu" | |
| 5039 | +version = "0.52.6" | |
| 5040 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5041 | +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" | |
| 5042 | + | |
| 5043 | +[[package]] | |
| 5044 | +name = "windows_i686_gnu" | |
| 5045 | +version = "0.53.1" | |
| 5046 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5047 | +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" | |
| 5048 | + | |
| 5049 | +[[package]] | |
| 5050 | +name = "windows_i686_gnullvm" | |
| 5051 | +version = "0.52.6" | |
| 5052 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5053 | +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" | |
| 5054 | + | |
| 5055 | +[[package]] | |
| 5056 | +name = "windows_i686_gnullvm" | |
| 5057 | +version = "0.53.1" | |
| 5058 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5059 | +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" | |
| 5060 | + | |
| 5061 | +[[package]] | |
| 5062 | +name = "windows_i686_msvc" | |
| 5063 | +version = "0.52.6" | |
| 5064 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5065 | +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" | |
| 5066 | + | |
| 5067 | +[[package]] | |
| 5068 | +name = "windows_i686_msvc" | |
| 5069 | +version = "0.53.1" | |
| 5070 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5071 | +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" | |
| 5072 | + | |
| 5073 | +[[package]] | |
| 5074 | +name = "windows_x86_64_gnu" | |
| 5075 | +version = "0.52.6" | |
| 5076 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5077 | +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" | |
| 5078 | + | |
| 5079 | +[[package]] | |
| 5080 | +name = "windows_x86_64_gnu" | |
| 5081 | +version = "0.53.1" | |
| 5082 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5083 | +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" | |
| 5084 | + | |
| 5085 | +[[package]] | |
| 5086 | +name = "windows_x86_64_gnullvm" | |
| 5087 | +version = "0.52.6" | |
| 5088 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5089 | +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" | |
| 5090 | + | |
| 5091 | +[[package]] | |
| 5092 | +name = "windows_x86_64_gnullvm" | |
| 5093 | +version = "0.53.1" | |
| 5094 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5095 | +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" | |
| 5096 | + | |
| 5097 | +[[package]] | |
| 5098 | +name = "windows_x86_64_msvc" | |
| 5099 | +version = "0.52.6" | |
| 5100 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5101 | +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |
| 5102 | + | |
| 5103 | +[[package]] | |
| 5104 | +name = "windows_x86_64_msvc" | |
| 5105 | +version = "0.53.1" | |
| 5106 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5107 | +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" | |
| 5108 | + | |
| 5109 | +[[package]] | |
| 5110 | +name = "winnow" | |
| 5111 | +version = "1.0.4" | |
| 5112 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5113 | +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" | |
| 5114 | + | |
| 5115 | +[[package]] | |
| 5116 | +name = "wit-bindgen" | |
| 5117 | +version = "0.57.1" | |
| 5118 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5119 | +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" | |
| 5120 | + | |
| 5121 | +[[package]] | |
| 5122 | +name = "wnaf" | |
| 5123 | +version = "0.14.1" | |
| 5124 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5125 | +checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" | |
| 5126 | +dependencies = [ | |
| 5127 | + "ff", | |
| 5128 | + "group", | |
| 5129 | + "hybrid-array", | |
| 5130 | + "primefield", | |
| 5131 | +] | |
| 5132 | + | |
| 5133 | +[[package]] | |
| 5134 | +name = "writeable" | |
| 5135 | +version = "0.6.4" | |
| 5136 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5137 | +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" | |
| 5138 | + | |
| 5139 | +[[package]] | |
| 5140 | +name = "xdg" | |
| 5141 | +version = "3.0.0" | |
| 5142 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5143 | +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" | |
| 5144 | + | |
| 5145 | +[[package]] | |
| 5146 | +name = "xmlparser" | |
| 5147 | +version = "0.13.6" | |
| 5148 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5149 | +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" | |
| 5150 | + | |
| 5151 | +[[package]] | |
| 5152 | +name = "yaml-rust" | |
| 5153 | +version = "0.4.5" | |
| 5154 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5155 | +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" | |
| 5156 | +dependencies = [ | |
| 5157 | + "linked-hash-map", | |
| 5158 | +] | |
| 5159 | + | |
| 5160 | +[[package]] | |
| 5161 | +name = "yoke" | |
| 5162 | +version = "0.8.3" | |
| 5163 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5164 | +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" | |
| 5165 | +dependencies = [ | |
| 5166 | + "stable_deref_trait", | |
| 5167 | + "yoke-derive", | |
| 5168 | + "zerofrom", | |
| 5169 | +] | |
| 5170 | + | |
| 5171 | +[[package]] | |
| 5172 | +name = "yoke-derive" | |
| 5173 | +version = "0.8.4" | |
| 5174 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5175 | +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" | |
| 5176 | +dependencies = [ | |
| 5177 | + "proc-macro2", | |
| 5178 | + "quote", | |
| 5179 | + "syn 3.0.6", | |
| 5180 | + "synstructure", | |
| 5181 | +] | |
| 5182 | + | |
| 5183 | +[[package]] | |
| 5184 | +name = "zerocopy" | |
| 5185 | +version = "0.8.62" | |
| 5186 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5187 | +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" | |
| 5188 | +dependencies = [ | |
| 5189 | + "zerocopy-derive", | |
| 5190 | +] | |
| 5191 | + | |
| 5192 | +[[package]] | |
| 5193 | +name = "zerocopy-derive" | |
| 5194 | +version = "0.8.62" | |
| 5195 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5196 | +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" | |
| 5197 | +dependencies = [ | |
| 5198 | + "proc-macro2", | |
| 5199 | + "quote", | |
| 5200 | + "syn 2.0.119", | |
| 5201 | +] | |
| 5202 | + | |
| 5203 | +[[package]] | |
| 5204 | +name = "zerofrom" | |
| 5205 | +version = "0.1.8" | |
| 5206 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5207 | +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" | |
| 5208 | +dependencies = [ | |
| 5209 | + "zerofrom-derive", | |
| 5210 | +] | |
| 5211 | + | |
| 5212 | +[[package]] | |
| 5213 | +name = "zerofrom-derive" | |
| 5214 | +version = "0.1.8" | |
| 5215 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5216 | +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" | |
| 5217 | +dependencies = [ | |
| 5218 | + "proc-macro2", | |
| 5219 | + "quote", | |
| 5220 | + "syn 3.0.6", | |
| 5221 | + "synstructure", | |
| 5222 | +] | |
| 5223 | + | |
| 5224 | +[[package]] | |
| 5225 | +name = "zeroize" | |
| 5226 | +version = "1.9.1" | |
| 5227 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5228 | +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" | |
| 5229 | + | |
| 5230 | +[[package]] | |
| 5231 | +name = "zerotrie" | |
| 5232 | +version = "0.2.5" | |
| 5233 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5234 | +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" | |
| 5235 | +dependencies = [ | |
| 5236 | + "displaydoc", | |
| 5237 | + "yoke", | |
| 5238 | + "zerofrom", | |
| 5239 | +] | |
| 5240 | + | |
| 5241 | +[[package]] | |
| 5242 | +name = "zerovec" | |
| 5243 | +version = "0.11.8" | |
| 5244 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5245 | +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" | |
| 5246 | +dependencies = [ | |
| 5247 | + "yoke", | |
| 5248 | + "zerofrom", | |
| 5249 | + "zerovec-derive", | |
| 5250 | +] | |
| 5251 | + | |
| 5252 | +[[package]] | |
| 5253 | +name = "zerovec-derive" | |
| 5254 | +version = "0.11.6" | |
| 5255 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5256 | +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" | |
| 5257 | +dependencies = [ | |
| 5258 | + "proc-macro2", | |
| 5259 | + "quote", | |
| 5260 | + "syn 3.0.6", | |
| 5261 | +] | |
| 5262 | + | |
| 5263 | +[[package]] | |
| 5264 | +name = "zlib-rs" | |
| 5265 | +version = "0.6.8" | |
| 5266 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5267 | +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" | |
| 5268 | + | |
| 5269 | +[[package]] | |
| 5270 | +name = "zmij" | |
| 5271 | +version = "1.0.23" | |
| 5272 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5273 | +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" |
+13-0Cargo.toml
| @@ -0,0 +1,13 @@ | ||
| 1 | +[workspace] | |
| 2 | +resolver = "3" | |
| 3 | +members = ["backend", "cli", "shared"] | |
| 4 | + | |
| 5 | +[profile.release] | |
| 6 | +lto = "thin" | |
| 7 | +codegen-units = 4 | |
| 8 | +strip = true | |
| 9 | + | |
| 10 | +# Dependencies compile with optimizations even in debug builds: argon2, | |
| 11 | +# sha256 over image layers and syntax highlighting are unusably slow otherwise. | |
| 12 | +[profile.dev.package."*"] | |
| 13 | +opt-level = 2 |
+53-0backend/Cargo.toml
| @@ -0,0 +1,53 @@ | ||
| 1 | +[package] | |
| 2 | +name = "irongit" | |
| 3 | +version = "0.1.0" | |
| 4 | +edition = "2024" | |
| 5 | + | |
| 6 | +[features] | |
| 7 | +default = ["hot-reload"] | |
| 8 | +# src/dev.rs: the bun rebuild, the file watcher and /dev-ws. Build a | |
| 9 | +# production binary with --no-default-features to leave all of it out. | |
| 10 | +hot-reload = ["dep:notify"] | |
| 11 | + | |
| 12 | +[dependencies] | |
| 13 | +notify = { version = "8", optional = true } | |
| 14 | +irongit-shared = { path = "../shared" } | |
| 15 | +anyhow = "1.0.104" | |
| 16 | +axum = { version = "0.8.9", features = ["ws", "multipart", "macros"] } | |
| 17 | +tokio = { version = "1.53.2", features = ["full"] } | |
| 18 | +tower-http = { version = "0.7.1", features = ["trace", "compression-gzip", "limit", "set-header", "timeout"] } | |
| 19 | +sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-webpki", "postgres", "macros", "migrate", "uuid", "chrono", "json"] } | |
| 20 | +uuid = { version = "1.27.0", features = ["v4", "v7", "serde"] } | |
| 21 | +chrono = { version = "0.4.45", features = ["serde"] } | |
| 22 | +serde = { version = "1.0.229", features = ["derive"] } | |
| 23 | +serde_json = "1.0.151" | |
| 24 | +mime_guess = "2.0.5" | |
| 25 | +rust-embed = "8.13.0" | |
| 26 | +tracing = "0.1.44" | |
| 27 | +tracing-appender = "0.2.5" | |
| 28 | +dotenvy = "0.15.7" | |
| 29 | +argon2 = "0.6.0" | |
| 30 | +rand = "0.10.3" | |
| 31 | +sha2 = "0.11.0" | |
| 32 | +hex = "0.4.3" | |
| 33 | +hmac = "0.13.0" | |
| 34 | +base64 = "0.23.1" | |
| 35 | +bytes = "1.12.1" | |
| 36 | +futures = "0.3.34" | |
| 37 | +tokio-util = { version = "0.7.19", features = ["io", "io-util"] } | |
| 38 | +url = "2.5.8" | |
| 39 | +maud = { version = "0.27.0", features = ["axum"] } | |
| 40 | +regex = "1.13.1" | |
| 41 | +once_cell = "1.21.4" | |
| 42 | +time = "0.3.55" | |
| 43 | +tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json"] } | |
| 44 | +rusty-s3 = "0.10.2" | |
| 45 | +reqwest = { version = "0.13.5", default-features = false, features = ["rustls", "stream", "json", "http2"] } | |
| 46 | +russh = "0.64.1" | |
| 47 | +comrak = "0.56.0" | |
| 48 | +syntect = "5.3.0" | |
| 49 | +flate2 = "1.1.10" | |
| 50 | +clap = { version = "4.6.7", features = ["derive", "env"] } | |
| 51 | +lettre = { version = "0.11.23", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "hostname"] } | |
| 52 | +axum-extra = { version = "0.12.6", features = ["cookie", "typed-header"] } | |
| 53 | +async-compression = { version = "0.4.50", features = ["tokio", "gzip"] } |
+7-0backend/build.rs
| @@ -0,0 +1,7 @@ | ||
| 1 | +// rust-embed reads ../frontend/dist while the Embed derive expands, and | |
| 2 | +// cargo has no idea those files exist. Without this, a release build happily | |
| 3 | +// embeds a stale frontend (including the dev reload script) because nothing | |
| 4 | +// in src/ changed. | |
| 5 | +fn main() { | |
| 6 | + println!("cargo:rerun-if-changed=../frontend/dist"); | |
| 7 | +} |
+288-0backend/migrations/0001_init.sql
| @@ -0,0 +1,288 @@ | ||
| 1 | +-- irongit schema. Postgres holds identity, permissions and metadata; git | |
| 2 | +-- objects live on the server's disk and large blobs (LFS, image layers, CLI | |
| 3 | +-- releases, avatars, backups) live in R2. | |
| 4 | + | |
| 5 | +create extension if not exists citext; | |
| 6 | +create extension if not exists pg_trgm; | |
| 7 | + | |
| 8 | +-- Users and organizations share one namespace, so /{name} is unambiguous. | |
| 9 | +create table accounts ( | |
| 10 | + id bigserial primary key, | |
| 11 | + kind text not null check (kind in ('user', 'org')), | |
| 12 | + name citext not null unique, | |
| 13 | + display_name text not null default '', | |
| 14 | + bio text not null default '', | |
| 15 | + location text not null default '', | |
| 16 | + website text not null default '', | |
| 17 | + -- R2 key of an uploaded avatar; null means the generated identicon. | |
| 18 | + avatar_key text, | |
| 19 | + -- Per-account override of the default storage quota, in bytes. | |
| 20 | + quota_bytes bigint, | |
| 21 | + created_at timestamptz not null default now(), | |
| 22 | + updated_at timestamptz not null default now() | |
| 23 | +); | |
| 24 | +create index accounts_name_trgm on accounts using gin (name gin_trgm_ops); | |
| 25 | + | |
| 26 | +create table users ( | |
| 27 | + account_id bigint primary key references accounts (id) on delete cascade, | |
| 28 | + -- Null for accounts that only sign in with Google. | |
| 29 | + password_hash text, | |
| 30 | + is_admin boolean not null default false, | |
| 31 | + suspended_at timestamptz, | |
| 32 | + -- Whether other people see counts from private repos on the heatmap. | |
| 33 | + show_private_contributions boolean not null default false, | |
| 34 | + last_login_at timestamptz | |
| 35 | +); | |
| 36 | + | |
| 37 | +create table emails ( | |
| 38 | + id bigserial primary key, | |
| 39 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 40 | + email citext not null unique, | |
| 41 | + is_primary boolean not null default false, | |
| 42 | + verified_at timestamptz, | |
| 43 | + verify_token_hash text, | |
| 44 | + verify_sent_at timestamptz, | |
| 45 | + created_at timestamptz not null default now() | |
| 46 | +); | |
| 47 | +create unique index emails_one_primary on emails (user_id) where is_primary; | |
| 48 | +create index emails_user on emails (user_id); | |
| 49 | + | |
| 50 | +-- External sign-in (Google). `subject` is the provider's stable user id. | |
| 51 | +create table oauth_identities ( | |
| 52 | + provider text not null check (provider in ('google')), | |
| 53 | + subject text not null, | |
| 54 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 55 | + email citext, | |
| 56 | + created_at timestamptz not null default now(), | |
| 57 | + last_used_at timestamptz, | |
| 58 | + primary key (provider, subject), | |
| 59 | + unique (provider, user_id) | |
| 60 | +); | |
| 61 | + | |
| 62 | +create table org_members ( | |
| 63 | + org_id bigint not null references accounts (id) on delete cascade, | |
| 64 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 65 | + role text not null check (role in ('owner', 'member')), | |
| 66 | + created_at timestamptz not null default now(), | |
| 67 | + primary key (org_id, user_id) | |
| 68 | +); | |
| 69 | +create index org_members_user on org_members (user_id); | |
| 70 | + | |
| 71 | +create table sessions ( | |
| 72 | + id_hash text primary key, | |
| 73 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 74 | + ip text, | |
| 75 | + user_agent text, | |
| 76 | + created_at timestamptz not null default now(), | |
| 77 | + last_seen_at timestamptz not null default now(), | |
| 78 | + expires_at timestamptz not null | |
| 79 | +); | |
| 80 | +create index sessions_user on sessions (user_id); | |
| 81 | + | |
| 82 | +-- Personal access tokens: the only credential git, docker and the CLI accept. | |
| 83 | +create table access_tokens ( | |
| 84 | + id bigserial primary key, | |
| 85 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 86 | + name text not null, | |
| 87 | + token_hash text not null unique, | |
| 88 | + token_prefix text not null, | |
| 89 | + scopes text[] not null default '{repo,packages,user}', | |
| 90 | + last_used_at timestamptz, | |
| 91 | + expires_at timestamptz, | |
| 92 | + created_at timestamptz not null default now() | |
| 93 | +); | |
| 94 | +create index access_tokens_user on access_tokens (user_id); | |
| 95 | + | |
| 96 | +create table ssh_keys ( | |
| 97 | + id bigserial primary key, | |
| 98 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 99 | + title text not null, | |
| 100 | + public_key text not null, | |
| 101 | + fingerprint text not null unique, | |
| 102 | + last_used_at timestamptz, | |
| 103 | + created_at timestamptz not null default now() | |
| 104 | +); | |
| 105 | +create index ssh_keys_user on ssh_keys (user_id); | |
| 106 | + | |
| 107 | +-- `ig login`: the CLI holds device_code, the person types user_code in a browser. | |
| 108 | +create table device_codes ( | |
| 109 | + device_code_hash text primary key, | |
| 110 | + user_code text not null unique, | |
| 111 | + client_name text not null default '', | |
| 112 | + user_id bigint references users (account_id) on delete cascade, | |
| 113 | + approved_at timestamptz, | |
| 114 | + denied_at timestamptz, | |
| 115 | + consumed_at timestamptz, | |
| 116 | + expires_at timestamptz not null, | |
| 117 | + created_at timestamptz not null default now() | |
| 118 | +); | |
| 119 | + | |
| 120 | +create table repos ( | |
| 121 | + id bigserial primary key, | |
| 122 | + owner_id bigint not null references accounts (id) on delete cascade, | |
| 123 | + name citext not null, | |
| 124 | + description text not null default '', | |
| 125 | + visibility text not null check (visibility in ('public', 'private')), | |
| 126 | + default_branch text not null default 'main', | |
| 127 | + size_bytes bigint not null default 0, | |
| 128 | + is_empty boolean not null default true, | |
| 129 | + archived boolean not null default false, | |
| 130 | + created_at timestamptz not null default now(), | |
| 131 | + updated_at timestamptz not null default now(), | |
| 132 | + pushed_at timestamptz, | |
| 133 | + unique (owner_id, name) | |
| 134 | +); | |
| 135 | +create index repos_name_trgm on repos using gin (name gin_trgm_ops); | |
| 136 | +create index repos_public_pushed on repos (pushed_at desc nulls last) where visibility = 'public'; | |
| 137 | + | |
| 138 | +create table repo_collaborators ( | |
| 139 | + repo_id bigint not null references repos (id) on delete cascade, | |
| 140 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 141 | + permission text not null check (permission in ('read', 'write', 'admin')), | |
| 142 | + created_at timestamptz not null default now(), | |
| 143 | + primary key (repo_id, user_id) | |
| 144 | +); | |
| 145 | +create index repo_collaborators_user on repo_collaborators (user_id); | |
| 146 | + | |
| 147 | +create table push_events ( | |
| 148 | + id bigserial primary key, | |
| 149 | + repo_id bigint not null references repos (id) on delete cascade, | |
| 150 | + pusher_id bigint references users (account_id) on delete set null, | |
| 151 | + ref_name text not null, | |
| 152 | + before_sha text not null, | |
| 153 | + after_sha text not null, | |
| 154 | + commit_count integer not null, | |
| 155 | + head_message text not null default '', | |
| 156 | + via text not null check (via in ('http', 'ssh', 'web')), | |
| 157 | + created_at timestamptz not null default now() | |
| 158 | +); | |
| 159 | +create index push_events_pusher on push_events (pusher_id, created_at desc); | |
| 160 | +create index push_events_repo on push_events (repo_id, created_at desc); | |
| 161 | + | |
| 162 | +-- One row per commit that landed on a repo's default branch and whose author | |
| 163 | +-- email is verified by a user. Idempotent, so force pushes never double count. | |
| 164 | +create table contribution_commits ( | |
| 165 | + repo_id bigint not null references repos (id) on delete cascade, | |
| 166 | + sha text not null, | |
| 167 | + user_id bigint not null references users (account_id) on delete cascade, | |
| 168 | + day date not null, | |
| 169 | + primary key (repo_id, sha) | |
| 170 | +); | |
| 171 | +create index contribution_commits_user_day on contribution_commits (user_id, day); | |
| 172 | + | |
| 173 | +-- Git LFS. Objects are stored once in R2 by oid; repo links gate access. | |
| 174 | +create table lfs_objects ( | |
| 175 | + oid text primary key, | |
| 176 | + size bigint not null, | |
| 177 | + created_at timestamptz not null default now() | |
| 178 | +); | |
| 179 | + | |
| 180 | +create table repo_lfs_objects ( | |
| 181 | + repo_id bigint not null references repos (id) on delete cascade, | |
| 182 | + oid text not null references lfs_objects (oid) on delete cascade, | |
| 183 | + created_at timestamptz not null default now(), | |
| 184 | + primary key (repo_id, oid) | |
| 185 | +); | |
| 186 | +create index repo_lfs_objects_oid on repo_lfs_objects (oid); | |
| 187 | + | |
| 188 | +-- Container images. Visibility is independent of any linked repo. | |
| 189 | +create table packages ( | |
| 190 | + id bigserial primary key, | |
| 191 | + owner_id bigint not null references accounts (id) on delete cascade, | |
| 192 | + -- Image path below the owner, lowercase: "api" or "tools/builder". | |
| 193 | + name text not null, | |
| 194 | + visibility text not null default 'private' check (visibility in ('public', 'private')), | |
| 195 | + repo_id bigint references repos (id) on delete set null, | |
| 196 | + description text not null default '', | |
| 197 | + pull_count bigint not null default 0, | |
| 198 | + created_at timestamptz not null default now(), | |
| 199 | + updated_at timestamptz not null default now(), | |
| 200 | + unique (owner_id, name) | |
| 201 | +); | |
| 202 | + | |
| 203 | +-- Layers and configs, stored once in R2 under their digest. | |
| 204 | +create table blobs ( | |
| 205 | + digest text primary key, | |
| 206 | + size bigint not null, | |
| 207 | + created_at timestamptz not null default now() | |
| 208 | +); | |
| 209 | + | |
| 210 | +-- Which packages may see a blob. Every HEAD, GET and mount checks this, so a | |
| 211 | +-- digest from someone's private image is useless without access to it. | |
| 212 | +create table package_blobs ( | |
| 213 | + package_id bigint not null references packages (id) on delete cascade, | |
| 214 | + digest text not null references blobs (digest) on delete cascade, | |
| 215 | + created_at timestamptz not null default now(), | |
| 216 | + primary key (package_id, digest) | |
| 217 | +); | |
| 218 | +create index package_blobs_digest on package_blobs (digest); | |
| 219 | + | |
| 220 | +create table manifests ( | |
| 221 | + id bigserial primary key, | |
| 222 | + package_id bigint not null references packages (id) on delete cascade, | |
| 223 | + digest text not null, | |
| 224 | + media_type text not null, | |
| 225 | + content bytea not null, | |
| 226 | + -- Sum of the config and layer sizes, for display. | |
| 227 | + total_size bigint not null default 0, | |
| 228 | + created_at timestamptz not null default now(), | |
| 229 | + unique (package_id, digest) | |
| 230 | +); | |
| 231 | + | |
| 232 | +create table manifest_references ( | |
| 233 | + manifest_id bigint not null references manifests (id) on delete cascade, | |
| 234 | + digest text not null, | |
| 235 | + kind text not null check (kind in ('blob', 'manifest')), | |
| 236 | + primary key (manifest_id, digest) | |
| 237 | +); | |
| 238 | +create index manifest_references_digest on manifest_references (digest); | |
| 239 | + | |
| 240 | +create table tags ( | |
| 241 | + package_id bigint not null references packages (id) on delete cascade, | |
| 242 | + name text not null, | |
| 243 | + manifest_id bigint not null references manifests (id) on delete cascade, | |
| 244 | + updated_at timestamptz not null default now(), | |
| 245 | + primary key (package_id, name) | |
| 246 | +); | |
| 247 | +create index tags_manifest on tags (manifest_id); | |
| 248 | + | |
| 249 | +-- In-progress pushes, staged on local disk under DATA_DIR/uploads/{id}. | |
| 250 | +create table blob_uploads ( | |
| 251 | + id uuid primary key, | |
| 252 | + package_id bigint not null references packages (id) on delete cascade, | |
| 253 | + user_id bigint references users (account_id) on delete set null, | |
| 254 | + size bigint not null default 0, | |
| 255 | + created_at timestamptz not null default now(), | |
| 256 | + updated_at timestamptz not null default now() | |
| 257 | +); | |
| 258 | + | |
| 259 | +create table cli_releases ( | |
| 260 | + id bigserial primary key, | |
| 261 | + version text not null unique, | |
| 262 | + target text not null default 'x86_64-unknown-linux-musl', | |
| 263 | + r2_key text not null, | |
| 264 | + sha256 text not null, | |
| 265 | + size bigint not null, | |
| 266 | + created_at timestamptz not null default now() | |
| 267 | +); | |
| 268 | + | |
| 269 | +create table backup_runs ( | |
| 270 | + id bigserial primary key, | |
| 271 | + started_at timestamptz not null default now(), | |
| 272 | + finished_at timestamptz, | |
| 273 | + status text not null default 'running' check (status in ('running', 'ok', 'failed')), | |
| 274 | + repo_count integer not null default 0, | |
| 275 | + bytes bigint not null default 0, | |
| 276 | + error text | |
| 277 | +); | |
| 278 | + | |
| 279 | +create table audit_log ( | |
| 280 | + id bigserial primary key, | |
| 281 | + actor_id bigint references users (account_id) on delete set null, | |
| 282 | + action text not null, | |
| 283 | + target text not null default '', | |
| 284 | + meta jsonb not null default '{}', | |
| 285 | + ip text, | |
| 286 | + created_at timestamptz not null default now() | |
| 287 | +); | |
| 288 | +create index audit_log_created on audit_log (created_at desc); |
+39-0backend/src/analytics.rs
| @@ -0,0 +1,39 @@ | ||
| 1 | +//! Server-side events for the self-hosted Rybbit at a.hygo.ai. Browser | |
| 2 | +//! events are sent by the page script; these cover what has no browser: | |
| 3 | +//! git pushes, image pushes and pulls, CLI logins. Fire and forget. | |
| 4 | + | |
| 5 | +use serde_json::{Value, json}; | |
| 6 | + | |
| 7 | +use crate::state::AppState; | |
| 8 | + | |
| 9 | +pub fn track(state: &AppState, event: &str, user: Option<&str>, path: &str, properties: Value) { | |
| 10 | + let Some(rybbit) = state.config.rybbit.clone() else { | |
| 11 | + tracing::debug!(event, %properties, "analytics disabled"); | |
| 12 | + return; | |
| 13 | + }; | |
| 14 | + let http = state.http.clone(); | |
| 15 | + let hostname = state.config.public_url.host_str().unwrap_or("localhost").to_string(); | |
| 16 | + let body = json!({ | |
| 17 | + "type": "custom_event", | |
| 18 | + "site_id": rybbit.site_id, | |
| 19 | + "event_name": event, | |
| 20 | + "properties": properties.to_string(), | |
| 21 | + "hostname": hostname, | |
| 22 | + "pathname": path, | |
| 23 | + "user_id": user, | |
| 24 | + "user_agent": "irongit-server", | |
| 25 | + }); | |
| 26 | + let event = event.to_string(); | |
| 27 | + tokio::spawn(async move { | |
| 28 | + let mut url = format!("{}/api/track", rybbit.host.trim_end_matches('/')); | |
| 29 | + if let Some(key) = &rybbit.api_key { | |
| 30 | + url.push_str("?api_key="); | |
| 31 | + url.push_str(key); | |
| 32 | + } | |
| 33 | + match http.post(url).json(&body).send().await { | |
| 34 | + Ok(r) if r.status().is_success() => tracing::debug!(event, "analytics event sent"), | |
| 35 | + Ok(r) => tracing::warn!(event, status = %r.status(), "analytics event rejected"), | |
| 36 | + Err(error) => tracing::warn!(event, %error, "analytics event failed"), | |
| 37 | + } | |
| 38 | + }); | |
| 39 | +} |
+11-0backend/src/api/mod.rs
| @@ -0,0 +1,11 @@ | ||
| 1 | +//! JSON API for the `ig` CLI at /api/v1. Stub; filled in by the API work. | |
| 2 | + | |
| 3 | +pub mod release; | |
| 4 | + | |
| 5 | +use axum::Router; | |
| 6 | + | |
| 7 | +use crate::state::AppState; | |
| 8 | + | |
| 9 | +pub fn router() -> Router<AppState> { | |
| 10 | + Router::new() | |
| 11 | +} |
+9-0backend/src/api/release.rs
| @@ -0,0 +1,9 @@ | ||
| 1 | +//! CLI releases stored in R2. Stub. | |
| 2 | + | |
| 3 | +use std::path::Path; | |
| 4 | + | |
| 5 | +use crate::storage::Storage; | |
| 6 | + | |
| 7 | +pub async fn publish(_db: &sqlx::PgPool, _storage: &Storage, _path: &Path, _version: &str, _target: &str) -> anyhow::Result<()> { | |
| 8 | + anyhow::bail!("not implemented yet") | |
| 9 | +} |
+463-0backend/src/auth.rs
| @@ -0,0 +1,463 @@ | ||
| 1 | +//! Who is asking. Browsers carry a session cookie; git, docker, LFS and the | |
| 2 | +//! CLI carry a personal access token (Bearer, or Basic with any username). | |
| 3 | +//! Passwords are only accepted by the sign-in form. | |
| 4 | + | |
| 5 | +use argon2::{ | |
| 6 | + Argon2, | |
| 7 | + password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash}, | |
| 8 | +}; | |
| 9 | +use axum::{ | |
| 10 | + extract::{FromRequestParts, OptionalFromRequestParts}, | |
| 11 | + http::{HeaderMap, StatusCode, header, request::Parts}, | |
| 12 | + response::{IntoResponse, Redirect, Response}, | |
| 13 | +}; | |
| 14 | +use axum_extra::extract::cookie::{Cookie, SameSite}; | |
| 15 | +use base64::Engine; | |
| 16 | +use chrono::{Duration, Utc}; | |
| 17 | +use sha2::{Digest, Sha256}; | |
| 18 | +use sqlx::{FromRow, PgPool}; | |
| 19 | + | |
| 20 | +use crate::{ | |
| 21 | + error::{ApiError, AppError}, | |
| 22 | + state::AppState, | |
| 23 | +}; | |
| 24 | + | |
| 25 | +pub const SESSION_COOKIE: &str = "ig_session"; | |
| 26 | +const SESSION_DAYS: i64 = 30; | |
| 27 | +pub const TOKEN_PREFIX: &str = "igp_"; | |
| 28 | + | |
| 29 | +/// What a credential may do. Sessions get everything; tokens get what they | |
| 30 | +/// were created with. | |
| 31 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 32 | +pub struct Scopes { | |
| 33 | + pub repo: bool, | |
| 34 | + pub packages: bool, | |
| 35 | + pub user: bool, | |
| 36 | + pub admin: bool, | |
| 37 | +} | |
| 38 | + | |
| 39 | +impl Scopes { | |
| 40 | + pub const ALL: Scopes = Scopes { repo: true, packages: true, user: true, admin: true }; | |
| 41 | + | |
| 42 | + pub fn from_list(list: &[String]) -> Self { | |
| 43 | + let has = |s: &str| list.iter().any(|x| x == s); | |
| 44 | + Scopes { repo: has("repo"), packages: has("packages"), user: has("user"), admin: has("admin") } | |
| 45 | + } | |
| 46 | + | |
| 47 | + pub const NAMES: &'static [(&'static str, &'static str)] = &[ | |
| 48 | + ("repo", "Clone, push and manage repositories"), | |
| 49 | + ("packages", "Push, pull and manage container images"), | |
| 50 | + ("user", "Manage your profile, SSH keys and tokens"), | |
| 51 | + ("admin", "Site administration (admins only)"), | |
| 52 | + ]; | |
| 53 | +} | |
| 54 | + | |
| 55 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 56 | +pub enum AuthVia { | |
| 57 | + Session, | |
| 58 | + Token(i64), | |
| 59 | +} | |
| 60 | + | |
| 61 | +/// The signed-in person making a request. | |
| 62 | +#[derive(Debug, Clone)] | |
| 63 | +pub struct Viewer { | |
| 64 | + pub id: i64, | |
| 65 | + pub name: String, | |
| 66 | + pub is_admin: bool, | |
| 67 | + pub avatar_key: Option<String>, | |
| 68 | + pub scopes: Scopes, | |
| 69 | + pub via: AuthVia, | |
| 70 | +} | |
| 71 | + | |
| 72 | +impl Viewer { | |
| 73 | + /// Site admin powers, only when the credential carries them. | |
| 74 | + pub fn site_admin(&self) -> bool { | |
| 75 | + self.is_admin && self.scopes.admin | |
| 76 | + } | |
| 77 | +} | |
| 78 | + | |
| 79 | +#[derive(FromRow)] | |
| 80 | +struct ViewerRow { | |
| 81 | + id: i64, | |
| 82 | + name: String, | |
| 83 | + is_admin: bool, | |
| 84 | + avatar_key: Option<String>, | |
| 85 | +} | |
| 86 | + | |
| 87 | +// --------------------------------------------------------------------------- | |
| 88 | +// Secrets and hashing | |
| 89 | + | |
| 90 | +pub fn sha256_hex(data: impl AsRef<[u8]>) -> String { | |
| 91 | + hex::encode(Sha256::digest(data.as_ref())) | |
| 92 | +} | |
| 93 | + | |
| 94 | +/// URL-safe random string with `bytes` bytes of entropy. | |
| 95 | +pub fn random_token(bytes: usize) -> String { | |
| 96 | + let mut buf = vec![0u8; bytes]; | |
| 97 | + rand::fill(&mut buf[..]); | |
| 98 | + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf) | |
| 99 | +} | |
| 100 | + | |
| 101 | +/// Base62, for tokens people copy around (no '-' or '_' to trip selection). | |
| 102 | +pub fn random_base62(len: usize) -> String { | |
| 103 | + const ALPHABET: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | |
| 104 | + (0..len).map(|_| ALPHABET[rand::random_range(0..ALPHABET.len())] as char).collect() | |
| 105 | +} | |
| 106 | + | |
| 107 | +pub fn hash_password(password: &str) -> anyhow::Result<String> { | |
| 108 | + Argon2::default() | |
| 109 | + .hash_password(password.as_bytes()) | |
| 110 | + .map(|h| h.to_string()) | |
| 111 | + .map_err(|e| anyhow::anyhow!("hashing password: {e}")) | |
| 112 | +} | |
| 113 | + | |
| 114 | +pub fn verify_password(password: &str, hash: &str) -> bool { | |
| 115 | + match PasswordHash::new(hash) { | |
| 116 | + Ok(parsed) => Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok(), | |
| 117 | + Err(_) => false, | |
| 118 | + } | |
| 119 | +} | |
| 120 | + | |
| 121 | +/// Password checks run on a blocking thread: argon2 takes tens of ms. | |
| 122 | +pub async fn verify_password_async(password: String, hash: String) -> bool { | |
| 123 | + tokio::task::spawn_blocking(move || verify_password(&password, &hash)).await.unwrap_or(false) | |
| 124 | +} | |
| 125 | + | |
| 126 | +pub async fn hash_password_async(password: String) -> anyhow::Result<String> { | |
| 127 | + tokio::task::spawn_blocking(move || hash_password(&password)).await? | |
| 128 | +} | |
| 129 | + | |
| 130 | +pub fn validate_password(password: &str) -> Result<(), &'static str> { | |
| 131 | + if password.chars().count() < 10 { | |
| 132 | + return Err("Passwords must be at least 10 characters."); | |
| 133 | + } | |
| 134 | + if password.len() > 512 { | |
| 135 | + return Err("That password is too long."); | |
| 136 | + } | |
| 137 | + Ok(()) | |
| 138 | +} | |
| 139 | + | |
| 140 | +// --------------------------------------------------------------------------- | |
| 141 | +// Sessions | |
| 142 | + | |
| 143 | +pub async fn create_session(db: &PgPool, user_id: i64, ip: Option<&str>, user_agent: Option<&str>) -> anyhow::Result<String> { | |
| 144 | + let token = random_token(32); | |
| 145 | + sqlx::query( | |
| 146 | + "insert into sessions (id_hash, user_id, ip, user_agent, expires_at) values ($1, $2, $3, $4, $5)", | |
| 147 | + ) | |
| 148 | + .bind(sha256_hex(&token)) | |
| 149 | + .bind(user_id) | |
| 150 | + .bind(ip) | |
| 151 | + .bind(user_agent.map(|ua| ua.chars().take(300).collect::<String>())) | |
| 152 | + .bind(Utc::now() + Duration::days(SESSION_DAYS)) | |
| 153 | + .execute(db) | |
| 154 | + .await?; | |
| 155 | + sqlx::query("update users set last_login_at = now() where account_id = $1").bind(user_id).execute(db).await?; | |
| 156 | + Ok(token) | |
| 157 | +} | |
| 158 | + | |
| 159 | +pub fn session_cookie(token: String, secure: bool) -> Cookie<'static> { | |
| 160 | + Cookie::build((SESSION_COOKIE, token)) | |
| 161 | + .path("/") | |
| 162 | + .http_only(true) | |
| 163 | + .secure(secure) | |
| 164 | + .same_site(SameSite::Lax) | |
| 165 | + .max_age(time::Duration::days(SESSION_DAYS)) | |
| 166 | + .build() | |
| 167 | +} | |
| 168 | + | |
| 169 | +pub fn clear_session_cookie() -> Cookie<'static> { | |
| 170 | + Cookie::build((SESSION_COOKIE, "")).path("/").max_age(time::Duration::ZERO).build() | |
| 171 | +} | |
| 172 | + | |
| 173 | +pub async fn delete_session(db: &PgPool, token: &str) -> anyhow::Result<()> { | |
| 174 | + sqlx::query("delete from sessions where id_hash = $1").bind(sha256_hex(token)).execute(db).await?; | |
| 175 | + Ok(()) | |
| 176 | +} | |
| 177 | + | |
| 178 | +fn cookie_value<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { | |
| 179 | + headers | |
| 180 | + .get_all(header::COOKIE) | |
| 181 | + .iter() | |
| 182 | + .filter_map(|v| v.to_str().ok()) | |
| 183 | + .flat_map(|v| v.split(';')) | |
| 184 | + .filter_map(|pair| pair.trim().split_once('=')) | |
| 185 | + .find(|(k, _)| *k == name) | |
| 186 | + .map(|(_, v)| v) | |
| 187 | +} | |
| 188 | + | |
| 189 | +pub fn session_token(headers: &HeaderMap) -> Option<&str> { | |
| 190 | + cookie_value(headers, SESSION_COOKIE).filter(|v| !v.is_empty()) | |
| 191 | +} | |
| 192 | + | |
| 193 | +async fn viewer_from_session(db: &PgPool, token: &str) -> anyhow::Result<Option<Viewer>> { | |
| 194 | + let row: Option<(ViewerRow, chrono::DateTime<Utc>)> = sqlx::query_as::<_, (i64, String, bool, Option<String>, chrono::DateTime<Utc>)>( | |
| 195 | + "select a.id, a.name::text, u.is_admin, a.avatar_key, s.last_seen_at | |
| 196 | + from sessions s join users u on u.account_id = s.user_id join accounts a on a.id = u.account_id | |
| 197 | + where s.id_hash = $1 and s.expires_at > now() and u.suspended_at is null", | |
| 198 | + ) | |
| 199 | + .bind(sha256_hex(token)) | |
| 200 | + .fetch_optional(db) | |
| 201 | + .await? | |
| 202 | + .map(|(id, name, is_admin, avatar_key, seen)| (ViewerRow { id, name, is_admin, avatar_key }, seen)); | |
| 203 | + | |
| 204 | + let Some((row, last_seen)) = row else { return Ok(None) }; | |
| 205 | + // Sliding expiry, written at most every ten minutes. | |
| 206 | + if Utc::now() - last_seen > Duration::minutes(10) { | |
| 207 | + sqlx::query("update sessions set last_seen_at = now(), expires_at = $2 where id_hash = $1") | |
| 208 | + .bind(sha256_hex(token)) | |
| 209 | + .bind(Utc::now() + Duration::days(SESSION_DAYS)) | |
| 210 | + .execute(db) | |
| 211 | + .await?; | |
| 212 | + } | |
| 213 | + Ok(Some(Viewer { | |
| 214 | + id: row.id, | |
| 215 | + name: row.name, | |
| 216 | + is_admin: row.is_admin, | |
| 217 | + avatar_key: row.avatar_key, | |
| 218 | + scopes: Scopes::ALL, | |
| 219 | + via: AuthVia::Session, | |
| 220 | + })) | |
| 221 | +} | |
| 222 | + | |
| 223 | +// --------------------------------------------------------------------------- | |
| 224 | +// Personal access tokens | |
| 225 | + | |
| 226 | +pub struct NewToken { | |
| 227 | + pub id: i64, | |
| 228 | + pub secret: String, | |
| 229 | +} | |
| 230 | + | |
| 231 | +pub async fn create_token(db: &PgPool, user_id: i64, name: &str, scopes: &[&str], expires_at: Option<chrono::DateTime<Utc>>) -> anyhow::Result<NewToken> { | |
| 232 | + let secret = format!("{TOKEN_PREFIX}{}", random_base62(40)); | |
| 233 | + let id: i64 = sqlx::query_scalar( | |
| 234 | + "insert into access_tokens (user_id, name, token_hash, token_prefix, scopes, expires_at) | |
| 235 | + values ($1, $2, $3, $4, $5, $6) returning id", | |
| 236 | + ) | |
| 237 | + .bind(user_id) | |
| 238 | + .bind(name) | |
| 239 | + .bind(sha256_hex(&secret)) | |
| 240 | + .bind(&secret[..TOKEN_PREFIX.len() + 6]) | |
| 241 | + .bind(scopes.iter().map(|s| s.to_string()).collect::<Vec<_>>()) | |
| 242 | + .bind(expires_at) | |
| 243 | + .fetch_one(db) | |
| 244 | + .await?; | |
| 245 | + Ok(NewToken { id, secret }) | |
| 246 | +} | |
| 247 | + | |
| 248 | +pub async fn viewer_from_token(db: &PgPool, secret: &str) -> anyhow::Result<Option<Viewer>> { | |
| 249 | + if !secret.starts_with(TOKEN_PREFIX) { | |
| 250 | + return Ok(None); | |
| 251 | + } | |
| 252 | + let row: Option<(i64, i64, String, bool, Option<String>, Vec<String>, Option<chrono::DateTime<Utc>>)> = sqlx::query_as( | |
| 253 | + "select t.id, a.id, a.name::text, u.is_admin, a.avatar_key, t.scopes, t.last_used_at | |
| 254 | + from access_tokens t join users u on u.account_id = t.user_id join accounts a on a.id = u.account_id | |
| 255 | + where t.token_hash = $1 and (t.expires_at is null or t.expires_at > now()) and u.suspended_at is null", | |
| 256 | + ) | |
| 257 | + .bind(sha256_hex(secret)) | |
| 258 | + .fetch_optional(db) | |
| 259 | + .await?; | |
| 260 | + let Some((token_id, id, name, is_admin, avatar_key, scopes, last_used)) = row else { return Ok(None) }; | |
| 261 | + if last_used.is_none_or(|t| Utc::now() - t > Duration::minutes(5)) { | |
| 262 | + sqlx::query("update access_tokens set last_used_at = now() where id = $1").bind(token_id).execute(db).await?; | |
| 263 | + } | |
| 264 | + let mut scopes = Scopes::from_list(&scopes); | |
| 265 | + scopes.admin &= is_admin; | |
| 266 | + Ok(Some(Viewer { id, name, is_admin, avatar_key, scopes, via: AuthVia::Token(token_id) })) | |
| 267 | +} | |
| 268 | + | |
| 269 | +/// Result of reading an Authorization header. | |
| 270 | +pub enum HeaderAuth { | |
| 271 | + /// No credentials were sent. | |
| 272 | + Anonymous, | |
| 273 | + /// Credentials were sent but are wrong, expired or revoked. | |
| 274 | + Invalid, | |
| 275 | + Viewer(Viewer), | |
| 276 | +} | |
| 277 | + | |
| 278 | +/// Accepts `Bearer igp_...`, `token igp_...` and `Basic base64(any:igp_...)`. | |
| 279 | +pub async fn authenticate_header(db: &PgPool, headers: &HeaderMap) -> anyhow::Result<HeaderAuth> { | |
| 280 | + let Some(value) = headers.get(header::AUTHORIZATION).and_then(|v| v.to_str().ok()) else { | |
| 281 | + return Ok(HeaderAuth::Anonymous); | |
| 282 | + }; | |
| 283 | + let (scheme, rest) = value.split_once(' ').unwrap_or((value, "")); | |
| 284 | + let secret = match scheme.to_ascii_lowercase().as_str() { | |
| 285 | + "bearer" | "token" => rest.trim().to_string(), | |
| 286 | + "basic" => match basic_credentials(rest.trim()) { | |
| 287 | + Some((_, password)) => password, | |
| 288 | + None => return Ok(HeaderAuth::Invalid), | |
| 289 | + }, | |
| 290 | + _ => return Ok(HeaderAuth::Invalid), | |
| 291 | + }; | |
| 292 | + Ok(match viewer_from_token(db, &secret).await? { | |
| 293 | + Some(viewer) => HeaderAuth::Viewer(viewer), | |
| 294 | + None => HeaderAuth::Invalid, | |
| 295 | + }) | |
| 296 | +} | |
| 297 | + | |
| 298 | +/// Decodes a Basic credential into (username, password). | |
| 299 | +pub fn basic_credentials(encoded: &str) -> Option<(String, String)> { | |
| 300 | + let decoded = base64::engine::general_purpose::STANDARD.decode(encoded).ok()?; | |
| 301 | + let text = String::from_utf8(decoded).ok()?; | |
| 302 | + let (user, pass) = text.split_once(':')?; | |
| 303 | + Some((user.to_string(), pass.to_string())) | |
| 304 | +} | |
| 305 | + | |
| 306 | +// --------------------------------------------------------------------------- | |
| 307 | +// Extractors | |
| 308 | + | |
| 309 | +/// The signed-in browser user, if any. Never rejects. | |
| 310 | +pub struct MaybeViewer(pub Option<Viewer>); | |
| 311 | + | |
| 312 | +impl FromRequestParts<AppState> for MaybeViewer { | |
| 313 | + type Rejection = AppError; | |
| 314 | + | |
| 315 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { | |
| 316 | + if let Some(viewer) = parts.extensions.get::<Option<Viewer>>() { | |
| 317 | + return Ok(MaybeViewer(viewer.clone())); | |
| 318 | + } | |
| 319 | + let viewer = match session_token(&parts.headers) { | |
| 320 | + Some(token) => viewer_from_session(&state.db, token).await?, | |
| 321 | + None => None, | |
| 322 | + }; | |
| 323 | + parts.extensions.insert(viewer.clone()); | |
| 324 | + Ok(MaybeViewer(viewer)) | |
| 325 | + } | |
| 326 | +} | |
| 327 | + | |
| 328 | +/// A page that needs a signed-in user. Redirects to /login?next=... otherwise. | |
| 329 | +pub struct RequireViewer(pub Viewer); | |
| 330 | + | |
| 331 | +pub struct LoginRedirect(String); | |
| 332 | + | |
| 333 | +impl IntoResponse for LoginRedirect { | |
| 334 | + fn into_response(self) -> Response { | |
| 335 | + Redirect::to(&format!("/login?next={}", urlencode(&self.0))).into_response() | |
| 336 | + } | |
| 337 | +} | |
| 338 | + | |
| 339 | +impl FromRequestParts<AppState> for RequireViewer { | |
| 340 | + type Rejection = Response; | |
| 341 | + | |
| 342 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { | |
| 343 | + let MaybeViewer(viewer) = MaybeViewer::from_request_parts(parts, state).await.map_err(IntoResponse::into_response)?; | |
| 344 | + match viewer { | |
| 345 | + Some(viewer) => Ok(RequireViewer(viewer)), | |
| 346 | + None => { | |
| 347 | + let next = parts.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/").to_string(); | |
| 348 | + Err(LoginRedirect(next).into_response()) | |
| 349 | + } | |
| 350 | + } | |
| 351 | + } | |
| 352 | +} | |
| 353 | + | |
| 354 | +/// API caller: a token in the Authorization header, or a browser session. | |
| 355 | +pub struct ApiViewer(pub Viewer); | |
| 356 | + | |
| 357 | +impl FromRequestParts<AppState> for ApiViewer { | |
| 358 | + type Rejection = ApiError; | |
| 359 | + | |
| 360 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { | |
| 361 | + match <ApiViewer as OptionalFromRequestParts<AppState>>::from_request_parts(parts, state).await? { | |
| 362 | + Some(viewer) => Ok(viewer), | |
| 363 | + None => Err(ApiError(AppError::Unauthorized)), | |
| 364 | + } | |
| 365 | + } | |
| 366 | +} | |
| 367 | + | |
| 368 | +impl OptionalFromRequestParts<AppState> for ApiViewer { | |
| 369 | + type Rejection = ApiError; | |
| 370 | + | |
| 371 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Option<Self>, Self::Rejection> { | |
| 372 | + match authenticate_header(&state.db, &parts.headers).await? { | |
| 373 | + HeaderAuth::Viewer(viewer) => Ok(Some(ApiViewer(viewer))), | |
| 374 | + HeaderAuth::Invalid => Err(ApiError(AppError::Unauthorized)), | |
| 375 | + HeaderAuth::Anonymous => { | |
| 376 | + let MaybeViewer(viewer) = MaybeViewer::from_request_parts(parts, state).await?; | |
| 377 | + Ok(viewer.map(ApiViewer)) | |
| 378 | + } | |
| 379 | + } | |
| 380 | + } | |
| 381 | +} | |
| 382 | + | |
| 383 | +/// The site admin panel. 404s for everyone else so it is not discoverable. | |
| 384 | +pub struct AdminViewer(pub Viewer); | |
| 385 | + | |
| 386 | +impl FromRequestParts<AppState> for AdminViewer { | |
| 387 | + type Rejection = Response; | |
| 388 | + | |
| 389 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { | |
| 390 | + let RequireViewer(viewer) = RequireViewer::from_request_parts(parts, state).await?; | |
| 391 | + if viewer.site_admin() { | |
| 392 | + Ok(AdminViewer(viewer)) | |
| 393 | + } else { | |
| 394 | + Err(AppError::NotFound.into_response()) | |
| 395 | + } | |
| 396 | + } | |
| 397 | +} | |
| 398 | + | |
| 399 | +/// Client IP as seen through a reverse proxy, for logs and the audit trail. | |
| 400 | +pub fn client_ip(headers: &HeaderMap) -> Option<String> { | |
| 401 | + for name in ["cf-connecting-ip", "x-real-ip"] { | |
| 402 | + if let Some(v) = headers.get(name).and_then(|v| v.to_str().ok()) { | |
| 403 | + return Some(v.trim().to_string()); | |
| 404 | + } | |
| 405 | + } | |
| 406 | + headers | |
| 407 | + .get("x-forwarded-for") | |
| 408 | + .and_then(|v| v.to_str().ok()) | |
| 409 | + .and_then(|v| v.split(',').next()) | |
| 410 | + .map(|v| v.trim().to_string()) | |
| 411 | +} | |
| 412 | + | |
| 413 | +pub fn user_agent(headers: &HeaderMap) -> Option<&str> { | |
| 414 | + headers.get(header::USER_AGENT).and_then(|v| v.to_str().ok()) | |
| 415 | +} | |
| 416 | + | |
| 417 | +pub fn urlencode(s: &str) -> String { | |
| 418 | + url::form_urlencoded::byte_serialize(s.as_bytes()).collect() | |
| 419 | +} | |
| 420 | + | |
| 421 | +/// Only same-site relative paths are allowed as post-login destinations. | |
| 422 | +pub fn safe_next(next: Option<&str>) -> String { | |
| 423 | + match next { | |
| 424 | + Some(n) if n.starts_with('/') && !n.starts_with("//") && !n.starts_with("/\\") => n.to_string(), | |
| 425 | + _ => "/".to_string(), | |
| 426 | + } | |
| 427 | +} | |
| 428 | + | |
| 429 | +/// A 401 that makes git and docker prompt for (or send) credentials. | |
| 430 | +pub fn basic_challenge(message: &str) -> Response { | |
| 431 | + ( | |
| 432 | + StatusCode::UNAUTHORIZED, | |
| 433 | + [(header::WWW_AUTHENTICATE, "Basic realm=\"irongit\"")], | |
| 434 | + format!("{message}\n"), | |
| 435 | + ) | |
| 436 | + .into_response() | |
| 437 | +} | |
| 438 | + | |
| 439 | +#[cfg(test)] | |
| 440 | +mod tests { | |
| 441 | + use super::*; | |
| 442 | + | |
| 443 | + #[test] | |
| 444 | + fn password_roundtrip() { | |
| 445 | + let hash = hash_password("correct horse battery").unwrap(); | |
| 446 | + assert!(verify_password("correct horse battery", &hash)); | |
| 447 | + assert!(!verify_password("wrong", &hash)); | |
| 448 | + } | |
| 449 | + | |
| 450 | + #[test] | |
| 451 | + fn basic_parsing() { | |
| 452 | + let encoded = base64::engine::general_purpose::STANDARD.encode("alice:igp_abc"); | |
| 453 | + assert_eq!(basic_credentials(&encoded), Some(("alice".into(), "igp_abc".into()))); | |
| 454 | + } | |
| 455 | + | |
| 456 | + #[test] | |
| 457 | + fn next_is_local_only() { | |
| 458 | + assert_eq!(safe_next(Some("/a/b?c")), "/a/b?c"); | |
| 459 | + assert_eq!(safe_next(Some("//evil.com")), "/"); | |
| 460 | + assert_eq!(safe_next(Some("https://evil.com")), "/"); | |
| 461 | + assert_eq!(safe_next(None), "/"); | |
| 462 | + } | |
| 463 | +} |
+5-0backend/src/backup.rs
| @@ -0,0 +1,5 @@ | ||
| 1 | +//! Nightly backups of repositories and Postgres to R2. Stub. | |
| 2 | + | |
| 3 | +use crate::state::AppState; | |
| 4 | + | |
| 5 | +pub fn spawn(_state: AppState) {} |
+51-0backend/src/clientlog.rs
| @@ -0,0 +1,51 @@ | ||
| 1 | +//! Browser logs. The page script batches console errors, uncaught | |
| 2 | +//! exceptions and failed fetches to POST /api/v1/client-logs; they are | |
| 3 | +//! written to LOG_DIR/frontend.<date>.log with the user and page. | |
| 4 | + | |
| 5 | +use axum::{ | |
| 6 | + Json, Router, | |
| 7 | + extract::DefaultBodyLimit, | |
| 8 | + http::{HeaderMap, StatusCode}, | |
| 9 | + routing::post, | |
| 10 | +}; | |
| 11 | +use serde::Deserialize; | |
| 12 | + | |
| 13 | +use crate::{ | |
| 14 | + auth::{self, MaybeViewer}, | |
| 15 | + logging::FRONTEND_TARGET, | |
| 16 | + state::AppState, | |
| 17 | +}; | |
| 18 | + | |
| 19 | +pub fn router() -> Router<AppState> { | |
| 20 | + Router::new().route("/api/v1/client-logs", post(ingest).layer(DefaultBodyLimit::max(64 * 1024))) | |
| 21 | +} | |
| 22 | + | |
| 23 | +#[derive(Deserialize)] | |
| 24 | +struct Batch { | |
| 25 | + entries: Vec<Entry>, | |
| 26 | +} | |
| 27 | + | |
| 28 | +#[derive(Deserialize)] | |
| 29 | +struct Entry { | |
| 30 | + level: String, | |
| 31 | + message: String, | |
| 32 | + url: String, | |
| 33 | + at: Option<String>, | |
| 34 | + stack: Option<String>, | |
| 35 | +} | |
| 36 | + | |
| 37 | +async fn ingest(MaybeViewer(viewer): MaybeViewer, headers: HeaderMap, Json(batch): Json<Batch>) -> StatusCode { | |
| 38 | + let user = viewer.as_ref().map(|v| v.name.as_str()).unwrap_or("-"); | |
| 39 | + let agent = auth::user_agent(&headers).unwrap_or("-"); | |
| 40 | + for entry in batch.entries.into_iter().take(50) { | |
| 41 | + let message: String = entry.message.chars().take(2000).collect(); | |
| 42 | + let stack: String = entry.stack.unwrap_or_default().chars().take(4000).collect(); | |
| 43 | + let at = entry.at.unwrap_or_default(); | |
| 44 | + match entry.level.as_str() { | |
| 45 | + "error" => tracing::error!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, stack, "{message}"), | |
| 46 | + "warn" => tracing::warn!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, "{message}"), | |
| 47 | + _ => tracing::info!(target: FRONTEND_TARGET, user, url = %entry.url, at, agent, "{message}"), | |
| 48 | + } | |
| 49 | + } | |
| 50 | + StatusCode::NO_CONTENT | |
| 51 | +} |
+218-0backend/src/config.rs
| @@ -0,0 +1,218 @@ | ||
| 1 | +//! Configuration from the environment (and `.env` in development). | |
| 2 | + | |
| 3 | +use std::path::PathBuf; | |
| 4 | + | |
| 5 | +use anyhow::Context; | |
| 6 | +use url::Url; | |
| 7 | + | |
| 8 | +#[derive(Clone, Debug)] | |
| 9 | +pub struct Config { | |
| 10 | + pub host: String, | |
| 11 | + pub port: u16, | |
| 12 | + pub ssh_port: u16, | |
| 13 | + /// How people reach the site, e.g. "https://git.example.com". Used for | |
| 14 | + /// clone URLs, the registry token realm, LFS hrefs and cookie security. | |
| 15 | + pub public_url: Url, | |
| 16 | + /// Host (and port, if not 22) shown in SSH clone URLs. | |
| 17 | + pub ssh_host: String, | |
| 18 | + pub database_url: String, | |
| 19 | + pub data_dir: PathBuf, | |
| 20 | + pub log_dir: PathBuf, | |
| 21 | + /// HMAC key for registry and LFS bearer tokens. | |
| 22 | + pub secret_key: Vec<u8>, | |
| 23 | + pub r2: R2Config, | |
| 24 | + pub registration_open: bool, | |
| 25 | + pub limits: Limits, | |
| 26 | + pub smtp: Option<SmtpConfig>, | |
| 27 | + pub rybbit: Option<RybbitConfig>, | |
| 28 | + pub google: Option<GoogleConfig>, | |
| 29 | + pub backup_interval_hours: u64, | |
| 30 | +} | |
| 31 | + | |
| 32 | +/// Google sign-in (OpenID Connect). Redirect URI: {PUBLIC_URL}/login/google/callback | |
| 33 | +#[derive(Clone, Debug)] | |
| 34 | +pub struct GoogleConfig { | |
| 35 | + pub client_id: String, | |
| 36 | + pub client_secret: String, | |
| 37 | +} | |
| 38 | + | |
| 39 | +#[derive(Clone, Debug)] | |
| 40 | +pub struct R2Config { | |
| 41 | + pub endpoint: String, | |
| 42 | + pub bucket: String, | |
| 43 | + pub access_key_id: String, | |
| 44 | + pub secret_access_key: String, | |
| 45 | +} | |
| 46 | + | |
| 47 | +#[derive(Clone, Debug)] | |
| 48 | +pub struct Limits { | |
| 49 | + /// Largest single file a git push may contain. Bigger files belong in LFS. | |
| 50 | + pub max_file_bytes: u64, | |
| 51 | + /// Default storage per account across its repos (git on disk). | |
| 52 | + pub account_quota_bytes: u64, | |
| 53 | + pub max_lfs_object_bytes: u64, | |
| 54 | + pub max_image_layer_bytes: u64, | |
| 55 | + pub max_manifest_bytes: usize, | |
| 56 | +} | |
| 57 | + | |
| 58 | +#[derive(Clone, Debug)] | |
| 59 | +pub struct SmtpConfig { | |
| 60 | + pub host: String, | |
| 61 | + pub port: u16, | |
| 62 | + pub username: String, | |
| 63 | + pub password: String, | |
| 64 | + pub from: String, | |
| 65 | +} | |
| 66 | + | |
| 67 | +#[derive(Clone, Debug)] | |
| 68 | +pub struct RybbitConfig { | |
| 69 | + pub host: String, | |
| 70 | + pub site_id: String, | |
| 71 | + pub api_key: Option<String>, | |
| 72 | +} | |
| 73 | + | |
| 74 | +const MB: u64 = 1024 * 1024; | |
| 75 | + | |
| 76 | +impl Config { | |
| 77 | + pub fn from_env() -> anyhow::Result<Self> { | |
| 78 | + let port: u16 = var("PORT").map(|v| v.parse()).transpose()?.unwrap_or(7878); | |
| 79 | + let public_url: Url = var("PUBLIC_URL") | |
| 80 | + .unwrap_or_else(|| format!("http://localhost:{port}")) | |
| 81 | + .parse() | |
| 82 | + .context("PUBLIC_URL is not a valid URL")?; | |
| 83 | + let ssh_port: u16 = var("SSH_PORT").map(|v| v.parse()).transpose()?.unwrap_or(2222); | |
| 84 | + let ssh_host = var("SSH_HOST").unwrap_or_else(|| { | |
| 85 | + let host = public_url.host_str().unwrap_or("localhost").to_string(); | |
| 86 | + if ssh_port == 22 { host } else { format!("{host}:{ssh_port}") } | |
| 87 | + }); | |
| 88 | + | |
| 89 | + let data_dir = PathBuf::from(var("DATA_DIR").unwrap_or_else(|| "data".into())); | |
| 90 | + std::fs::create_dir_all(&data_dir).with_context(|| format!("cannot create {}", data_dir.display()))?; | |
| 91 | + let data_dir = data_dir.canonicalize()?; | |
| 92 | + let log_dir = var("LOG_DIR").map(PathBuf::from).unwrap_or_else(|| data_dir.join("logs")); | |
| 93 | + | |
| 94 | + let r2_account = var("R2_ACCOUNT_ID"); | |
| 95 | + let r2 = R2Config { | |
| 96 | + endpoint: var("R2_ENDPOINT") | |
| 97 | + .or_else(|| r2_account.map(|id| format!("https://{id}.r2.cloudflarestorage.com"))) | |
| 98 | + .context("set R2_ACCOUNT_ID (or R2_ENDPOINT)")?, | |
| 99 | + bucket: var("R2_BUCKET").context("set R2_BUCKET")?, | |
| 100 | + access_key_id: var("R2_ACCESS_KEY_ID").context("set R2_ACCESS_KEY_ID")?, | |
| 101 | + secret_access_key: var("R2_SECRET_ACCESS_KEY").context("set R2_SECRET_ACCESS_KEY")?, | |
| 102 | + }; | |
| 103 | + | |
| 104 | + let smtp = match (var("SMTP_HOST"), var("SMTP_USERNAME"), var("SMTP_PASSWORD")) { | |
| 105 | + (Some(host), Some(username), Some(password)) => Some(SmtpConfig { | |
| 106 | + port: var("SMTP_PORT").map(|v| v.parse()).transpose()?.unwrap_or(465), | |
| 107 | + from: var("SMTP_FROM").unwrap_or_else(|| username.clone()), | |
| 108 | + host, | |
| 109 | + username, | |
| 110 | + password, | |
| 111 | + }), | |
| 112 | + _ => None, | |
| 113 | + }; | |
| 114 | + | |
| 115 | + let rybbit = var("RYBBIT_SITE_ID").map(|site_id| RybbitConfig { | |
| 116 | + host: var("RYBBIT_HOST").unwrap_or_else(|| "https://a.hygo.ai".into()), | |
| 117 | + site_id, | |
| 118 | + api_key: var("RYBBIT_API_KEY"), | |
| 119 | + }); | |
| 120 | + | |
| 121 | + let google = match (var("GOOGLE_CLIENT_ID"), var("GOOGLE_CLIENT_SECRET")) { | |
| 122 | + (Some(client_id), Some(client_secret)) => Some(GoogleConfig { client_id, client_secret }), | |
| 123 | + _ => None, | |
| 124 | + }; | |
| 125 | + | |
| 126 | + Ok(Self { | |
| 127 | + host: var("HOST").unwrap_or_else(|| "127.0.0.1".into()), | |
| 128 | + port, | |
| 129 | + ssh_port, | |
| 130 | + public_url, | |
| 131 | + ssh_host, | |
| 132 | + database_url: var("DATABASE_URL").context("set DATABASE_URL")?, | |
| 133 | + secret_key: load_secret_key(&data_dir)?, | |
| 134 | + data_dir, | |
| 135 | + log_dir, | |
| 136 | + r2, | |
| 137 | + registration_open: flag("REGISTRATION_OPEN", true), | |
| 138 | + limits: Limits { | |
| 139 | + max_file_bytes: mb("MAX_FILE_MB", 50)?, | |
| 140 | + account_quota_bytes: mb("ACCOUNT_QUOTA_MB", 5 * 1024)?, | |
| 141 | + max_lfs_object_bytes: mb("MAX_LFS_OBJECT_MB", 2 * 1024)?, | |
| 142 | + max_image_layer_bytes: mb("MAX_IMAGE_LAYER_MB", 10 * 1024)?, | |
| 143 | + max_manifest_bytes: 4 * 1024 * 1024, | |
| 144 | + }, | |
| 145 | + smtp, | |
| 146 | + rybbit, | |
| 147 | + google, | |
| 148 | + backup_interval_hours: var("BACKUP_INTERVAL_HOURS").map(|v| v.parse()).transpose()?.unwrap_or(24), | |
| 149 | + }) | |
| 150 | + } | |
| 151 | + | |
| 152 | + pub fn repos_dir(&self) -> PathBuf { | |
| 153 | + self.data_dir.join("repos") | |
| 154 | + } | |
| 155 | + | |
| 156 | + pub fn uploads_dir(&self) -> PathBuf { | |
| 157 | + self.data_dir.join("uploads") | |
| 158 | + } | |
| 159 | + | |
| 160 | + pub fn hooks_dir(&self) -> PathBuf { | |
| 161 | + self.data_dir.join("hooks") | |
| 162 | + } | |
| 163 | + | |
| 164 | + /// "https://git.example.com" with no trailing slash. | |
| 165 | + pub fn base_url(&self) -> String { | |
| 166 | + self.public_url.as_str().trim_end_matches('/').to_string() | |
| 167 | + } | |
| 168 | + | |
| 169 | + /// "git.example.com" or "localhost:8080": what goes in `docker pull`. | |
| 170 | + pub fn registry_host(&self) -> String { | |
| 171 | + let host = self.public_url.host_str().unwrap_or("localhost"); | |
| 172 | + match self.public_url.port() { | |
| 173 | + Some(port) => format!("{host}:{port}"), | |
| 174 | + None => host.to_string(), | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + pub fn secure_cookies(&self) -> bool { | |
| 179 | + self.public_url.scheme() == "https" | |
| 180 | + } | |
| 181 | +} | |
| 182 | + | |
| 183 | +/// SECRET_KEY (hex) if set; otherwise a random key persisted in the data dir, | |
| 184 | +/// so development needs no setup and tokens survive restarts. | |
| 185 | +fn load_secret_key(data_dir: &std::path::Path) -> anyhow::Result<Vec<u8>> { | |
| 186 | + if let Some(hex_key) = var("SECRET_KEY") { | |
| 187 | + let key = hex::decode(hex_key).context("SECRET_KEY must be hex")?; | |
| 188 | + anyhow::ensure!(key.len() >= 32, "SECRET_KEY must be at least 32 bytes (64 hex chars)"); | |
| 189 | + return Ok(key); | |
| 190 | + } | |
| 191 | + let path = data_dir.join("secret.key"); | |
| 192 | + if let Ok(existing) = std::fs::read_to_string(&path) { | |
| 193 | + return hex::decode(existing.trim()).context("data/secret.key is corrupt"); | |
| 194 | + } | |
| 195 | + let key: [u8; 32] = rand::random(); | |
| 196 | + std::fs::write(&path, hex::encode(key))?; | |
| 197 | + #[cfg(unix)] | |
| 198 | + { | |
| 199 | + use std::os::unix::fs::PermissionsExt; | |
| 200 | + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; | |
| 201 | + } | |
| 202 | + Ok(key.to_vec()) | |
| 203 | +} | |
| 204 | + | |
| 205 | +pub fn var(name: &str) -> Option<String> { | |
| 206 | + std::env::var(name).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty()) | |
| 207 | +} | |
| 208 | + | |
| 209 | +pub fn flag(name: &str, default: bool) -> bool { | |
| 210 | + match var(name) { | |
| 211 | + Some(v) => !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no" | "off"), | |
| 212 | + None => default, | |
| 213 | + } | |
| 214 | +} | |
| 215 | + | |
| 216 | +fn mb(name: &str, default: u64) -> anyhow::Result<u64> { | |
| 217 | + Ok(var(name).map(|v| v.parse::<u64>()).transpose().with_context(|| format!("{name} must be a number"))?.unwrap_or(default) * MB) | |
| 218 | +} |
+122-0backend/src/dev.rs
| @@ -0,0 +1,122 @@ | ||
| 1 | +//! Hot reload, debug builds only. | |
| 2 | +//! | |
| 3 | +//! One `bun run build` at startup, another whenever a source file under | |
| 4 | +//! `frontend/` changes, and a `reload` frame to every browser holding the | |
| 5 | +//! `/dev-ws` socket afterwards. The page only opens that socket when it was | |
| 6 | +//! built with `PUBLIC_HOT_RELOAD=true`, which this module sets, so a | |
| 7 | +//! production build never carries the client script. | |
| 8 | + | |
| 9 | +use std::{path::PathBuf, process::Command, sync::mpsc, time::Duration}; | |
| 10 | + | |
| 11 | +use axum::{ | |
| 12 | + extract::{ | |
| 13 | + State, WebSocketUpgrade, | |
| 14 | + ws::{Message, WebSocket}, | |
| 15 | + }, | |
| 16 | + response::IntoResponse, | |
| 17 | +}; | |
| 18 | +use notify::{RecursiveMode, Watcher, recommended_watcher}; | |
| 19 | +use tokio::sync::broadcast::{self, Sender}; | |
| 20 | + | |
| 21 | +use crate::AppState; | |
| 22 | + | |
| 23 | +fn frontend_dir() -> PathBuf { | |
| 24 | + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../frontend") | |
| 25 | +} | |
| 26 | + | |
| 27 | +fn build_frontend(reload_tx: &Sender<()>) -> anyhow::Result<()> { | |
| 28 | + tracing::info!("building frontend"); | |
| 29 | + let status = Command::new("bun") | |
| 30 | + .args(["run", "build"]) | |
| 31 | + .env("PUBLIC_HOT_RELOAD", "true") | |
| 32 | + .current_dir(frontend_dir()) | |
| 33 | + .status()?; | |
| 34 | + | |
| 35 | + if status.success() { | |
| 36 | + let browsers = reload_tx.send(()).unwrap_or_default(); | |
| 37 | + tracing::info!(browsers, "frontend built; reloading browsers"); | |
| 38 | + } else { | |
| 39 | + // Leave the last good build in place and wait for the next edit. | |
| 40 | + tracing::error!("frontend build failed"); | |
| 41 | + } | |
| 42 | + Ok(()) | |
| 43 | +} | |
| 44 | + | |
| 45 | +pub fn watch_frontend(reload_tx: Sender<()>) -> anyhow::Result<()> { | |
| 46 | + build_frontend(&reload_tx)?; | |
| 47 | + | |
| 48 | + let debounce = Duration::from_millis( | |
| 49 | + std::env::var("DEBOUNCE_MILLIS") | |
| 50 | + .ok() | |
| 51 | + .and_then(|v| v.parse().ok()) | |
| 52 | + .unwrap_or(500), | |
| 53 | + ); | |
| 54 | + | |
| 55 | + std::thread::spawn(move || { | |
| 56 | + let (event_tx, event_rx) = mpsc::channel(); | |
| 57 | + let mut watcher = | |
| 58 | + recommended_watcher(move |event| { let _ = event_tx.send(event); }) | |
| 59 | + .expect("failed to create frontend watcher"); | |
| 60 | + watcher | |
| 61 | + .watch(&frontend_dir(), RecursiveMode::Recursive) | |
| 62 | + .expect("failed to watch frontend directory"); | |
| 63 | + | |
| 64 | + while let Ok(event) = event_rx.recv() { | |
| 65 | + let Ok(event) = event else { continue }; | |
| 66 | + if !is_source_change(&event) { | |
| 67 | + continue; | |
| 68 | + } | |
| 69 | + | |
| 70 | + // Wait for the editor to finish its burst of writes. Trailing | |
| 71 | + // edge, so the last event can never be dropped. | |
| 72 | + while event_rx.recv_timeout(debounce).is_ok() {} | |
| 73 | + | |
| 74 | + if let Err(error) = build_frontend(&reload_tx) { | |
| 75 | + tracing::error!(%error, "could not build frontend"); | |
| 76 | + } | |
| 77 | + } | |
| 78 | + }); | |
| 79 | + | |
| 80 | + Ok(()) | |
| 81 | +} | |
| 82 | + | |
| 83 | +/// Ignores the build's own output, so a rebuild cannot trigger another one. | |
| 84 | +fn is_source_change(event: ¬ify::Event) -> bool { | |
| 85 | + use notify::EventKind; | |
| 86 | + matches!( | |
| 87 | + event.kind, | |
| 88 | + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) | |
| 89 | + ) && event.paths.iter().any(|path| { | |
| 90 | + !path.components().any(|part| { | |
| 91 | + matches!( | |
| 92 | + part.as_os_str().to_str(), | |
| 93 | + Some("dist" | "node_modules" | ".astro" | ".git") | |
| 94 | + ) | |
| 95 | + }) | |
| 96 | + }) | |
| 97 | +} | |
| 98 | + | |
| 99 | +pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse { | |
| 100 | + let reload_rx = state.reload_tx.subscribe(); | |
| 101 | + ws.on_upgrade(move |socket| handle_socket(socket, reload_rx)) | |
| 102 | +} | |
| 103 | + | |
| 104 | +async fn handle_socket(mut socket: WebSocket, mut reload_rx: broadcast::Receiver<()>) { | |
| 105 | + loop { | |
| 106 | + tokio::select! { | |
| 107 | + message = socket.recv() => match message { | |
| 108 | + Some(Ok(Message::Ping(payload))) => { | |
| 109 | + if socket.send(Message::Pong(payload)).await.is_err() { break; } | |
| 110 | + } | |
| 111 | + Some(Ok(Message::Close(_))) | Some(Err(_)) | None => break, | |
| 112 | + _ => {} | |
| 113 | + }, | |
| 114 | + reload = reload_rx.recv() => match reload { | |
| 115 | + Ok(()) | Err(broadcast::error::RecvError::Lagged(_)) => { | |
| 116 | + if socket.send(Message::Text("reload".into())).await.is_err() { break; } | |
| 117 | + } | |
| 118 | + Err(broadcast::error::RecvError::Closed) => break, | |
| 119 | + } | |
| 120 | + } | |
| 121 | + } | |
| 122 | +} |
+134-0backend/src/error.rs
| @@ -0,0 +1,134 @@ | ||
| 1 | +//! Error types. `AppError` renders an HTML page for the web UI; `ApiError` | |
| 2 | +//! renders JSON for `/api/v1`. The git, registry and LFS protocols have their | |
| 3 | +//! own wire formats and convert from these where they need to. | |
| 4 | + | |
| 5 | +use axum::{ | |
| 6 | + Json, | |
| 7 | + http::{HeaderValue, StatusCode, header}, | |
| 8 | + response::{IntoResponse, Response}, | |
| 9 | +}; | |
| 10 | +use serde_json::json; | |
| 11 | + | |
| 12 | +#[derive(Debug)] | |
| 13 | +pub enum AppError { | |
| 14 | + NotFound, | |
| 15 | + /// Not signed in: the web UI redirects to /login, APIs answer 401. | |
| 16 | + Unauthorized, | |
| 17 | + Forbidden(String), | |
| 18 | + BadRequest(String), | |
| 19 | + Conflict(String), | |
| 20 | + TooLarge(String), | |
| 21 | + Internal(anyhow::Error), | |
| 22 | +} | |
| 23 | + | |
| 24 | +pub type AppResult<T> = Result<T, AppError>; | |
| 25 | + | |
| 26 | +impl AppError { | |
| 27 | + pub fn bad(message: impl Into<String>) -> Self { | |
| 28 | + Self::BadRequest(message.into()) | |
| 29 | + } | |
| 30 | + | |
| 31 | + pub fn forbidden(message: impl Into<String>) -> Self { | |
| 32 | + Self::Forbidden(message.into()) | |
| 33 | + } | |
| 34 | + | |
| 35 | + pub fn conflict(message: impl Into<String>) -> Self { | |
| 36 | + Self::Conflict(message.into()) | |
| 37 | + } | |
| 38 | + | |
| 39 | + pub fn status(&self) -> StatusCode { | |
| 40 | + match self { | |
| 41 | + Self::NotFound => StatusCode::NOT_FOUND, | |
| 42 | + Self::Unauthorized => StatusCode::UNAUTHORIZED, | |
| 43 | + Self::Forbidden(_) => StatusCode::FORBIDDEN, | |
| 44 | + Self::BadRequest(_) => StatusCode::BAD_REQUEST, | |
| 45 | + Self::Conflict(_) => StatusCode::CONFLICT, | |
| 46 | + Self::TooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, | |
| 47 | + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, | |
| 48 | + } | |
| 49 | + } | |
| 50 | + | |
| 51 | + /// What it is safe to show the person. Internal details only go to logs. | |
| 52 | + pub fn public_message(&self) -> String { | |
| 53 | + match self { | |
| 54 | + Self::NotFound => "Not found".into(), | |
| 55 | + Self::Unauthorized => "Sign in required".into(), | |
| 56 | + Self::Forbidden(m) | Self::BadRequest(m) | Self::Conflict(m) | Self::TooLarge(m) => m.clone(), | |
| 57 | + Self::Internal(_) => "Something went wrong on our side. It has been logged.".into(), | |
| 58 | + } | |
| 59 | + } | |
| 60 | + | |
| 61 | + fn log(&self) { | |
| 62 | + if let Self::Internal(error) = self { | |
| 63 | + tracing::error!(error = ?error, "internal error"); | |
| 64 | + } | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +impl std::fmt::Display for AppError { | |
| 69 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 70 | + match self { | |
| 71 | + Self::Internal(e) => write!(f, "{e:#}"), | |
| 72 | + other => f.write_str(&other.public_message()), | |
| 73 | + } | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +impl From<anyhow::Error> for AppError { | |
| 78 | + fn from(error: anyhow::Error) -> Self { | |
| 79 | + Self::Internal(error) | |
| 80 | + } | |
| 81 | +} | |
| 82 | + | |
| 83 | +impl From<sqlx::Error> for AppError { | |
| 84 | + fn from(error: sqlx::Error) -> Self { | |
| 85 | + match error { | |
| 86 | + sqlx::Error::RowNotFound => Self::NotFound, | |
| 87 | + other => Self::Internal(other.into()), | |
| 88 | + } | |
| 89 | + } | |
| 90 | +} | |
| 91 | + | |
| 92 | +impl From<std::io::Error> for AppError { | |
| 93 | + fn from(error: std::io::Error) -> Self { | |
| 94 | + Self::Internal(error.into()) | |
| 95 | + } | |
| 96 | +} | |
| 97 | + | |
| 98 | +impl IntoResponse for AppError { | |
| 99 | + fn into_response(self) -> Response { | |
| 100 | + self.log(); | |
| 101 | + if matches!(self, Self::Unauthorized) { | |
| 102 | + // Pages that need a session send people to sign in. The original | |
| 103 | + // path is attached by the extractor that raised this, when known. | |
| 104 | + return (StatusCode::SEE_OTHER, [(header::LOCATION, HeaderValue::from_static("/login"))]).into_response(); | |
| 105 | + } | |
| 106 | + crate::web::layout::error_page(self.status(), &self.public_message()) | |
| 107 | + } | |
| 108 | +} | |
| 109 | + | |
| 110 | +/// JSON errors for the CLI-facing API: `{"error": "...", "status": 404}`. | |
| 111 | +#[derive(Debug)] | |
| 112 | +pub struct ApiError(pub AppError); | |
| 113 | + | |
| 114 | +pub type ApiResult<T> = Result<T, ApiError>; | |
| 115 | + | |
| 116 | +impl<E: Into<AppError>> From<E> for ApiError { | |
| 117 | + fn from(error: E) -> Self { | |
| 118 | + Self(error.into()) | |
| 119 | + } | |
| 120 | +} | |
| 121 | + | |
| 122 | +impl IntoResponse for ApiError { | |
| 123 | + fn into_response(self) -> Response { | |
| 124 | + self.0.log(); | |
| 125 | + let status = self.0.status(); | |
| 126 | + let mut response = (status, Json(json!({ "error": self.0.public_message(), "status": status.as_u16() }))).into_response(); | |
| 127 | + if status == StatusCode::UNAUTHORIZED { | |
| 128 | + response | |
| 129 | + .headers_mut() | |
| 130 | + .insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Bearer realm=\"irongit\"")); | |
| 131 | + } | |
| 132 | + response | |
| 133 | + } | |
| 134 | +} |
+125-0backend/src/frontend.rs
| @@ -0,0 +1,125 @@ | ||
| 1 | +//! Serving the Astro build. | |
| 2 | +//! | |
| 3 | +//! Debug builds read `frontend/dist` from disk, so a rebuild shows up without | |
| 4 | +//! recompiling the server. Release builds embed the same directory through | |
| 5 | +//! `rust-embed`. Both resolve paths identically, so a URL that works locally | |
| 6 | +//! works in production. | |
| 7 | + | |
| 8 | +use axum::{ | |
| 9 | + extract::State, | |
| 10 | + http::{StatusCode, Uri, header}, | |
| 11 | + response::{IntoResponse, Response}, | |
| 12 | +}; | |
| 13 | +use rust_embed::Embed; | |
| 14 | + | |
| 15 | +use crate::{auth::MaybeViewer, state::AppState, web::layout}; | |
| 16 | + | |
| 17 | +#[derive(Embed)] | |
| 18 | +#[folder = "../frontend/dist"] | |
| 19 | +// Only read in release builds, but derived in both so a missing or empty | |
| 20 | +// frontend/dist fails at the first `cargo build`, not at release time. | |
| 21 | +#[cfg_attr(debug_assertions, allow(dead_code))] | |
| 22 | +struct Dist; | |
| 23 | + | |
| 24 | +/// Static files and Astro pages. HTML gets the shared head and the nav for | |
| 25 | +/// whoever is signed in. | |
| 26 | +pub async fn serve(State(state): State<AppState>, MaybeViewer(viewer): MaybeViewer, uri: Uri) -> Response { | |
| 27 | + let path = uri.path().trim_matches('/'); | |
| 28 | + let path = if path.is_empty() { "index.html" } else { path }; | |
| 29 | + // The app shell is a template, not a page. | |
| 30 | + let found = if path.starts_with("app-shell") { None } else { load(path).await }; | |
| 31 | + | |
| 32 | + let (status, body, served) = match found { | |
| 33 | + Some((body, served)) => (StatusCode::OK, body, served), | |
| 34 | + None => match load("404.html").await { | |
| 35 | + Some((body, served)) => (StatusCode::NOT_FOUND, body, served), | |
| 36 | + None => return (StatusCode::NOT_FOUND, "Not Found").into_response(), | |
| 37 | + }, | |
| 38 | + }; | |
| 39 | + if served.ends_with(".html") { | |
| 40 | + let html = String::from_utf8_lossy(&body); | |
| 41 | + let filled = layout::inject(&state, viewer.as_ref(), uri.path(), &html); | |
| 42 | + let mut response = respond(status, filled.into_bytes(), &served); | |
| 43 | + response.headers_mut().insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-store")); | |
| 44 | + return response; | |
| 45 | + } | |
| 46 | + respond(status, body, &served) | |
| 47 | +} | |
| 48 | + | |
| 49 | +/// The Astro-built page template the Rust-rendered pages are poured into. | |
| 50 | +/// Read from disk on every call in debug builds so frontend edits show up. | |
| 51 | +pub fn shell() -> Option<String> { | |
| 52 | + #[cfg(debug_assertions)] | |
| 53 | + { | |
| 54 | + std::fs::read_to_string(dist_dir().join("app-shell/index.html")).ok() | |
| 55 | + } | |
| 56 | + #[cfg(not(debug_assertions))] | |
| 57 | + { | |
| 58 | + static SHELL: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new(); | |
| 59 | + SHELL | |
| 60 | + .get_or_init(|| Dist::get("app-shell/index.html").map(|f| String::from_utf8_lossy(&f.data).into_owned())) | |
| 61 | + .clone() | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +/// Tries the path itself, then `.html`, then `/index.html`, so `/about`, | |
| 66 | +/// `/about/` and `/about.html` all reach `dist/about/index.html`. | |
| 67 | +async fn load(path: &str) -> Option<(Vec<u8>, String)> { | |
| 68 | + for candidate in [ | |
| 69 | + path.to_string(), | |
| 70 | + format!("{path}.html"), | |
| 71 | + format!("{path}/index.html"), | |
| 72 | + ] { | |
| 73 | + if let Some(body) = read(&candidate).await { | |
| 74 | + return Some((body, candidate)); | |
| 75 | + } | |
| 76 | + } | |
| 77 | + None | |
| 78 | +} | |
| 79 | + | |
| 80 | +#[cfg(debug_assertions)] | |
| 81 | +async fn read(path: &str) -> Option<Vec<u8>> { | |
| 82 | + use std::path::{Component, Path}; | |
| 83 | + | |
| 84 | + // Nothing may escape dist/ (`..`, absolute paths, Windows prefixes). | |
| 85 | + let relative = Path::new(path); | |
| 86 | + if relative | |
| 87 | + .components() | |
| 88 | + .any(|part| !matches!(part, Component::Normal(_))) | |
| 89 | + { | |
| 90 | + return None; | |
| 91 | + } | |
| 92 | + tokio::fs::read(dist_dir().join(relative)).await.ok() | |
| 93 | +} | |
| 94 | + | |
| 95 | +#[cfg(not(debug_assertions))] | |
| 96 | +async fn read(path: &str) -> Option<Vec<u8>> { | |
| 97 | + Dist::get(path).map(|file| file.data.into_owned()) | |
| 98 | +} | |
| 99 | + | |
| 100 | +#[cfg(debug_assertions)] | |
| 101 | +pub fn dist_dir() -> std::path::PathBuf { | |
| 102 | + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../frontend/dist") | |
| 103 | +} | |
| 104 | + | |
| 105 | +fn respond(status: StatusCode, body: Vec<u8>, path: &str) -> Response { | |
| 106 | + let mime = mime_guess::from_path(path).first_or_octet_stream(); | |
| 107 | + // Never cache while developing; the browser reloads on every rebuild. | |
| 108 | + // Astro fingerprints everything under _astro/, so it can be cached forever. | |
| 109 | + let cache = if cfg!(debug_assertions) { | |
| 110 | + "no-store" | |
| 111 | + } else if path.starts_with("_astro/") { | |
| 112 | + "public, max-age=31536000, immutable" | |
| 113 | + } else { | |
| 114 | + "public, max-age=3600" | |
| 115 | + }; | |
| 116 | + ( | |
| 117 | + status, | |
| 118 | + [ | |
| 119 | + (header::CONTENT_TYPE, mime.as_ref()), | |
| 120 | + (header::CACHE_CONTROL, cache), | |
| 121 | + ], | |
| 122 | + body, | |
| 123 | + ) | |
| 124 | + .into_response() | |
| 125 | +} |
+452-0backend/src/git.rs
| @@ -0,0 +1,452 @@ | ||
| 1 | +//! Reading and managing bare repositories through the system `git`. | |
| 2 | +//! | |
| 3 | +//! The git binary is the reference implementation of every edge case (pack | |
| 4 | +//! formats, protocol v2, fsck), so transport and browsing both go through it. | |
| 5 | +//! Every call passes revisions after `--end-of-options` and paths after `--`, | |
| 6 | +//! so user-controlled names can never be read as flags. | |
| 7 | + | |
| 8 | +use std::{ | |
| 9 | + path::{Path, PathBuf}, | |
| 10 | + process::Stdio, | |
| 11 | +}; | |
| 12 | + | |
| 13 | +use anyhow::{Context, bail}; | |
| 14 | +use chrono::{DateTime, TimeZone, Utc}; | |
| 15 | +use tokio::process::Command; | |
| 16 | + | |
| 17 | +pub const ZERO_SHA: &str = "0000000000000000000000000000000000000000"; | |
| 18 | + | |
| 19 | +/// A `git` command isolated from the host's user and system config. | |
| 20 | +pub fn git_command(git_dir: &Path) -> Command { | |
| 21 | + let mut command = Command::new("git"); | |
| 22 | + command | |
| 23 | + .arg("--git-dir") | |
| 24 | + .arg(git_dir) | |
| 25 | + .env("GIT_CONFIG_NOSYSTEM", "1") | |
| 26 | + .env("GIT_CONFIG_GLOBAL", "/dev/null") | |
| 27 | + .env("GIT_TERMINAL_PROMPT", "0") | |
| 28 | + .env("LC_ALL", "C") | |
| 29 | + .env_remove("GIT_DIR") | |
| 30 | + .env_remove("GIT_WORK_TREE") | |
| 31 | + .stdin(Stdio::null()) | |
| 32 | + .kill_on_drop(true); | |
| 33 | + command | |
| 34 | +} | |
| 35 | + | |
| 36 | +#[derive(Clone, Debug)] | |
| 37 | +pub struct Git { | |
| 38 | + pub path: PathBuf, | |
| 39 | +} | |
| 40 | + | |
| 41 | +#[derive(Clone, Debug)] | |
| 42 | +pub struct RefInfo { | |
| 43 | + /// Full name, e.g. "refs/heads/main". | |
| 44 | + pub name: String, | |
| 45 | + pub sha: String, | |
| 46 | + /// Peeled commit for annotated tags, otherwise the same as `sha`. | |
| 47 | + pub commit: String, | |
| 48 | + pub committed_at: Option<DateTime<Utc>>, | |
| 49 | + pub subject: String, | |
| 50 | +} | |
| 51 | + | |
| 52 | +impl RefInfo { | |
| 53 | + pub fn short_name(&self) -> &str { | |
| 54 | + self.name | |
| 55 | + .strip_prefix("refs/heads/") | |
| 56 | + .or_else(|| self.name.strip_prefix("refs/tags/")) | |
| 57 | + .unwrap_or(&self.name) | |
| 58 | + } | |
| 59 | + | |
| 60 | + pub fn is_branch(&self) -> bool { | |
| 61 | + self.name.starts_with("refs/heads/") | |
| 62 | + } | |
| 63 | + | |
| 64 | + pub fn is_tag(&self) -> bool { | |
| 65 | + self.name.starts_with("refs/tags/") | |
| 66 | + } | |
| 67 | +} | |
| 68 | + | |
| 69 | +#[derive(Clone, Debug)] | |
| 70 | +pub struct TreeEntry { | |
| 71 | + pub mode: String, | |
| 72 | + /// "blob", "tree" or "commit" (a submodule). | |
| 73 | + pub kind: String, | |
| 74 | + pub sha: String, | |
| 75 | + pub size: Option<u64>, | |
| 76 | + pub name: String, | |
| 77 | +} | |
| 78 | + | |
| 79 | +impl TreeEntry { | |
| 80 | + pub fn is_dir(&self) -> bool { | |
| 81 | + self.kind == "tree" | |
| 82 | + } | |
| 83 | + | |
| 84 | + pub fn is_symlink(&self) -> bool { | |
| 85 | + self.mode == "120000" | |
| 86 | + } | |
| 87 | +} | |
| 88 | + | |
| 89 | +#[derive(Clone, Debug)] | |
| 90 | +pub struct Commit { | |
| 91 | + pub sha: String, | |
| 92 | + pub parents: Vec<String>, | |
| 93 | + pub author_name: String, | |
| 94 | + pub author_email: String, | |
| 95 | + pub authored_at: DateTime<Utc>, | |
| 96 | + pub committer_name: String, | |
| 97 | + pub committed_at: DateTime<Utc>, | |
| 98 | + pub subject: String, | |
| 99 | + pub body: String, | |
| 100 | +} | |
| 101 | + | |
| 102 | +impl Commit { | |
| 103 | + pub fn short_sha(&self) -> &str { | |
| 104 | + &self.sha[..self.sha.len().min(8)] | |
| 105 | + } | |
| 106 | +} | |
| 107 | + | |
| 108 | +const COMMIT_FORMAT: &str = "%H%x00%P%x00%an%x00%ae%x00%at%x00%cn%x00%ct%x00%s%x00%b%x1e"; | |
| 109 | + | |
| 110 | +fn parse_commits(output: &[u8]) -> Vec<Commit> { | |
| 111 | + String::from_utf8_lossy(output) | |
| 112 | + .split('\x1e') | |
| 113 | + .filter_map(|record| { | |
| 114 | + let record = record.trim_start_matches('\n'); | |
| 115 | + if record.is_empty() { | |
| 116 | + return None; | |
| 117 | + } | |
| 118 | + let mut f = record.splitn(9, '\0'); | |
| 119 | + let sha = f.next()?.to_string(); | |
| 120 | + let parents = f.next()?.split_whitespace().map(String::from).collect(); | |
| 121 | + let author_name = f.next()?.to_string(); | |
| 122 | + let author_email = f.next()?.to_string(); | |
| 123 | + let authored_at = timestamp(f.next()?); | |
| 124 | + let committer_name = f.next()?.to_string(); | |
| 125 | + let committed_at = timestamp(f.next()?); | |
| 126 | + let subject = f.next()?.to_string(); | |
| 127 | + let body = f.next().unwrap_or("").trim_end().to_string(); | |
| 128 | + Some(Commit { sha, parents, author_name, author_email, authored_at, committer_name, committed_at, subject, body }) | |
| 129 | + }) | |
| 130 | + .collect() | |
| 131 | +} | |
| 132 | + | |
| 133 | +fn timestamp(s: &str) -> DateTime<Utc> { | |
| 134 | + s.trim().parse::<i64>().ok().and_then(|t| Utc.timestamp_opt(t, 0).single()).unwrap_or_default() | |
| 135 | +} | |
| 136 | + | |
| 137 | +/// Accepts names a human could type as a ref: no option-looking or control | |
| 138 | +/// characters. Git's own check-ref-format runs on the result anyway. | |
| 139 | +pub fn plausible_ref(name: &str) -> bool { | |
| 140 | + !name.is_empty() | |
| 141 | + && name.len() <= 255 | |
| 142 | + && !name.starts_with('-') | |
| 143 | + && !name.contains("..") | |
| 144 | + && !name.bytes().any(|b| b < 0x20 || b == 0x7f || b" ~^:?*[\\".contains(&b)) | |
| 145 | +} | |
| 146 | + | |
| 147 | +pub fn is_hex_sha(s: &str) -> bool { | |
| 148 | + (4..=64).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_hexdigit()) | |
| 149 | +} | |
| 150 | + | |
| 151 | +impl Git { | |
| 152 | + pub fn new(path: impl Into<PathBuf>) -> Self { | |
| 153 | + Self { path: path.into() } | |
| 154 | + } | |
| 155 | + | |
| 156 | + pub fn command(&self) -> Command { | |
| 157 | + git_command(&self.path) | |
| 158 | + } | |
| 159 | + | |
| 160 | + /// Creates a bare repository with server-side defaults. | |
| 161 | + pub async fn init_bare(path: &Path, default_branch: &str) -> anyhow::Result<Self> { | |
| 162 | + if let Some(parent) = path.parent() { | |
| 163 | + tokio::fs::create_dir_all(parent).await?; | |
| 164 | + } | |
| 165 | + let output = Command::new("git") | |
| 166 | + .env("GIT_CONFIG_NOSYSTEM", "1") | |
| 167 | + .env("GIT_CONFIG_GLOBAL", "/dev/null") | |
| 168 | + .args(["init", "--bare", "--quiet"]) | |
| 169 | + .arg(format!("--initial-branch={default_branch}")) | |
| 170 | + .arg(path) | |
| 171 | + .output() | |
| 172 | + .await?; | |
| 173 | + if !output.status.success() { | |
| 174 | + bail!("git init failed: {}", String::from_utf8_lossy(&output.stderr)); | |
| 175 | + } | |
| 176 | + let git = Self::new(path); | |
| 177 | + for (key, value) in [ | |
| 178 | + ("core.logAllRefUpdates", "false"), | |
| 179 | + ("receive.denyNonFastForwards", "false"), | |
| 180 | + ("receive.denyDeleteCurrent", "warn"), | |
| 181 | + ("uploadpack.allowFilter", "true"), | |
| 182 | + ("uploadpack.allowReachableSHA1InWant", "true"), | |
| 183 | + ("gc.auto", "6700"), | |
| 184 | + ] { | |
| 185 | + git.run(&["config", key, value]).await?; | |
| 186 | + } | |
| 187 | + // Sample hooks are noise; ours come from core.hooksPath at push time. | |
| 188 | + let _ = tokio::fs::remove_dir_all(path.join("hooks")).await; | |
| 189 | + let _ = tokio::fs::write(path.join("description"), "").await; | |
| 190 | + Ok(git) | |
| 191 | + } | |
| 192 | + | |
| 193 | + /// Runs git and returns stdout, failing with stderr on a non-zero exit. | |
| 194 | + pub async fn run(&self, args: &[&str]) -> anyhow::Result<Vec<u8>> { | |
| 195 | + let output = self.command().args(args).output().await.context("could not run git")?; | |
| 196 | + if !output.status.success() { | |
| 197 | + bail!( | |
| 198 | + "git {} failed: {}", | |
| 199 | + args.first().unwrap_or(&""), | |
| 200 | + String::from_utf8_lossy(&output.stderr).trim() | |
| 201 | + ); | |
| 202 | + } | |
| 203 | + Ok(output.stdout) | |
| 204 | + } | |
| 205 | + | |
| 206 | + /// Like `run`, but a non-zero exit is `None` (missing ref, path, ...). | |
| 207 | + pub async fn run_opt(&self, args: &[&str]) -> anyhow::Result<Option<Vec<u8>>> { | |
| 208 | + let output = self.command().args(args).output().await.context("could not run git")?; | |
| 209 | + Ok(output.status.success().then_some(output.stdout)) | |
| 210 | + } | |
| 211 | + | |
| 212 | + pub async fn refs(&self) -> anyhow::Result<Vec<RefInfo>> { | |
| 213 | + let out = self | |
| 214 | + .run(&[ | |
| 215 | + "for-each-ref", | |
| 216 | + "--sort=-committerdate", | |
| 217 | + "--format=%(refname)%00%(objectname)%00%(*objectname)%00%(committerdate:unix)%00%(*committerdate:unix)%00%(contents:subject)", | |
| 218 | + "refs/heads/", | |
| 219 | + "refs/tags/", | |
| 220 | + ]) | |
| 221 | + .await?; | |
| 222 | + Ok(String::from_utf8_lossy(&out) | |
| 223 | + .lines() | |
| 224 | + .filter_map(|line| { | |
| 225 | + let mut f = line.splitn(6, '\0'); | |
| 226 | + let name = f.next()?.to_string(); | |
| 227 | + let sha = f.next()?.to_string(); | |
| 228 | + let peeled = f.next()?.to_string(); | |
| 229 | + let date = f.next()?; | |
| 230 | + let peeled_date = f.next()?; | |
| 231 | + let subject = f.next().unwrap_or("").to_string(); | |
| 232 | + let commit = if peeled.is_empty() { sha.clone() } else { peeled }; | |
| 233 | + let date = if date.is_empty() { peeled_date } else { date }; | |
| 234 | + let committed_at = date.parse::<i64>().ok().and_then(|t| Utc.timestamp_opt(t, 0).single()); | |
| 235 | + Some(RefInfo { name, sha, commit, committed_at, subject }) | |
| 236 | + }) | |
| 237 | + .collect()) | |
| 238 | + } | |
| 239 | + | |
| 240 | + /// Map of ref name to sha for every ref, used to diff before/after a push. | |
| 241 | + pub async fn ref_snapshot(&self) -> anyhow::Result<std::collections::BTreeMap<String, String>> { | |
| 242 | + let out = self.run(&["for-each-ref", "--format=%(refname) %(objectname)"]).await?; | |
| 243 | + Ok(String::from_utf8_lossy(&out) | |
| 244 | + .lines() | |
| 245 | + .filter_map(|l| l.split_once(' ').map(|(n, s)| (n.to_string(), s.to_string()))) | |
| 246 | + .collect()) | |
| 247 | + } | |
| 248 | + | |
| 249 | + pub async fn has_commits(&self) -> anyhow::Result<bool> { | |
| 250 | + Ok(!self.run(&["for-each-ref", "--count=1", "refs/heads/"]).await?.is_empty()) | |
| 251 | + } | |
| 252 | + | |
| 253 | + /// Full commit sha for a branch, tag or (abbreviated) sha. | |
| 254 | + pub async fn resolve_commit(&self, rev: &str) -> anyhow::Result<Option<String>> { | |
| 255 | + if !plausible_ref(rev) { | |
| 256 | + return Ok(None); | |
| 257 | + } | |
| 258 | + let spec = format!("{rev}^{{commit}}"); | |
| 259 | + Ok(self | |
| 260 | + .run_opt(&["rev-parse", "--verify", "--quiet", "--end-of-options", &spec]) | |
| 261 | + .await? | |
| 262 | + .map(|out| String::from_utf8_lossy(&out).trim().to_string()) | |
| 263 | + .filter(|s| !s.is_empty())) | |
| 264 | + } | |
| 265 | + | |
| 266 | + /// Entries of the directory at `path` ("" for the root) in `commit`. | |
| 267 | + pub async fn ls_tree(&self, commit: &str, path: &str) -> anyhow::Result<Option<Vec<TreeEntry>>> { | |
| 268 | + let spec = if path.is_empty() { format!("{commit}^{{tree}}") } else { format!("{commit}:{path}") }; | |
| 269 | + let Some(out) = self.run_opt(&["ls-tree", "-l", "-z", "--end-of-options", &spec]).await? else { | |
| 270 | + return Ok(None); | |
| 271 | + }; | |
| 272 | + let mut entries: Vec<TreeEntry> = out | |
| 273 | + .split(|b| *b == 0) | |
| 274 | + .filter(|chunk| !chunk.is_empty()) | |
| 275 | + .filter_map(|chunk| { | |
| 276 | + let line = String::from_utf8_lossy(chunk); | |
| 277 | + let (meta, name) = line.split_once('\t')?; | |
| 278 | + let mut f = meta.split_whitespace(); | |
| 279 | + let mode = f.next()?.to_string(); | |
| 280 | + let kind = f.next()?.to_string(); | |
| 281 | + let sha = f.next()?.to_string(); | |
| 282 | + let size = f.next().and_then(|s| s.parse().ok()); | |
| 283 | + Some(TreeEntry { mode, kind, sha, size, name: name.to_string() }) | |
| 284 | + }) | |
| 285 | + .collect(); | |
| 286 | + // Directories first, then files, each alphabetical. | |
| 287 | + entries.sort_by(|a, b| b.is_dir().cmp(&a.is_dir()).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))); | |
| 288 | + Ok(Some(entries)) | |
| 289 | + } | |
| 290 | + | |
| 291 | + /// The object at `path` in `commit`: (kind, sha, size). | |
| 292 | + pub async fn object_at(&self, commit: &str, path: &str) -> anyhow::Result<Option<(String, String, u64)>> { | |
| 293 | + if path.is_empty() { | |
| 294 | + return Ok(None); | |
| 295 | + } | |
| 296 | + // Specs go over stdin, so a path can never be parsed as an option. | |
| 297 | + self.batch_check(&format!("{commit}:{path}")).await | |
| 298 | + } | |
| 299 | + | |
| 300 | + async fn batch_check(&self, spec: &str) -> anyhow::Result<Option<(String, String, u64)>> { | |
| 301 | + use tokio::io::AsyncWriteExt; | |
| 302 | + let mut child = self | |
| 303 | + .command() | |
| 304 | + .args(["cat-file", "--batch-check=%(objecttype) %(objectname) %(objectsize)"]) | |
| 305 | + .stdin(Stdio::piped()) | |
| 306 | + .stdout(Stdio::piped()) | |
| 307 | + .stderr(Stdio::null()) | |
| 308 | + .spawn()?; | |
| 309 | + let mut stdin = child.stdin.take().context("no stdin")?; | |
| 310 | + stdin.write_all(spec.as_bytes()).await?; | |
| 311 | + stdin.write_all(b"\n").await?; | |
| 312 | + drop(stdin); | |
| 313 | + let output = child.wait_with_output().await?; | |
| 314 | + let line = String::from_utf8_lossy(&output.stdout); | |
| 315 | + let mut f = line.split_whitespace(); | |
| 316 | + match (f.next(), f.next(), f.next()) { | |
| 317 | + (Some(kind), Some(sha), Some(size)) if kind != "missing" && kind != "ambiguous" => { | |
| 318 | + Ok(Some((kind.to_string(), sha.to_string(), size.parse().unwrap_or(0)))) | |
| 319 | + } | |
| 320 | + _ => Ok(None), | |
| 321 | + } | |
| 322 | + } | |
| 323 | + | |
| 324 | + /// Raw blob contents by sha. | |
| 325 | + pub async fn read_blob(&self, sha: &str) -> anyhow::Result<Vec<u8>> { | |
| 326 | + anyhow::ensure!(is_hex_sha(sha), "not a sha"); | |
| 327 | + self.run(&["cat-file", "blob", sha]).await | |
| 328 | + } | |
| 329 | + | |
| 330 | + /// Blob contents streamed from git, for raw downloads. | |
| 331 | + pub fn blob_stream(&self, sha: &str) -> anyhow::Result<tokio::process::Child> { | |
| 332 | + anyhow::ensure!(is_hex_sha(sha), "not a sha"); | |
| 333 | + Ok(self.command().args(["cat-file", "blob", sha]).stdout(Stdio::piped()).stderr(Stdio::null()).spawn()?) | |
| 334 | + } | |
| 335 | + | |
| 336 | + /// History of `rev`, optionally limited to `path`, newest first. | |
| 337 | + pub async fn log(&self, rev: &str, path: Option<&str>, skip: usize, limit: usize) -> anyhow::Result<Vec<Commit>> { | |
| 338 | + let format = format!("--format={COMMIT_FORMAT}"); | |
| 339 | + let skip = format!("--skip={skip}"); | |
| 340 | + let limit = format!("--max-count={limit}"); | |
| 341 | + let mut args = vec!["log", &format, &skip, &limit, "--end-of-options", rev, "--"]; | |
| 342 | + if let Some(path) = path.filter(|p| !p.is_empty()) { | |
| 343 | + args.push(path); | |
| 344 | + } | |
| 345 | + Ok(self.run_opt(&args).await?.map(|out| parse_commits(&out)).unwrap_or_default()) | |
| 346 | + } | |
| 347 | + | |
| 348 | + pub async fn commit(&self, rev: &str) -> anyhow::Result<Option<Commit>> { | |
| 349 | + Ok(self.log(rev, None, 0, 1).await?.into_iter().next()) | |
| 350 | + } | |
| 351 | + | |
| 352 | + /// Commits in `new` that are not in `old` (or all of `new` when `old` | |
| 353 | + /// is the zero sha), newest first, capped at `limit`. | |
| 354 | + pub async fn new_commits(&self, old: &str, new: &str, limit: usize) -> anyhow::Result<Vec<Commit>> { | |
| 355 | + let format = format!("--format={COMMIT_FORMAT}"); | |
| 356 | + let limit = format!("--max-count={limit}"); | |
| 357 | + let exclude = format!("^{old}"); | |
| 358 | + let mut args = vec!["log", &format, &limit, "--end-of-options", new]; | |
| 359 | + if old != ZERO_SHA { | |
| 360 | + args.push(&exclude); | |
| 361 | + } | |
| 362 | + args.push("--"); | |
| 363 | + Ok(parse_commits(&self.run(&args).await?)) | |
| 364 | + } | |
| 365 | + | |
| 366 | + pub async fn count_commits(&self, rev: &str) -> anyhow::Result<u64> { | |
| 367 | + let out = self.run_opt(&["rev-list", "--count", "--end-of-options", rev, "--"]).await?; | |
| 368 | + Ok(out.and_then(|o| String::from_utf8_lossy(&o).trim().parse().ok()).unwrap_or(0)) | |
| 369 | + } | |
| 370 | + | |
| 371 | + /// The patch a commit introduces (against its first parent), truncated. | |
| 372 | + pub async fn commit_patch(&self, sha: &str, max_bytes: usize) -> anyhow::Result<(String, bool)> { | |
| 373 | + anyhow::ensure!(is_hex_sha(sha), "not a sha"); | |
| 374 | + let out = self | |
| 375 | + .run(&["show", "--format=", "--patch", "--find-renames", "--no-color", "--no-ext-diff", "--first-parent", "--end-of-options", sha]) | |
| 376 | + .await?; | |
| 377 | + let truncated = out.len() > max_bytes; | |
| 378 | + let slice = &out[..out.len().min(max_bytes)]; | |
| 379 | + Ok((String::from_utf8_lossy(slice).into_owned(), truncated)) | |
| 380 | + } | |
| 381 | + | |
| 382 | + /// Files changed by a commit: (status, path, additions, deletions). | |
| 383 | + pub async fn commit_stats(&self, sha: &str) -> anyhow::Result<Vec<(String, String, Option<u64>, Option<u64>)>> { | |
| 384 | + anyhow::ensure!(is_hex_sha(sha), "not a sha"); | |
| 385 | + let numstat = self.run(&["show", "--format=", "--numstat", "--find-renames", "--first-parent", "-z", "--end-of-options", sha]).await?; | |
| 386 | + let status = self.run(&["show", "--format=", "--name-status", "--find-renames", "--first-parent", "--end-of-options", sha]).await?; | |
| 387 | + let statuses: Vec<String> = String::from_utf8_lossy(&status).lines().map(|l| l.chars().next().unwrap_or('M').to_string()).collect(); | |
| 388 | + let mut out = Vec::new(); | |
| 389 | + let text = String::from_utf8_lossy(&numstat).into_owned(); | |
| 390 | + let mut parts = text.split('\0').filter(|s| !s.is_empty()).peekable(); | |
| 391 | + let mut index = 0; | |
| 392 | + while let Some(record) = parts.next() { | |
| 393 | + let mut f = record.splitn(3, '\t'); | |
| 394 | + let adds = f.next().and_then(|v| v.parse().ok()); | |
| 395 | + let dels = f.next().and_then(|v| v.parse().ok()); | |
| 396 | + let mut path = f.next().unwrap_or("").to_string(); | |
| 397 | + if path.is_empty() { | |
| 398 | + // Rename: "\0old\0new" follows. | |
| 399 | + let _old = parts.next(); | |
| 400 | + path = parts.next().unwrap_or("").to_string(); | |
| 401 | + } | |
| 402 | + out.push((statuses.get(index).cloned().unwrap_or_else(|| "M".into()), path, adds, dels)); | |
| 403 | + index += 1; | |
| 404 | + } | |
| 405 | + Ok(out) | |
| 406 | + } | |
| 407 | + | |
| 408 | + pub async fn set_head(&self, branch: &str) -> anyhow::Result<()> { | |
| 409 | + anyhow::ensure!(plausible_ref(branch), "invalid branch name"); | |
| 410 | + self.run(&["symbolic-ref", "HEAD", &format!("refs/heads/{branch}")]).await?; | |
| 411 | + Ok(()) | |
| 412 | + } | |
| 413 | + | |
| 414 | + /// Bytes on disk, from `count-objects` (loose + packed). | |
| 415 | + pub async fn disk_size(&self) -> anyhow::Result<u64> { | |
| 416 | + let out = self.run(&["count-objects", "-v"]).await?; | |
| 417 | + let mut kib = 0u64; | |
| 418 | + for line in String::from_utf8_lossy(&out).lines() { | |
| 419 | + if let Some((key, value)) = line.split_once(": ") { | |
| 420 | + if matches!(key, "size" | "size-pack" | "size-garbage") { | |
| 421 | + kib += value.trim().parse::<u64>().unwrap_or(0); | |
| 422 | + } | |
| 423 | + } | |
| 424 | + } | |
| 425 | + Ok(kib * 1024) | |
| 426 | + } | |
| 427 | +} | |
| 428 | + | |
| 429 | +#[cfg(test)] | |
| 430 | +mod tests { | |
| 431 | + use super::*; | |
| 432 | + | |
| 433 | + #[test] | |
| 434 | + fn rejects_flag_like_refs() { | |
| 435 | + assert!(plausible_ref("main")); | |
| 436 | + assert!(plausible_ref("feature/x")); | |
| 437 | + assert!(!plausible_ref("--upload-pack=evil")); | |
| 438 | + assert!(!plausible_ref("a..b")); | |
| 439 | + assert!(!plausible_ref("a b")); | |
| 440 | + } | |
| 441 | + | |
| 442 | + #[test] | |
| 443 | + fn parses_commit_records() { | |
| 444 | + let raw = b"abc\0p1 p2\0Ann\0ann@x.io\01700000000\0Bob\01700000100\0Subject line\0Body text\n\x1e"; | |
| 445 | + let commits = parse_commits(raw); | |
| 446 | + assert_eq!(commits.len(), 1); | |
| 447 | + assert_eq!(commits[0].parents, vec!["p1", "p2"]); | |
| 448 | + assert_eq!(commits[0].author_email, "ann@x.io"); | |
| 449 | + assert_eq!(commits[0].subject, "Subject line"); | |
| 450 | + assert_eq!(commits[0].body, "Body text"); | |
| 451 | + } | |
| 452 | +} |
+243-0backend/src/git_http.rs
| @@ -0,0 +1,243 @@ | ||
| 1 | +//! Git smart HTTP (protocol v0, v1 and v2), the same wire protocol as | |
| 2 | +//! `git http-backend`: | |
| 3 | +//! | |
| 4 | +//! GET /{owner}/{repo}/info/refs?service=git-upload-pack|git-receive-pack | |
| 5 | +//! POST /{owner}/{repo}/git-upload-pack | |
| 6 | +//! POST /{owner}/{repo}/git-receive-pack | |
| 7 | +//! | |
| 8 | +//! Request bodies stream into git's stdin and git's stdout streams back, so | |
| 9 | +//! memory stays flat for any repository size. | |
| 10 | + | |
| 11 | +use std::process::Stdio; | |
| 12 | + | |
| 13 | +use async_compression::tokio::bufread::GzipDecoder; | |
| 14 | +use axum::{ | |
| 15 | + Router, | |
| 16 | + body::Body, | |
| 17 | + extract::{Path, Query, Request, State}, | |
| 18 | + http::{HeaderMap, StatusCode, header}, | |
| 19 | + response::{IntoResponse, Response}, | |
| 20 | + routing::{get, post}, | |
| 21 | +}; | |
| 22 | +use futures::TryStreamExt; | |
| 23 | +use serde::Deserialize; | |
| 24 | +use serde_json::json; | |
| 25 | +use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWriteExt, BufReader}; | |
| 26 | +use tokio_util::io::{ReaderStream, StreamReader}; | |
| 27 | + | |
| 28 | +use crate::{ | |
| 29 | + analytics, | |
| 30 | + auth::{self, HeaderAuth, Viewer}, | |
| 31 | + git::Git, | |
| 32 | + models::Repo, | |
| 33 | + perm::{self, Access, Area}, | |
| 34 | + push, | |
| 35 | + state::AppState, | |
| 36 | + transport::{self, Service, ServiceOptions}, | |
| 37 | +}; | |
| 38 | + | |
| 39 | +pub fn router() -> Router<AppState> { | |
| 40 | + Router::new() | |
| 41 | + .route("/{owner}/{repo}/info/refs", get(info_refs)) | |
| 42 | + .route("/{owner}/{repo}/git-upload-pack", post(upload_pack)) | |
| 43 | + .route("/{owner}/{repo}/git-receive-pack", post(receive_pack)) | |
| 44 | +} | |
| 45 | + | |
| 46 | +#[derive(Deserialize)] | |
| 47 | +struct InfoRefsQuery { | |
| 48 | + service: Option<String>, | |
| 49 | +} | |
| 50 | + | |
| 51 | +fn plain(status: StatusCode, message: &str) -> Response { | |
| 52 | + (status, [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], format!("{message}\n")).into_response() | |
| 53 | +} | |
| 54 | + | |
| 55 | +/// Resolves the repo and the caller's access, answering the way git clients | |
| 56 | +/// expect: 401 with a Basic challenge whenever credentials might help, so a | |
| 57 | +/// private repo is indistinguishable from a missing one until you sign in. | |
| 58 | +async fn authorize(state: &AppState, headers: &HeaderMap, owner: &str, name: &str, service: Service) -> Result<(Repo, Option<Viewer>), Response> { | |
| 59 | + let viewer = match auth::authenticate_header(&state.db, headers).await { | |
| 60 | + Ok(HeaderAuth::Viewer(viewer)) => Some(viewer), | |
| 61 | + Ok(HeaderAuth::Anonymous) => None, | |
| 62 | + Ok(HeaderAuth::Invalid) => return Err(auth::basic_challenge("Invalid credentials. Use a personal access token as the password (ig auth token).")), | |
| 63 | + Err(error) => { | |
| 64 | + tracing::error!(?error, "git auth lookup failed"); | |
| 65 | + return Err(plain(StatusCode::INTERNAL_SERVER_ERROR, "internal error")); | |
| 66 | + } | |
| 67 | + }; | |
| 68 | + let repo = match Repo::by_path(&state.db, owner, name).await { | |
| 69 | + Ok(repo) => repo, | |
| 70 | + Err(error) => { | |
| 71 | + tracing::error!(?error, "repo lookup failed"); | |
| 72 | + return Err(plain(StatusCode::INTERNAL_SERVER_ERROR, "internal error")); | |
| 73 | + } | |
| 74 | + }; | |
| 75 | + let access = match &repo { | |
| 76 | + Some(repo) => perm::repo_access(&state.db, viewer.as_ref(), repo, Area::Repo).await.unwrap_or(Access::None), | |
| 77 | + None => Access::None, | |
| 78 | + }; | |
| 79 | + let needed = match service { | |
| 80 | + Service::UploadPack => Access::Read, | |
| 81 | + Service::ReceivePack => Access::Write, | |
| 82 | + }; | |
| 83 | + if access >= needed { | |
| 84 | + return Ok((repo.expect("access implies repo"), viewer)); | |
| 85 | + } | |
| 86 | + if viewer.is_none() { | |
| 87 | + return Err(auth::basic_challenge("Authentication required.")); | |
| 88 | + } | |
| 89 | + match (&repo, access.can_read()) { | |
| 90 | + (Some(repo), true) if repo.archived => Err(plain(StatusCode::FORBIDDEN, "This repository is archived and read-only.")), | |
| 91 | + (Some(_), true) => Err(plain(StatusCode::FORBIDDEN, "You have read access only; pushing needs write access.")), | |
| 92 | + _ => Err(plain(StatusCode::NOT_FOUND, "Repository not found.")), | |
| 93 | + } | |
| 94 | +} | |
| 95 | + | |
| 96 | +fn protocol_header(headers: &HeaderMap) -> Option<String> { | |
| 97 | + headers.get("git-protocol").and_then(|v| v.to_str().ok()).map(str::to_string) | |
| 98 | +} | |
| 99 | + | |
| 100 | +async fn info_refs( | |
| 101 | + State(state): State<AppState>, | |
| 102 | + Path((owner, name)): Path<(String, String)>, | |
| 103 | + Query(query): Query<InfoRefsQuery>, | |
| 104 | + headers: HeaderMap, | |
| 105 | +) -> Response { | |
| 106 | + let Some(service) = query.service.as_deref().and_then(Service::from_name) else { | |
| 107 | + return plain(StatusCode::FORBIDDEN, "Only smart HTTP is supported; upgrade git."); | |
| 108 | + }; | |
| 109 | + let (repo, viewer) = match authorize(&state, &headers, &owner, &name, service).await { | |
| 110 | + Ok(found) => found, | |
| 111 | + Err(response) => return response, | |
| 112 | + }; | |
| 113 | + let protocol = protocol_header(&headers); | |
| 114 | + let command = transport::service_command( | |
| 115 | + &state, | |
| 116 | + &repo, | |
| 117 | + service, | |
| 118 | + viewer.as_ref(), | |
| 119 | + ServiceOptions { stateless_rpc: true, advertise_refs: true, protocol: protocol.as_deref() }, | |
| 120 | + ) | |
| 121 | + .await; | |
| 122 | + let output = match command { | |
| 123 | + Ok(mut command) => command.stdout(Stdio::piped()).stderr(Stdio::piped()).output().await, | |
| 124 | + Err(error) => return internal(error), | |
| 125 | + }; | |
| 126 | + let output = match output { | |
| 127 | + Ok(output) if output.status.success() => output, | |
| 128 | + Ok(output) => { | |
| 129 | + tracing::error!(repo = %repo.full_name(), stderr = %String::from_utf8_lossy(&output.stderr), "advertise refs failed"); | |
| 130 | + return plain(StatusCode::INTERNAL_SERVER_ERROR, "git failed"); | |
| 131 | + } | |
| 132 | + Err(error) => return internal(error.into()), | |
| 133 | + }; | |
| 134 | + | |
| 135 | + // Protocol v2 replies with capabilities only; v0/v1 lead with a | |
| 136 | + // "# service=" packet and a flush, as git http-backend does. | |
| 137 | + let mut body = Vec::with_capacity(output.stdout.len() + 64); | |
| 138 | + if !protocol.as_deref().is_some_and(|p| p.contains("version=2")) { | |
| 139 | + let line = format!("# service={}\n", service.name()); | |
| 140 | + body.extend_from_slice(format!("{:04x}{line}0000", line.len() + 4).as_bytes()); | |
| 141 | + } | |
| 142 | + body.extend_from_slice(&output.stdout); | |
| 143 | + ( | |
| 144 | + StatusCode::OK, | |
| 145 | + [ | |
| 146 | + (header::CONTENT_TYPE, format!("application/x-{}-advertisement", service.name())), | |
| 147 | + (header::CACHE_CONTROL, "no-cache, max-age=0, must-revalidate".to_string()), | |
| 148 | + ], | |
| 149 | + body, | |
| 150 | + ) | |
| 151 | + .into_response() | |
| 152 | +} | |
| 153 | + | |
| 154 | +async fn upload_pack(state: State<AppState>, path: Path<(String, String)>, request: Request) -> Response { | |
| 155 | + run_service(state, path, request, Service::UploadPack).await | |
| 156 | +} | |
| 157 | + | |
| 158 | +async fn receive_pack(state: State<AppState>, path: Path<(String, String)>, request: Request) -> Response { | |
| 159 | + run_service(state, path, request, Service::ReceivePack).await | |
| 160 | +} | |
| 161 | + | |
| 162 | +async fn run_service(State(state): State<AppState>, Path((owner, name)): Path<(String, String)>, request: Request, service: Service) -> Response { | |
| 163 | + let headers = request.headers().clone(); | |
| 164 | + let (repo, viewer) = match authorize(&state, &headers, &owner, &name, service).await { | |
| 165 | + Ok(found) => found, | |
| 166 | + Err(response) => return response, | |
| 167 | + }; | |
| 168 | + let protocol = protocol_header(&headers); | |
| 169 | + let git = Git::new(repo.disk_path(&state.config)); | |
| 170 | + let before = if service == Service::ReceivePack { | |
| 171 | + match git.ref_snapshot().await { | |
| 172 | + Ok(refs) => Some(refs), | |
| 173 | + Err(error) => return internal(error), | |
| 174 | + } | |
| 175 | + } else { | |
| 176 | + None | |
| 177 | + }; | |
| 178 | + | |
| 179 | + let command = transport::service_command( | |
| 180 | + &state, | |
| 181 | + &repo, | |
| 182 | + service, | |
| 183 | + viewer.as_ref(), | |
| 184 | + ServiceOptions { stateless_rpc: true, advertise_refs: false, protocol: protocol.as_deref() }, | |
| 185 | + ) | |
| 186 | + .await; | |
| 187 | + let mut child = match command.and_then(|mut c| Ok(c.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?)) { | |
| 188 | + Ok(child) => child, | |
| 189 | + Err(error) => return internal(error), | |
| 190 | + }; | |
| 191 | + | |
| 192 | + // Request body -> git stdin, gunzipping when the client compressed it. | |
| 193 | + let gzip = headers.get(header::CONTENT_ENCODING).and_then(|v| v.to_str().ok()).is_some_and(|v| v.eq_ignore_ascii_case("gzip")); | |
| 194 | + let body = request.into_body().into_data_stream().map_err(std::io::Error::other); | |
| 195 | + let reader = StreamReader::new(body); | |
| 196 | + let mut input: Box<dyn AsyncRead + Send + Unpin> = if gzip { Box::new(GzipDecoder::new(reader)) } else { Box::new(reader) }; | |
| 197 | + let mut stdin = child.stdin.take().expect("piped"); | |
| 198 | + let repo_name = repo.full_name(); | |
| 199 | + tokio::spawn(async move { | |
| 200 | + if let Err(error) = tokio::io::copy(&mut input, &mut stdin).await { | |
| 201 | + tracing::warn!(%error, "client request body ended early"); | |
| 202 | + } | |
| 203 | + let _ = stdin.shutdown().await; | |
| 204 | + }); | |
| 205 | + | |
| 206 | + let stderr = child.stderr.take().expect("piped"); | |
| 207 | + let log_name = repo_name.clone(); | |
| 208 | + tokio::spawn(async move { | |
| 209 | + let mut lines = BufReader::new(stderr).lines(); | |
| 210 | + while let Ok(Some(line)) = lines.next_line().await { | |
| 211 | + tracing::warn!(repo = %log_name, service = service.name(), "git: {line}"); | |
| 212 | + } | |
| 213 | + }); | |
| 214 | + | |
| 215 | + let stdout = child.stdout.take().expect("piped"); | |
| 216 | + let via_user = viewer.as_ref().map(|v| v.name.clone()); | |
| 217 | + let finish_state = state.clone(); | |
| 218 | + tokio::spawn(async move { | |
| 219 | + let status = child.wait().await; | |
| 220 | + match (status, before) { | |
| 221 | + (Ok(status), Some(before)) if status.success() => { | |
| 222 | + push::spawn_after_push(finish_state, repo, viewer, before, "http"); | |
| 223 | + } | |
| 224 | + (Ok(status), _) if status.success() => { | |
| 225 | + analytics::track(&finish_state, "git_fetch", via_user.as_deref(), &repo.url(), json!({ "via": "http", "visibility": repo.visibility })); | |
| 226 | + } | |
| 227 | + (Ok(status), _) => tracing::info!(repo = %repo_name, service = service.name(), %status, "git service exited unsuccessfully"), | |
| 228 | + (Err(error), _) => tracing::error!(repo = %repo_name, %error, "waiting for git failed"), | |
| 229 | + } | |
| 230 | + }); | |
| 231 | + | |
| 232 | + Response::builder() | |
| 233 | + .status(StatusCode::OK) | |
| 234 | + .header(header::CONTENT_TYPE, format!("application/x-{}-result", service.name())) | |
| 235 | + .header(header::CACHE_CONTROL, "no-cache, max-age=0, must-revalidate") | |
| 236 | + .body(Body::from_stream(ReaderStream::new(stdout))) | |
| 237 | + .unwrap_or_else(|_| plain(StatusCode::INTERNAL_SERVER_ERROR, "internal error")) | |
| 238 | +} | |
| 239 | + | |
| 240 | +fn internal(error: anyhow::Error) -> Response { | |
| 241 | + tracing::error!(?error, "git http error"); | |
| 242 | + plain(StatusCode::INTERNAL_SERVER_ERROR, "internal error") | |
| 243 | +} |
+204-0backend/src/hook.rs
| @@ -0,0 +1,204 @@ | ||
| 1 | +//! The pre-receive hook. git runs `irongit hook pre-receive` (through the | |
| 2 | +//! script `install` writes) after receiving a pack and before updating refs. | |
| 3 | +//! The new objects sit in a quarantine directory at that point, so this is | |
| 4 | +//! the one place a push can be inspected and refused as a whole. | |
| 5 | +//! | |
| 6 | +//! Limits arrive in the environment from whoever spawned receive-pack: | |
| 7 | +//! IRONGIT_MAX_FILE_BYTES largest blob allowed (bigger belongs in LFS) | |
| 8 | +//! IRONGIT_QUOTA_REMAINING bytes the owner may still add | |
| 9 | + | |
| 10 | +use std::{ | |
| 11 | + collections::HashMap, | |
| 12 | + io::{BufRead, Write}, | |
| 13 | + path::Path, | |
| 14 | + process::{Command, Stdio}, | |
| 15 | +}; | |
| 16 | + | |
| 17 | +use crate::{config::Config, git::ZERO_SHA}; | |
| 18 | + | |
| 19 | +pub const ENV_MAX_FILE: &str = "IRONGIT_MAX_FILE_BYTES"; | |
| 20 | +pub const ENV_QUOTA_REMAINING: &str = "IRONGIT_QUOTA_REMAINING"; | |
| 21 | +pub const ENV_REPO: &str = "IRONGIT_REPO"; | |
| 22 | +pub const ENV_PUSHER: &str = "IRONGIT_PUSHER"; | |
| 23 | + | |
| 24 | +/// Writes the hook scripts git is pointed at with `core.hooksPath`. They | |
| 25 | +/// re-exec this binary, so the hook always matches the running server. | |
| 26 | +pub async fn install(config: &Config) -> anyhow::Result<()> { | |
| 27 | + let exe = std::env::current_exe()?; | |
| 28 | + let dir = config.hooks_dir(); | |
| 29 | + tokio::fs::create_dir_all(&dir).await?; | |
| 30 | + let log_dir = config.log_dir.display().to_string(); | |
| 31 | + let script = format!( | |
| 32 | + "#!/bin/sh\n# Written by irongit at startup; edits are overwritten.\nIRONGIT_LOG_DIR='{}' exec '{}' hook pre-receive\n", | |
| 33 | + log_dir.replace('\'', "'\\''"), | |
| 34 | + exe.display().to_string().replace('\'', "'\\''") | |
| 35 | + ); | |
| 36 | + let path = dir.join("pre-receive"); | |
| 37 | + tokio::fs::write(&path, script).await?; | |
| 38 | + #[cfg(unix)] | |
| 39 | + { | |
| 40 | + use std::os::unix::fs::PermissionsExt; | |
| 41 | + tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).await?; | |
| 42 | + } | |
| 43 | + tracing::info!(hook = %path.display(), "installed pre-receive hook"); | |
| 44 | + Ok(()) | |
| 45 | +} | |
| 46 | + | |
| 47 | +/// Entry point for `irongit hook <name>`. Exit status is the verdict. | |
| 48 | +pub fn run(name: &str) -> anyhow::Result<()> { | |
| 49 | + let _guard = std::env::var("IRONGIT_LOG_DIR").ok().and_then(|dir| crate::logging::init_hook(Path::new(&dir))); | |
| 50 | + match name { | |
| 51 | + "pre-receive" => match pre_receive() { | |
| 52 | + Ok(()) => Ok(()), | |
| 53 | + Err(Rejection(message)) => { | |
| 54 | + let mut stderr = std::io::stderr(); | |
| 55 | + for line in message.lines() { | |
| 56 | + let _ = writeln!(stderr, "irongit: {line}"); | |
| 57 | + } | |
| 58 | + tracing::info!(repo = %std::env::var(ENV_REPO).unwrap_or_default(), pusher = %std::env::var(ENV_PUSHER).unwrap_or_default(), %message, "push rejected"); | |
| 59 | + std::process::exit(1); | |
| 60 | + } | |
| 61 | + }, | |
| 62 | + other => anyhow::bail!("unknown hook {other}"), | |
| 63 | + } | |
| 64 | +} | |
| 65 | + | |
| 66 | +struct Rejection(String); | |
| 67 | + | |
| 68 | +impl<E: std::fmt::Display> From<E> for Rejection { | |
| 69 | + fn from(error: E) -> Self { | |
| 70 | + tracing::error!(%error, "pre-receive hook failed"); | |
| 71 | + Rejection(format!("internal error while checking the push: {error}")) | |
| 72 | + } | |
| 73 | +} | |
| 74 | + | |
| 75 | +fn pre_receive() -> Result<(), Rejection> { | |
| 76 | + let max_file: u64 = std::env::var(ENV_MAX_FILE).ok().and_then(|v| v.parse().ok()).unwrap_or(u64::MAX); | |
| 77 | + let quota_remaining: u64 = std::env::var(ENV_QUOTA_REMAINING).ok().and_then(|v| v.parse().ok()).unwrap_or(u64::MAX); | |
| 78 | + | |
| 79 | + let mut new_tips = Vec::new(); | |
| 80 | + for line in std::io::stdin().lock().lines() { | |
| 81 | + let line = line?; | |
| 82 | + let mut parts = line.split_whitespace(); | |
| 83 | + let (Some(_old), Some(new), Some(_name)) = (parts.next(), parts.next(), parts.next()) else { continue }; | |
| 84 | + if new != ZERO_SHA { | |
| 85 | + new_tips.push(new.to_string()); | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | + // Quarantined objects are what this push adds to disk. | |
| 90 | + if let Ok(quarantine) = std::env::var("GIT_QUARANTINE_PATH") { | |
| 91 | + let added = dir_size(Path::new(&quarantine)); | |
| 92 | + if added > quota_remaining { | |
| 93 | + return Err(Rejection(format!( | |
| 94 | + "this push adds {} but the account has {} of storage left.\nDelete unused repositories or ask an administrator for more space.", | |
| 95 | + human(added), | |
| 96 | + human(quota_remaining) | |
| 97 | + ))); | |
| 98 | + } | |
| 99 | + } | |
| 100 | + if new_tips.is_empty() || max_file == u64::MAX { | |
| 101 | + return Ok(()); | |
| 102 | + } | |
| 103 | + | |
| 104 | + // Objects reachable from the new tips but from no existing ref: exactly | |
| 105 | + // what this push introduces. rev-list prints "sha path" for blobs. | |
| 106 | + let mut rev_list = Command::new("git") | |
| 107 | + .args(["rev-list", "--objects", "--no-object-names"]) | |
| 108 | + .args(["--stdin"]) | |
| 109 | + .stdin(Stdio::piped()) | |
| 110 | + .stdout(Stdio::piped()) | |
| 111 | + .spawn()?; | |
| 112 | + // --no-object-names keeps output parseable; names are recovered below. | |
| 113 | + { | |
| 114 | + let mut stdin = rev_list.stdin.take().expect("piped"); | |
| 115 | + for tip in &new_tips { | |
| 116 | + writeln!(stdin, "{tip}")?; | |
| 117 | + } | |
| 118 | + writeln!(stdin, "--not")?; | |
| 119 | + writeln!(stdin, "--all")?; | |
| 120 | + } | |
| 121 | + let listed = rev_list.wait_with_output()?; | |
| 122 | + if !listed.status.success() { | |
| 123 | + return Err(Rejection("could not list the pushed objects".into())); | |
| 124 | + } | |
| 125 | + | |
| 126 | + let mut check = Command::new("git") | |
| 127 | + .args(["cat-file", "--batch-check=%(objecttype) %(objectname) %(objectsize)"]) | |
| 128 | + .stdin(Stdio::piped()) | |
| 129 | + .stdout(Stdio::piped()) | |
| 130 | + .spawn()?; | |
| 131 | + let objects = listed.stdout; | |
| 132 | + let mut stdin = check.stdin.take().expect("piped"); | |
| 133 | + let writer = std::thread::spawn(move || stdin.write_all(&objects)); | |
| 134 | + let output = check.wait_with_output()?; | |
| 135 | + let _ = writer.join(); | |
| 136 | + | |
| 137 | + let oversized: Vec<(String, u64)> = String::from_utf8_lossy(&output.stdout) | |
| 138 | + .lines() | |
| 139 | + .filter_map(|line| { | |
| 140 | + let mut f = line.split_whitespace(); | |
| 141 | + let (kind, sha, size) = (f.next()?, f.next()?, f.next()?.parse::<u64>().ok()?); | |
| 142 | + (kind == "blob" && size > max_file).then(|| (sha.to_string(), size)) | |
| 143 | + }) | |
| 144 | + .collect(); | |
| 145 | + if oversized.is_empty() { | |
| 146 | + return Ok(()); | |
| 147 | + } | |
| 148 | + | |
| 149 | + let names = blob_paths(&new_tips, &oversized.iter().map(|(sha, _)| sha.clone()).collect::<Vec<_>>()); | |
| 150 | + let mut message = format!("push rejected: files larger than {} must use Git LFS.\n", human(max_file)); | |
| 151 | + for (sha, size) in oversized.iter().take(20) { | |
| 152 | + let name = names.get(sha).map(String::as_str).unwrap_or(&sha[..12]); | |
| 153 | + message.push_str(&format!(" {name} ({})\n", human(*size))); | |
| 154 | + } | |
| 155 | + message.push_str("Track them with `git lfs track <pattern>`, then rewrite the commits that added them\n"); | |
| 156 | + message.push_str("(for example `git lfs migrate import --include=<pattern>`) and push again."); | |
| 157 | + Err(Rejection(message)) | |
| 158 | +} | |
| 159 | + | |
| 160 | +/// Recovers file paths for blob shas, for a readable rejection message. | |
| 161 | +fn blob_paths(tips: &[String], wanted: &[String]) -> HashMap<String, String> { | |
| 162 | + let mut out = HashMap::new(); | |
| 163 | + let Ok(mut child) = Command::new("git") | |
| 164 | + .args(["rev-list", "--objects", "--stdin"]) | |
| 165 | + .stdin(Stdio::piped()) | |
| 166 | + .stdout(Stdio::piped()) | |
| 167 | + .spawn() | |
| 168 | + else { | |
| 169 | + return out; | |
| 170 | + }; | |
| 171 | + if let Some(mut stdin) = child.stdin.take() { | |
| 172 | + for tip in tips { | |
| 173 | + let _ = writeln!(stdin, "{tip}"); | |
| 174 | + } | |
| 175 | + let _ = writeln!(stdin, "--not"); | |
| 176 | + let _ = writeln!(stdin, "--all"); | |
| 177 | + } | |
| 178 | + if let Ok(output) = child.wait_with_output() { | |
| 179 | + for line in String::from_utf8_lossy(&output.stdout).lines() { | |
| 180 | + if let Some((sha, path)) = line.split_once(' ') { | |
| 181 | + if wanted.iter().any(|w| w == sha) { | |
| 182 | + out.entry(sha.to_string()).or_insert_with(|| path.to_string()); | |
| 183 | + } | |
| 184 | + } | |
| 185 | + } | |
| 186 | + } | |
| 187 | + out | |
| 188 | +} | |
| 189 | + | |
| 190 | +fn dir_size(path: &Path) -> u64 { | |
| 191 | + let Ok(entries) = std::fs::read_dir(path) else { return 0 }; | |
| 192 | + entries | |
| 193 | + .flatten() | |
| 194 | + .map(|entry| match entry.metadata() { | |
| 195 | + Ok(meta) if meta.is_dir() => dir_size(&entry.path()), | |
| 196 | + Ok(meta) => meta.len(), | |
| 197 | + Err(_) => 0, | |
| 198 | + }) | |
| 199 | + .sum() | |
| 200 | +} | |
| 201 | + | |
| 202 | +fn human(bytes: u64) -> String { | |
| 203 | + crate::web::ui::bytes(bytes) | |
| 204 | +} |
+25-0backend/src/jobs.rs
| @@ -0,0 +1,25 @@ | ||
| 1 | +//! Background work started with the server. | |
| 2 | + | |
| 3 | +use std::time::Duration; | |
| 4 | + | |
| 5 | +use crate::state::AppState; | |
| 6 | + | |
| 7 | +pub fn spawn_all(state: AppState) { | |
| 8 | + crate::registry::spawn_gc(state.clone()); | |
| 9 | + crate::backup::spawn(state.clone()); | |
| 10 | + tokio::spawn(prune_sessions(state)); | |
| 11 | +} | |
| 12 | + | |
| 13 | +/// Expired sessions and device codes are deleted hourly. | |
| 14 | +async fn prune_sessions(state: AppState) { | |
| 15 | + let mut tick = tokio::time::interval(Duration::from_secs(3600)); | |
| 16 | + loop { | |
| 17 | + tick.tick().await; | |
| 18 | + let sessions = sqlx::query("delete from sessions where expires_at < now()").execute(&state.db).await; | |
| 19 | + let codes = sqlx::query("delete from device_codes where expires_at < now() - interval '1 day'").execute(&state.db).await; | |
| 20 | + match (sessions, codes) { | |
| 21 | + (Ok(s), Ok(c)) => tracing::debug!(sessions = s.rows_affected(), device_codes = c.rows_affected(), "pruned"), | |
| 22 | + (Err(e), _) | (_, Err(e)) => tracing::warn!(error = %e, "pruning failed"), | |
| 23 | + } | |
| 24 | + } | |
| 25 | +} |
+9-0backend/src/lfs.rs
| @@ -0,0 +1,9 @@ | ||
| 1 | +//! Git LFS batch API backed by R2. Stub. | |
| 2 | + | |
| 3 | +use axum::Router; | |
| 4 | + | |
| 5 | +use crate::state::AppState; | |
| 6 | + | |
| 7 | +pub fn router() -> Router<AppState> { | |
| 8 | + Router::new() | |
| 9 | +} |
+67-0backend/src/logging.rs
| @@ -0,0 +1,67 @@ | ||
| 1 | +//! Logs go to stdout and to files under LOG_DIR (default DATA_DIR/logs): | |
| 2 | +//! server.YYYY-MM-DD.log everything the server does, one request per line | |
| 3 | +//! frontend.YYYY-MM-DD.log browser errors posted to /api/v1/client-logs | |
| 4 | +//! hooks.YYYY-MM-DD.log git hook runs (pre-receive size checks) | |
| 5 | + | |
| 6 | +use std::path::Path; | |
| 7 | + | |
| 8 | +use tracing_appender::{non_blocking::WorkerGuard, rolling}; | |
| 9 | +use tracing_subscriber::{EnvFilter, Layer, filter::filter_fn, fmt, layer::SubscriberExt, util::SubscriberInitExt}; | |
| 10 | + | |
| 11 | +pub const FRONTEND_TARGET: &str = "frontend"; | |
| 12 | + | |
| 13 | +pub fn init(log_dir: &Path) -> anyhow::Result<Vec<WorkerGuard>> { | |
| 14 | + std::fs::create_dir_all(log_dir)?; | |
| 15 | + let server_appender = rolling::Builder::new() | |
| 16 | + .rotation(rolling::Rotation::DAILY) | |
| 17 | + .filename_prefix("server") | |
| 18 | + .filename_suffix("log") | |
| 19 | + .max_log_files(14) | |
| 20 | + .build(log_dir)?; | |
| 21 | + let frontend_appender = rolling::Builder::new() | |
| 22 | + .rotation(rolling::Rotation::DAILY) | |
| 23 | + .filename_prefix("frontend") | |
| 24 | + .filename_suffix("log") | |
| 25 | + .max_log_files(14) | |
| 26 | + .build(log_dir)?; | |
| 27 | + let (server_writer, server_guard) = tracing_appender::non_blocking(server_appender); | |
| 28 | + let (frontend_writer, frontend_guard) = tracing_appender::non_blocking(frontend_appender); | |
| 29 | + | |
| 30 | + let filter = || EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,russh=warn,tower_http=info")); | |
| 31 | + | |
| 32 | + tracing_subscriber::registry() | |
| 33 | + .with(fmt::layer().with_target(true).with_filter(filter())) | |
| 34 | + .with( | |
| 35 | + fmt::layer() | |
| 36 | + .with_ansi(false) | |
| 37 | + .with_writer(server_writer) | |
| 38 | + .with_filter(filter()) | |
| 39 | + .with_filter(filter_fn(|meta| meta.target() != FRONTEND_TARGET)), | |
| 40 | + ) | |
| 41 | + .with( | |
| 42 | + fmt::layer() | |
| 43 | + .with_ansi(false) | |
| 44 | + .with_target(false) | |
| 45 | + .with_writer(frontend_writer) | |
| 46 | + .with_filter(filter_fn(|meta| meta.target() == FRONTEND_TARGET)), | |
| 47 | + ) | |
| 48 | + .init(); | |
| 49 | + Ok(vec![server_guard, frontend_guard]) | |
| 50 | +} | |
| 51 | + | |
| 52 | +/// Hooks run as short-lived child processes; they append to their own file. | |
| 53 | +pub fn init_hook(log_dir: &Path) -> Option<WorkerGuard> { | |
| 54 | + std::fs::create_dir_all(log_dir).ok()?; | |
| 55 | + let appender = rolling::Builder::new() | |
| 56 | + .rotation(rolling::Rotation::DAILY) | |
| 57 | + .filename_prefix("hooks") | |
| 58 | + .filename_suffix("log") | |
| 59 | + .max_log_files(14) | |
| 60 | + .build(log_dir) | |
| 61 | + .ok()?; | |
| 62 | + let (writer, guard) = tracing_appender::non_blocking(appender); | |
| 63 | + tracing_subscriber::registry() | |
| 64 | + .with(fmt::layer().with_ansi(false).with_writer(writer).with_filter(EnvFilter::new("info"))) | |
| 65 | + .init(); | |
| 66 | + Some(guard) | |
| 67 | +} |
+29-0backend/src/mail.rs
| @@ -0,0 +1,29 @@ | ||
| 1 | +//! Outgoing email (verification links). Without SMTP configured the message | |
| 2 | +//! is written to the log instead, which is how development works. | |
| 3 | + | |
| 4 | +use lettre::{ | |
| 5 | + AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, message::header::ContentType, | |
| 6 | + transport::smtp::authentication::Credentials, | |
| 7 | +}; | |
| 8 | + | |
| 9 | +use crate::state::AppState; | |
| 10 | + | |
| 11 | +pub async fn send(state: &AppState, to: &str, subject: &str, body: &str) -> anyhow::Result<()> { | |
| 12 | + let Some(smtp) = &state.config.smtp else { | |
| 13 | + tracing::info!(target: "mail", to, subject, body, "SMTP not configured; email logged instead of sent"); | |
| 14 | + return Ok(()); | |
| 15 | + }; | |
| 16 | + let message = Message::builder() | |
| 17 | + .from(smtp.from.parse()?) | |
| 18 | + .to(to.parse()?) | |
| 19 | + .subject(subject) | |
| 20 | + .header(ContentType::TEXT_PLAIN) | |
| 21 | + .body(body.to_string())?; | |
| 22 | + let transport = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.host)? | |
| 23 | + .port(smtp.port) | |
| 24 | + .credentials(Credentials::new(smtp.username.clone(), smtp.password.clone())) | |
| 25 | + .build(); | |
| 26 | + transport.send(message).await?; | |
| 27 | + tracing::info!(to, subject, "email sent"); | |
| 28 | + Ok(()) | |
| 29 | +} |
+214-0backend/src/main.rs
| @@ -0,0 +1,214 @@ | ||
| 1 | +//! irongit: git hosting, a container registry and public profiles, served by | |
| 2 | +//! one binary. axum handles HTTP (web UI, JSON API, git smart HTTP, LFS, the | |
| 3 | +//! OCI registry); a built-in SSH server handles git over SSH. | |
| 4 | +//! | |
| 5 | +//! The same binary is also the git hook (`irongit hook pre-receive`) and the | |
| 6 | +//! admin tool (`irongit admin ...`). | |
| 7 | + | |
| 8 | +// Scaffolding is wired up before every caller exists; revisit before release. | |
| 9 | +#![allow(dead_code)] | |
| 10 | + | |
| 11 | +mod analytics; | |
| 12 | +mod api; | |
| 13 | +mod auth; | |
| 14 | +mod backup; | |
| 15 | +mod clientlog; | |
| 16 | +mod config; | |
| 17 | +#[cfg(feature = "hot-reload")] | |
| 18 | +mod dev; | |
| 19 | +mod error; | |
| 20 | +mod frontend; | |
| 21 | +mod git; | |
| 22 | +mod git_http; | |
| 23 | +mod hook; | |
| 24 | +mod jobs; | |
| 25 | +mod lfs; | |
| 26 | +mod logging; | |
| 27 | +mod mail; | |
| 28 | +mod models; | |
| 29 | +mod ops; | |
| 30 | +mod perm; | |
| 31 | +mod push; | |
| 32 | +mod registry; | |
| 33 | +mod routes; | |
| 34 | +mod signed; | |
| 35 | +mod sshkeys; | |
| 36 | +mod ssh; | |
| 37 | +mod state; | |
| 38 | +mod storage; | |
| 39 | +mod transport; | |
| 40 | +mod web; | |
| 41 | + | |
| 42 | +use std::sync::Arc; | |
| 43 | + | |
| 44 | +use anyhow::Context; | |
| 45 | +use clap::{Parser, Subcommand}; | |
| 46 | +use sqlx::postgres::PgPoolOptions; | |
| 47 | +use tokio::{net::TcpListener, sync::broadcast}; | |
| 48 | + | |
| 49 | +use crate::{config::Config, state::AppState, storage::Storage}; | |
| 50 | + | |
| 51 | +#[derive(Parser)] | |
| 52 | +#[command(name = "irongit", version, about = "Git hosting and container registry server")] | |
| 53 | +struct Cli { | |
| 54 | + #[command(subcommand)] | |
| 55 | + command: Option<Command>, | |
| 56 | +} | |
| 57 | + | |
| 58 | +#[derive(Subcommand)] | |
| 59 | +enum Command { | |
| 60 | + /// Run the server (the default). | |
| 61 | + Serve, | |
| 62 | + /// Git hook entry point; git runs this, not people. | |
| 63 | + Hook { | |
| 64 | + #[arg(value_parser = ["pre-receive"])] | |
| 65 | + name: String, | |
| 66 | + }, | |
| 67 | + /// Administrative tasks against the configured database. | |
| 68 | + Admin { | |
| 69 | + #[command(subcommand)] | |
| 70 | + command: AdminCommand, | |
| 71 | + }, | |
| 72 | +} | |
| 73 | + | |
| 74 | +#[derive(Subcommand)] | |
| 75 | +enum AdminCommand { | |
| 76 | + /// Make an existing user a site admin. | |
| 77 | + Promote { username: String }, | |
| 78 | + /// Remove site admin from a user. | |
| 79 | + Demote { username: String }, | |
| 80 | + /// Write, read and delete a test object in R2 to verify credentials. | |
| 81 | + CheckStorage, | |
| 82 | + /// Publish a CLI build to R2 so `ig upgrade` and install.sh serve it. | |
| 83 | + PublishCli { | |
| 84 | + /// Path to the built `ig` binary. | |
| 85 | + path: std::path::PathBuf, | |
| 86 | + /// Version string, e.g. 0.1.0. | |
| 87 | + #[arg(long)] | |
| 88 | + version: String, | |
| 89 | + #[arg(long, default_value = "x86_64-unknown-linux-musl")] | |
| 90 | + target: String, | |
| 91 | + }, | |
| 92 | +} | |
| 93 | + | |
| 94 | +fn main() -> anyhow::Result<()> { | |
| 95 | + // .env lives at the workspace root; dotenvy searches parent directories. | |
| 96 | + let _ = dotenvy::dotenv(); | |
| 97 | + let cli = Cli::parse(); | |
| 98 | + match cli.command.unwrap_or(Command::Serve) { | |
| 99 | + // Hooks are short-lived and must not spin up a runtime per thread. | |
| 100 | + Command::Hook { name } => hook::run(&name), | |
| 101 | + Command::Serve => tokio_main(serve()), | |
| 102 | + Command::Admin { command } => tokio_main(admin(command)), | |
| 103 | + } | |
| 104 | +} | |
| 105 | + | |
| 106 | +fn tokio_main<F: std::future::Future<Output = anyhow::Result<()>>>(future: F) -> anyhow::Result<()> { | |
| 107 | + tokio::runtime::Builder::new_multi_thread().enable_all().build()?.block_on(future) | |
| 108 | +} | |
| 109 | + | |
| 110 | +async fn connect(config: &Config) -> anyhow::Result<sqlx::PgPool> { | |
| 111 | + let db = PgPoolOptions::new() | |
| 112 | + .max_connections(20) | |
| 113 | + .acquire_timeout(std::time::Duration::from_secs(10)) | |
| 114 | + .connect(&config.database_url) | |
| 115 | + .await | |
| 116 | + .context("connecting to Postgres (DATABASE_URL)")?; | |
| 117 | + sqlx::migrate!("./migrations").run(&db).await.context("running migrations")?; | |
| 118 | + Ok(db) | |
| 119 | +} | |
| 120 | + | |
| 121 | +async fn serve() -> anyhow::Result<()> { | |
| 122 | + let config = Config::from_env()?; | |
| 123 | + let _log_guards = logging::init(&config.log_dir)?; | |
| 124 | + tracing::info!(public_url = %config.public_url, data_dir = %config.data_dir.display(), logs = %config.log_dir.display(), "starting irongit"); | |
| 125 | + | |
| 126 | + let db = connect(&config).await?; | |
| 127 | + for dir in [config.repos_dir(), config.uploads_dir(), config.hooks_dir()] { | |
| 128 | + tokio::fs::create_dir_all(&dir).await?; | |
| 129 | + } | |
| 130 | + hook::install(&config).await?; | |
| 131 | + | |
| 132 | + let (reload_tx, _) = broadcast::channel::<()>(16); | |
| 133 | + #[cfg(feature = "hot-reload")] | |
| 134 | + if config::flag("FRONTEND_WATCH", cfg!(debug_assertions)) { | |
| 135 | + dev::watch_frontend(reload_tx.clone())?; | |
| 136 | + } | |
| 137 | + | |
| 138 | + let state = AppState { | |
| 139 | + storage: Storage::new(&config.r2)?, | |
| 140 | + http: reqwest::Client::builder().timeout(std::time::Duration::from_secs(15)).build()?, | |
| 141 | + config: Arc::new(config), | |
| 142 | + db, | |
| 143 | + reload_tx, | |
| 144 | + }; | |
| 145 | + | |
| 146 | + jobs::spawn_all(state.clone()); | |
| 147 | + ssh::spawn(state.clone()); | |
| 148 | + | |
| 149 | + let app = routes::router(state.clone()); | |
| 150 | + let address = format!("{}:{}", state.config.host, state.config.port); | |
| 151 | + let listener = TcpListener::bind(&address).await.with_context(|| format!("binding {address}"))?; | |
| 152 | + tracing::info!(address = %listener.local_addr()?, "http listening"); | |
| 153 | + axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()) | |
| 154 | + .with_graceful_shutdown(shutdown_signal()) | |
| 155 | + .await?; | |
| 156 | + Ok(()) | |
| 157 | +} | |
| 158 | + | |
| 159 | +async fn shutdown_signal() { | |
| 160 | + let ctrl_c = async { | |
| 161 | + let _ = tokio::signal::ctrl_c().await; | |
| 162 | + }; | |
| 163 | + let terminate = async { | |
| 164 | + if let Ok(mut signal) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { | |
| 165 | + signal.recv().await; | |
| 166 | + } | |
| 167 | + }; | |
| 168 | + tokio::select! { _ = ctrl_c => {}, _ = terminate => {} } | |
| 169 | + tracing::info!("shutting down"); | |
| 170 | +} | |
| 171 | + | |
| 172 | +async fn admin(command: AdminCommand) -> anyhow::Result<()> { | |
| 173 | + let config = Config::from_env()?; | |
| 174 | + let db = connect(&config).await?; | |
| 175 | + match command { | |
| 176 | + AdminCommand::Promote { username } => set_admin(&db, &username, true).await, | |
| 177 | + AdminCommand::Demote { username } => set_admin(&db, &username, false).await, | |
| 178 | + AdminCommand::CheckStorage => check_storage(&config).await, | |
| 179 | + AdminCommand::PublishCli { path, version, target } => { | |
| 180 | + let storage = Storage::new(&config.r2)?; | |
| 181 | + api::release::publish(&db, &storage, &path, &version, &target).await | |
| 182 | + } | |
| 183 | + } | |
| 184 | +} | |
| 185 | + | |
| 186 | +async fn check_storage(config: &Config) -> anyhow::Result<()> { | |
| 187 | + let storage = Storage::new(&config.r2)?; | |
| 188 | + let key = format!("healthcheck/{}", uuid::Uuid::new_v4()); | |
| 189 | + let body = bytes::Bytes::from_static(b"irongit storage check"); | |
| 190 | + storage.put_bytes(&key, body.clone(), "text/plain").await.context("PUT")?; | |
| 191 | + let size = storage.head(&key).await.context("HEAD")?; | |
| 192 | + anyhow::ensure!(size == Some(body.len() as u64), "HEAD returned {size:?}"); | |
| 193 | + let read = storage.get_bytes(&key).await.context("GET")?; | |
| 194 | + anyhow::ensure!(read.as_deref() == Some(&body[..]), "GET returned different bytes"); | |
| 195 | + let presigned = storage.presign_get(&key, std::time::Duration::from_secs(60), None); | |
| 196 | + let fetched = reqwest::get(presigned).await?.error_for_status()?.bytes().await?; | |
| 197 | + anyhow::ensure!(fetched == body, "presigned GET returned different bytes"); | |
| 198 | + storage.delete(&key).await.context("DELETE")?; | |
| 199 | + anyhow::ensure!(storage.head(&key).await?.is_none(), "object still exists after DELETE"); | |
| 200 | + println!("R2 bucket {} OK: put, head, get, presigned get, delete", storage.bucket_name()); | |
| 201 | + Ok(()) | |
| 202 | +} | |
| 203 | + | |
| 204 | +async fn set_admin(db: &sqlx::PgPool, username: &str, admin: bool) -> anyhow::Result<()> { | |
| 205 | + let updated = sqlx::query("update users set is_admin = $2 where account_id = (select id from accounts where name = $1 and kind = 'user')") | |
| 206 | + .bind(username) | |
| 207 | + .bind(admin) | |
| 208 | + .execute(db) | |
| 209 | + .await? | |
| 210 | + .rows_affected(); | |
| 211 | + anyhow::ensure!(updated == 1, "no user named {username}"); | |
| 212 | + println!("{username} is {}a site admin", if admin { "now " } else { "no longer " }); | |
| 213 | + Ok(()) | |
| 214 | +} |
+251-0backend/src/models.rs
| @@ -0,0 +1,251 @@ | ||
| 1 | +//! Rows shared across modules, and the lookups everything needs. | |
| 2 | + | |
| 3 | +use std::path::PathBuf; | |
| 4 | + | |
| 5 | +use chrono::{DateTime, Utc}; | |
| 6 | +use serde::Serialize; | |
| 7 | +use sqlx::{FromRow, PgPool}; | |
| 8 | + | |
| 9 | +use crate::config::Config; | |
| 10 | + | |
| 11 | +#[derive(Debug, Clone, FromRow, Serialize)] | |
| 12 | +pub struct Account { | |
| 13 | + pub id: i64, | |
| 14 | + pub kind: String, | |
| 15 | + pub name: String, | |
| 16 | + pub display_name: String, | |
| 17 | + pub bio: String, | |
| 18 | + pub location: String, | |
| 19 | + pub website: String, | |
| 20 | + pub avatar_key: Option<String>, | |
| 21 | + pub quota_bytes: Option<i64>, | |
| 22 | + pub created_at: DateTime<Utc>, | |
| 23 | + pub updated_at: DateTime<Utc>, | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// Column list for `Account`, as a macro so queries stay `&'static str` | |
| 27 | +/// (sqlx refuses runtime-built SQL): `concat!("select ", account_columns!(), " from ...")`. | |
| 28 | +#[macro_export] | |
| 29 | +macro_rules! account_columns { | |
| 30 | + () => { | |
| 31 | + "id, kind, name, display_name, bio, location, website, avatar_key, quota_bytes, created_at, updated_at" | |
| 32 | + }; | |
| 33 | +} | |
| 34 | + | |
| 35 | +impl Account { | |
| 36 | + pub async fn by_name(db: &PgPool, name: &str) -> sqlx::Result<Option<Self>> { | |
| 37 | + sqlx::query_as(concat!("select ", account_columns!(), " from accounts where name = $1")) | |
| 38 | + .bind(name) | |
| 39 | + .fetch_optional(db) | |
| 40 | + .await | |
| 41 | + } | |
| 42 | + | |
| 43 | + pub async fn by_id(db: &PgPool, id: i64) -> sqlx::Result<Option<Self>> { | |
| 44 | + sqlx::query_as(concat!("select ", account_columns!(), " from accounts where id = $1")) | |
| 45 | + .bind(id) | |
| 46 | + .fetch_optional(db) | |
| 47 | + .await | |
| 48 | + } | |
| 49 | + | |
| 50 | + pub fn is_org(&self) -> bool { | |
| 51 | + self.kind == "org" | |
| 52 | + } | |
| 53 | + | |
| 54 | + /// Display name, falling back to the handle. | |
| 55 | + pub fn label(&self) -> &str { | |
| 56 | + if self.display_name.trim().is_empty() { &self.name } else { &self.display_name } | |
| 57 | + } | |
| 58 | + | |
| 59 | + pub fn quota(&self, config: &Config) -> u64 { | |
| 60 | + self.quota_bytes.map(|q| q.max(0) as u64).unwrap_or(config.limits.account_quota_bytes) | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +#[derive(Debug, Clone, FromRow, Serialize)] | |
| 65 | +pub struct Repo { | |
| 66 | + pub id: i64, | |
| 67 | + pub owner_id: i64, | |
| 68 | + pub owner_name: String, | |
| 69 | + pub owner_kind: String, | |
| 70 | + pub name: String, | |
| 71 | + pub description: String, | |
| 72 | + pub visibility: String, | |
| 73 | + pub default_branch: String, | |
| 74 | + pub size_bytes: i64, | |
| 75 | + pub is_empty: bool, | |
| 76 | + pub archived: bool, | |
| 77 | + pub created_at: DateTime<Utc>, | |
| 78 | + pub updated_at: DateTime<Utc>, | |
| 79 | + pub pushed_at: Option<DateTime<Utc>>, | |
| 80 | +} | |
| 81 | + | |
| 82 | +/// `select ... from repos r join accounts a` yielding `Repo` rows; append | |
| 83 | +/// conditions with `concat!(repo_select!(), " where ...")`. | |
| 84 | +#[macro_export] | |
| 85 | +macro_rules! repo_select { | |
| 86 | + () => { | |
| 87 | + "select r.id, r.owner_id, a.name as owner_name, a.kind as owner_kind, r.name, r.description, r.visibility, \ | |
| 88 | + r.default_branch, r.size_bytes, r.is_empty, r.archived, r.created_at, r.updated_at, r.pushed_at \ | |
| 89 | + from repos r join accounts a on a.id = r.owner_id" | |
| 90 | + }; | |
| 91 | +} | |
| 92 | + | |
| 93 | +impl Repo { | |
| 94 | + /// Looks a repo up by URL path segments. A trailing ".git" is ignored. | |
| 95 | + pub async fn by_path(db: &PgPool, owner: &str, name: &str) -> sqlx::Result<Option<Self>> { | |
| 96 | + let name = name.strip_suffix(".git").unwrap_or(name); | |
| 97 | + sqlx::query_as(concat!(repo_select!(), " where a.name = $1 and r.name = $2")) | |
| 98 | + .bind(owner) | |
| 99 | + .bind(name) | |
| 100 | + .fetch_optional(db) | |
| 101 | + .await | |
| 102 | + } | |
| 103 | + | |
| 104 | + pub async fn by_id(db: &PgPool, id: i64) -> sqlx::Result<Option<Self>> { | |
| 105 | + sqlx::query_as(concat!(repo_select!(), " where r.id = $1")) | |
| 106 | + .bind(id) | |
| 107 | + .fetch_optional(db) | |
| 108 | + .await | |
| 109 | + } | |
| 110 | + | |
| 111 | + pub fn is_public(&self) -> bool { | |
| 112 | + self.visibility == "public" | |
| 113 | + } | |
| 114 | + | |
| 115 | + pub fn full_name(&self) -> String { | |
| 116 | + format!("{}/{}", self.owner_name, self.name) | |
| 117 | + } | |
| 118 | + | |
| 119 | + pub fn url(&self) -> String { | |
| 120 | + format!("/{}/{}", self.owner_name, self.name) | |
| 121 | + } | |
| 122 | + | |
| 123 | + /// Repos are stored by id, so renames and transfers never move files. | |
| 124 | + pub fn disk_path(&self, config: &Config) -> PathBuf { | |
| 125 | + repo_disk_path(config, self.id) | |
| 126 | + } | |
| 127 | +} | |
| 128 | + | |
| 129 | +pub fn repo_disk_path(config: &Config, id: i64) -> PathBuf { | |
| 130 | + config.repos_dir().join(format!("{id}.git")) | |
| 131 | +} | |
| 132 | + | |
| 133 | +#[derive(Debug, Clone, FromRow, Serialize)] | |
| 134 | +pub struct Package { | |
| 135 | + pub id: i64, | |
| 136 | + pub owner_id: i64, | |
| 137 | + pub owner_name: String, | |
| 138 | + pub owner_kind: String, | |
| 139 | + pub name: String, | |
| 140 | + pub visibility: String, | |
| 141 | + pub repo_id: Option<i64>, | |
| 142 | + pub description: String, | |
| 143 | + pub pull_count: i64, | |
| 144 | + pub created_at: DateTime<Utc>, | |
| 145 | + pub updated_at: DateTime<Utc>, | |
| 146 | +} | |
| 147 | + | |
| 148 | +/// `select ... from packages p join accounts a` yielding `Package` rows. | |
| 149 | +#[macro_export] | |
| 150 | +macro_rules! package_select { | |
| 151 | + () => { | |
| 152 | + "select p.id, p.owner_id, a.name as owner_name, a.kind as owner_kind, p.name, p.visibility, p.repo_id, \ | |
| 153 | + p.description, p.pull_count, p.created_at, p.updated_at from packages p join accounts a on a.id = p.owner_id" | |
| 154 | + }; | |
| 155 | +} | |
| 156 | + | |
| 157 | +impl Package { | |
| 158 | + pub async fn by_path(db: &PgPool, owner: &str, name: &str) -> sqlx::Result<Option<Self>> { | |
| 159 | + sqlx::query_as(concat!(package_select!(), " where a.name = $1 and p.name = $2")) | |
| 160 | + .bind(owner) | |
| 161 | + .bind(name.to_ascii_lowercase()) | |
| 162 | + .fetch_optional(db) | |
| 163 | + .await | |
| 164 | + } | |
| 165 | + | |
| 166 | + pub async fn by_id(db: &PgPool, id: i64) -> sqlx::Result<Option<Self>> { | |
| 167 | + sqlx::query_as(concat!(package_select!(), " where p.id = $1")) | |
| 168 | + .bind(id) | |
| 169 | + .fetch_optional(db) | |
| 170 | + .await | |
| 171 | + } | |
| 172 | + | |
| 173 | + pub fn is_public(&self) -> bool { | |
| 174 | + self.visibility == "public" | |
| 175 | + } | |
| 176 | + | |
| 177 | + /// "owner/name", the image path after the registry host. | |
| 178 | + pub fn full_name(&self) -> String { | |
| 179 | + format!("{}/{}", self.owner_name, self.name) | |
| 180 | + } | |
| 181 | + | |
| 182 | + pub fn url(&self) -> String { | |
| 183 | + format!("/{}/-/packages/{}", self.owner_name, self.name) | |
| 184 | + } | |
| 185 | +} | |
| 186 | + | |
| 187 | +/// Records something an administrator may want to trace later. | |
| 188 | +pub async fn audit(db: &PgPool, actor: Option<i64>, action: &str, target: &str, meta: serde_json::Value, ip: Option<&str>) { | |
| 189 | + let result = sqlx::query("insert into audit_log (actor_id, action, target, meta, ip) values ($1, $2, $3, $4, $5)") | |
| 190 | + .bind(actor) | |
| 191 | + .bind(action) | |
| 192 | + .bind(target) | |
| 193 | + .bind(meta) | |
| 194 | + .bind(ip) | |
| 195 | + .execute(db) | |
| 196 | + .await; | |
| 197 | + if let Err(error) = result { | |
| 198 | + tracing::warn!(%error, action, "could not write audit log"); | |
| 199 | + } | |
| 200 | +} | |
| 201 | + | |
| 202 | +/// Names: lowercase letters, digits and single hyphens, 1 to 39 characters, | |
| 203 | +/// so they work in URLs, SSH paths and Docker image names unchanged. | |
| 204 | +pub fn valid_account_name(name: &str) -> Result<(), &'static str> { | |
| 205 | + if name.is_empty() || name.len() > 39 { | |
| 206 | + return Err("Names must be 1 to 39 characters."); | |
| 207 | + } | |
| 208 | + if !name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') { | |
| 209 | + return Err("Names may only contain lowercase letters, digits and hyphens."); | |
| 210 | + } | |
| 211 | + if name.starts_with('-') || name.ends_with('-') || name.contains("--") { | |
| 212 | + return Err("Names cannot start or end with a hyphen or contain two in a row."); | |
| 213 | + } | |
| 214 | + if RESERVED_NAMES.contains(&name) { | |
| 215 | + return Err("That name is reserved."); | |
| 216 | + } | |
| 217 | + Ok(()) | |
| 218 | +} | |
| 219 | + | |
| 220 | +/// Top-level paths the site uses itself. | |
| 221 | +pub const RESERVED_NAMES: &[&str] = &[ | |
| 222 | + "about", "admin", "api", "assets", "avatars", "dashboard", "dev-ws", "docs", "download", "explore", "help", | |
| 223 | + "install", "join", "login", "logout", "new", "notifications", "organizations", "orgs", "register", "repos", | |
| 224 | + "search", "settings", "signup", "site", "static", "status", "support", "users", "v2", "www", "irongit", | |
| 225 | + "root", "git", "ssh", "lfs", "registry", "security", "terms", "privacy", "favicon", "robots", "sitemap", | |
| 226 | +]; | |
| 227 | + | |
| 228 | +/// Repo names: letters, digits, '.', '_' and '-', up to 100 characters. | |
| 229 | +pub fn valid_repo_name(name: &str) -> Result<(), &'static str> { | |
| 230 | + if name.is_empty() || name.len() > 100 { | |
| 231 | + return Err("Repository names must be 1 to 100 characters."); | |
| 232 | + } | |
| 233 | + if !name.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-')) { | |
| 234 | + return Err("Repository names may only contain letters, digits, '.', '_' and '-'."); | |
| 235 | + } | |
| 236 | + if name.starts_with('.') || name.ends_with(".git") || name == "-" { | |
| 237 | + return Err("Repository names cannot start with '.' or end with '.git'."); | |
| 238 | + } | |
| 239 | + Ok(()) | |
| 240 | +} | |
| 241 | + | |
| 242 | +/// Image names below the owner: OCI path components joined by '/'. | |
| 243 | +pub fn valid_package_name(name: &str) -> Result<(), &'static str> { | |
| 244 | + static PATTERN: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| { | |
| 245 | + regex::Regex::new(r"^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$").unwrap() | |
| 246 | + }); | |
| 247 | + if name.is_empty() || name.len() > 200 || !PATTERN.is_match(name) { | |
| 248 | + return Err("Image names must be lowercase path components like 'api' or 'tools/builder'."); | |
| 249 | + } | |
| 250 | + Ok(()) | |
| 251 | +} |
+229-0backend/src/ops.rs
| @@ -0,0 +1,229 @@ | ||
| 1 | +//! Operations shared by the web UI, the JSON API and the admin commands, so | |
| 2 | +//! each rule (names, permissions, quotas, audit) is enforced in one place. | |
| 3 | + | |
| 4 | +use serde_json::json; | |
| 5 | + | |
| 6 | +use crate::{ | |
| 7 | + analytics, | |
| 8 | + auth::{self, Viewer}, | |
| 9 | + error::{AppError, AppResult}, | |
| 10 | + git::Git, | |
| 11 | + models::{self, Account, Repo, audit}, | |
| 12 | + perm, | |
| 13 | + state::AppState, | |
| 14 | +}; | |
| 15 | + | |
| 16 | +pub struct NewUser<'a> { | |
| 17 | + pub username: &'a str, | |
| 18 | + pub email: &'a str, | |
| 19 | + /// None for Google-only accounts. | |
| 20 | + pub password: Option<&'a str>, | |
| 21 | + pub display_name: &'a str, | |
| 22 | + /// Set when the email came from a provider that verified it (Google). | |
| 23 | + pub email_verified: bool, | |
| 24 | +} | |
| 25 | + | |
| 26 | +/// Creates a user. The first user on a fresh install becomes site admin. | |
| 27 | +pub async fn register_user(state: &AppState, new: NewUser<'_>) -> AppResult<i64> { | |
| 28 | + let username = new.username.trim().to_ascii_lowercase(); | |
| 29 | + models::valid_account_name(&username).map_err(AppError::bad)?; | |
| 30 | + let email = new.email.trim(); | |
| 31 | + if !valid_email(email) { | |
| 32 | + return Err(AppError::bad("Enter a valid email address.")); | |
| 33 | + } | |
| 34 | + let password_hash = match new.password { | |
| 35 | + Some(password) => { | |
| 36 | + auth::validate_password(password).map_err(AppError::bad)?; | |
| 37 | + Some(auth::hash_password_async(password.to_string()).await?) | |
| 38 | + } | |
| 39 | + None => None, | |
| 40 | + }; | |
| 41 | + | |
| 42 | + let mut tx = state.db.begin().await?; | |
| 43 | + let taken: bool = sqlx::query_scalar("select exists(select 1 from accounts where name = $1)") | |
| 44 | + .bind(&username) | |
| 45 | + .fetch_one(&mut *tx) | |
| 46 | + .await?; | |
| 47 | + if taken { | |
| 48 | + return Err(AppError::conflict("That username is taken.")); | |
| 49 | + } | |
| 50 | + let email_taken: bool = sqlx::query_scalar("select exists(select 1 from emails where email = $1)") | |
| 51 | + .bind(email) | |
| 52 | + .fetch_one(&mut *tx) | |
| 53 | + .await?; | |
| 54 | + if email_taken { | |
| 55 | + return Err(AppError::conflict("An account already uses that email. Sign in instead.")); | |
| 56 | + } | |
| 57 | + let first_user: bool = sqlx::query_scalar("select not exists(select 1 from users)").fetch_one(&mut *tx).await?; | |
| 58 | + let id: i64 = sqlx::query_scalar("insert into accounts (kind, name, display_name) values ('user', $1, $2) returning id") | |
| 59 | + .bind(&username) | |
| 60 | + .bind(new.display_name.trim()) | |
| 61 | + .fetch_one(&mut *tx) | |
| 62 | + .await?; | |
| 63 | + sqlx::query("insert into users (account_id, password_hash, is_admin) values ($1, $2, $3)") | |
| 64 | + .bind(id) | |
| 65 | + .bind(password_hash) | |
| 66 | + .bind(first_user) | |
| 67 | + .execute(&mut *tx) | |
| 68 | + .await?; | |
| 69 | + sqlx::query("insert into emails (user_id, email, is_primary, verified_at) values ($1, $2, true, case when $3 then now() end)") | |
| 70 | + .bind(id) | |
| 71 | + .bind(email) | |
| 72 | + .bind(new.email_verified) | |
| 73 | + .execute(&mut *tx) | |
| 74 | + .await?; | |
| 75 | + tx.commit().await?; | |
| 76 | + | |
| 77 | + if !new.email_verified { | |
| 78 | + if let Err(error) = send_verification(state, id, email).await { | |
| 79 | + tracing::warn!(%error, email, "could not send verification email"); | |
| 80 | + } | |
| 81 | + } | |
| 82 | + audit(&state.db, Some(id), "user.register", &username, json!({ "first_user": first_user, "google": new.password.is_none() }), None).await; | |
| 83 | + analytics::track(state, "user_registered", Some(&username), "/register", json!({ "method": if new.password.is_some() { "password" } else { "google" } })); | |
| 84 | + tracing::info!(user = %username, admin = first_user, "user registered"); | |
| 85 | + Ok(id) | |
| 86 | +} | |
| 87 | + | |
| 88 | +pub fn valid_email(email: &str) -> bool { | |
| 89 | + let Some((local, domain)) = email.split_once('@') else { return false }; | |
| 90 | + !local.is_empty() && domain.contains('.') && !domain.starts_with('.') && !domain.ends_with('.') && email.len() <= 254 && !email.contains(char::is_whitespace) | |
| 91 | +} | |
| 92 | + | |
| 93 | +/// Emails a verification link. Without SMTP the link is written to the log. | |
| 94 | +pub async fn send_verification(state: &AppState, user_id: i64, email: &str) -> anyhow::Result<()> { | |
| 95 | + let token = auth::random_token(24); | |
| 96 | + sqlx::query("update emails set verify_token_hash = $3, verify_sent_at = now() where user_id = $1 and email = $2") | |
| 97 | + .bind(user_id) | |
| 98 | + .bind(email) | |
| 99 | + .bind(auth::sha256_hex(&token)) | |
| 100 | + .execute(&state.db) | |
| 101 | + .await?; | |
| 102 | + let link = format!("{}/settings/emails/verify?token={token}", state.config.base_url()); | |
| 103 | + crate::mail::send( | |
| 104 | + state, | |
| 105 | + email, | |
| 106 | + "Verify your email for irongit", | |
| 107 | + &format!("Confirm this address so commits made with it count on your profile:\n\n{link}\n\nIf you did not sign up, ignore this email."), | |
| 108 | + ) | |
| 109 | + .await?; | |
| 110 | + tracing::info!(email, %link, "verification link issued"); | |
| 111 | + Ok(()) | |
| 112 | +} | |
| 113 | + | |
| 114 | +/// Creates a repository row and its bare repo on disk. | |
| 115 | +pub async fn create_repo(state: &AppState, actor: &Viewer, owner: &Account, name: &str, description: &str, visibility: &str) -> AppResult<Repo> { | |
| 116 | + let name = name.trim(); | |
| 117 | + models::valid_repo_name(name).map_err(AppError::bad)?; | |
| 118 | + if !matches!(visibility, "public" | "private") { | |
| 119 | + return Err(AppError::bad("Visibility must be public or private.")); | |
| 120 | + } | |
| 121 | + if !perm::can_create_under(&state.db, actor, owner).await? { | |
| 122 | + return Err(AppError::forbidden(format!("You cannot create repositories under {}.", owner.name))); | |
| 123 | + } | |
| 124 | + let description: String = description.trim().chars().take(350).collect(); | |
| 125 | + let id: i64 = match sqlx::query_scalar( | |
| 126 | + "insert into repos (owner_id, name, description, visibility) values ($1, $2, $3, $4) returning id", | |
| 127 | + ) | |
| 128 | + .bind(owner.id) | |
| 129 | + .bind(name) | |
| 130 | + .bind(&description) | |
| 131 | + .bind(visibility) | |
| 132 | + .fetch_one(&state.db) | |
| 133 | + .await | |
| 134 | + { | |
| 135 | + Ok(id) => id, | |
| 136 | + Err(sqlx::Error::Database(e)) if e.is_unique_violation() => { | |
| 137 | + return Err(AppError::conflict(format!("{}/{name} already exists.", owner.name))); | |
| 138 | + } | |
| 139 | + Err(e) => return Err(e.into()), | |
| 140 | + }; | |
| 141 | + | |
| 142 | + let path = models::repo_disk_path(&state.config, id); | |
| 143 | + if let Err(error) = Git::init_bare(&path, "main").await { | |
| 144 | + sqlx::query("delete from repos where id = $1").bind(id).execute(&state.db).await?; | |
| 145 | + return Err(AppError::Internal(error.context("initializing repository on disk"))); | |
| 146 | + } | |
| 147 | + let repo = Repo::by_id(&state.db, id).await?.ok_or(AppError::NotFound)?; | |
| 148 | + audit(&state.db, Some(actor.id), "repo.create", &repo.full_name(), json!({ "visibility": visibility }), None).await; | |
| 149 | + analytics::track(state, "repo_created", Some(&actor.name), &repo.url(), json!({ "visibility": visibility, "owner_kind": owner.kind })); | |
| 150 | + tracing::info!(repo = %repo.full_name(), actor = %actor.name, "repository created"); | |
| 151 | + Ok(repo) | |
| 152 | +} | |
| 153 | + | |
| 154 | +/// Deletes a repository: the row first (so it vanishes immediately), then | |
| 155 | +/// the files in the background. | |
| 156 | +pub async fn delete_repo(state: &AppState, actor: &Viewer, repo: &Repo) -> AppResult<()> { | |
| 157 | + let access = perm::repo_access(&state.db, Some(actor), repo, perm::Area::Repo).await?; | |
| 158 | + if !access.is_admin() { | |
| 159 | + return Err(AppError::forbidden("Only repository admins can delete it.")); | |
| 160 | + } | |
| 161 | + sqlx::query("delete from repos where id = $1").bind(repo.id).execute(&state.db).await?; | |
| 162 | + let path = repo.disk_path(&state.config); | |
| 163 | + let trash = state.config.data_dir.join("trash").join(format!("{}-{}", repo.id, chrono::Utc::now().timestamp())); | |
| 164 | + tokio::spawn(async move { | |
| 165 | + if let Some(parent) = trash.parent() { | |
| 166 | + let _ = tokio::fs::create_dir_all(parent).await; | |
| 167 | + } | |
| 168 | + let target = if tokio::fs::rename(&path, &trash).await.is_ok() { trash } else { path }; | |
| 169 | + if let Err(error) = tokio::fs::remove_dir_all(&target).await { | |
| 170 | + tracing::warn!(%error, path = %target.display(), "could not remove deleted repository files"); | |
| 171 | + } | |
| 172 | + }); | |
| 173 | + audit(&state.db, Some(actor.id), "repo.delete", &repo.full_name(), json!({}), None).await; | |
| 174 | + analytics::track(state, "repo_deleted", Some(&actor.name), &repo.url(), json!({})); | |
| 175 | + Ok(()) | |
| 176 | +} | |
| 177 | + | |
| 178 | +pub async fn set_repo_visibility(state: &AppState, actor: &Viewer, repo: &Repo, visibility: &str) -> AppResult<()> { | |
| 179 | + if !matches!(visibility, "public" | "private") { | |
| 180 | + return Err(AppError::bad("Visibility must be public or private.")); | |
| 181 | + } | |
| 182 | + let access = perm::repo_access(&state.db, Some(actor), repo, perm::Area::Repo).await?; | |
| 183 | + if !access.is_admin() { | |
| 184 | + return Err(AppError::forbidden("Only repository admins can change visibility.")); | |
| 185 | + } | |
| 186 | + sqlx::query("update repos set visibility = $2, updated_at = now() where id = $1") | |
| 187 | + .bind(repo.id) | |
| 188 | + .bind(visibility) | |
| 189 | + .execute(&state.db) | |
| 190 | + .await?; | |
| 191 | + audit(&state.db, Some(actor.id), "repo.visibility", &repo.full_name(), json!({ "visibility": visibility }), None).await; | |
| 192 | + analytics::track(state, "repo_visibility_changed", Some(&actor.name), &repo.url(), json!({ "visibility": visibility })); | |
| 193 | + Ok(()) | |
| 194 | +} | |
| 195 | + | |
| 196 | +/// Creates an organization with `actor` as its first owner. | |
| 197 | +pub async fn create_org(state: &AppState, actor: &Viewer, name: &str, display_name: &str) -> AppResult<Account> { | |
| 198 | + let name = name.trim().to_ascii_lowercase(); | |
| 199 | + models::valid_account_name(&name).map_err(AppError::bad)?; | |
| 200 | + let mut tx = state.db.begin().await?; | |
| 201 | + let id: i64 = match sqlx::query_scalar("insert into accounts (kind, name, display_name) values ('org', $1, $2) returning id") | |
| 202 | + .bind(&name) | |
| 203 | + .bind(display_name.trim()) | |
| 204 | + .fetch_one(&mut *tx) | |
| 205 | + .await | |
| 206 | + { | |
| 207 | + Ok(id) => id, | |
| 208 | + Err(sqlx::Error::Database(e)) if e.is_unique_violation() => return Err(AppError::conflict("That name is taken.")), | |
| 209 | + Err(e) => return Err(e.into()), | |
| 210 | + }; | |
| 211 | + sqlx::query("insert into org_members (org_id, user_id, role) values ($1, $2, 'owner')") | |
| 212 | + .bind(id) | |
| 213 | + .bind(actor.id) | |
| 214 | + .execute(&mut *tx) | |
| 215 | + .await?; | |
| 216 | + tx.commit().await?; | |
| 217 | + audit(&state.db, Some(actor.id), "org.create", &name, json!({}), None).await; | |
| 218 | + analytics::track(state, "org_created", Some(&actor.name), &format!("/{name}"), json!({})); | |
| 219 | + Account::by_id(&state.db, id).await?.ok_or(AppError::NotFound) | |
| 220 | +} | |
| 221 | + | |
| 222 | +/// Bytes an account uses across its repositories, for quota checks. | |
| 223 | +pub async fn account_usage(state: &AppState, account_id: i64) -> AppResult<u64> { | |
| 224 | + let used: Option<i64> = sqlx::query_scalar("select sum(size_bytes)::bigint from repos where owner_id = $1") | |
| 225 | + .bind(account_id) | |
| 226 | + .fetch_one(&state.db) | |
| 227 | + .await?; | |
| 228 | + Ok(used.unwrap_or(0).max(0) as u64) | |
| 229 | +} |
+161-0backend/src/perm.rs
| @@ -0,0 +1,161 @@ | ||
| 1 | +//! Every access decision goes through here: web pages, the API, git over | |
| 2 | +//! HTTP and SSH, LFS, the registry, raw files and archives. Keeping one | |
| 3 | +//! implementation is what stops a private repo leaking through a side door. | |
| 4 | + | |
| 5 | +use sqlx::PgPool; | |
| 6 | + | |
| 7 | +use crate::{ | |
| 8 | + auth::Viewer, | |
| 9 | + models::{Account, Package, Repo}, | |
| 10 | +}; | |
| 11 | + | |
| 12 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | |
| 13 | +pub enum Access { | |
| 14 | + None, | |
| 15 | + Read, | |
| 16 | + Write, | |
| 17 | + Admin, | |
| 18 | +} | |
| 19 | + | |
| 20 | +impl Access { | |
| 21 | + pub fn can_read(self) -> bool { | |
| 22 | + self >= Access::Read | |
| 23 | + } | |
| 24 | + | |
| 25 | + pub fn can_write(self) -> bool { | |
| 26 | + self >= Access::Write | |
| 27 | + } | |
| 28 | + | |
| 29 | + pub fn is_admin(self) -> bool { | |
| 30 | + self == Access::Admin | |
| 31 | + } | |
| 32 | + | |
| 33 | + fn from_permission(permission: &str) -> Self { | |
| 34 | + match permission { | |
| 35 | + "admin" => Access::Admin, | |
| 36 | + "write" => Access::Write, | |
| 37 | + "read" => Access::Read, | |
| 38 | + _ => Access::None, | |
| 39 | + } | |
| 40 | + } | |
| 41 | +} | |
| 42 | + | |
| 43 | +/// Which credential scope an operation needs. A token without the scope is | |
| 44 | +/// treated as if nobody were signed in. | |
| 45 | +#[derive(Debug, Clone, Copy)] | |
| 46 | +pub enum Area { | |
| 47 | + Repo, | |
| 48 | + Packages, | |
| 49 | +} | |
| 50 | + | |
| 51 | +fn scoped(viewer: Option<&Viewer>, area: Area) -> Option<&Viewer> { | |
| 52 | + viewer.filter(|v| match area { | |
| 53 | + Area::Repo => v.scopes.repo, | |
| 54 | + Area::Packages => v.scopes.packages, | |
| 55 | + }) | |
| 56 | +} | |
| 57 | + | |
| 58 | +/// What `viewer` may do with everything an account owns, before per-repo | |
| 59 | +/// grants: admin for yourself, your org as owner, or a site admin; write as | |
| 60 | +/// an org member. | |
| 61 | +pub async fn owner_access(db: &PgPool, viewer: Option<&Viewer>, owner_id: i64) -> sqlx::Result<Access> { | |
| 62 | + let Some(viewer) = viewer else { return Ok(Access::None) }; | |
| 63 | + if viewer.site_admin() || viewer.id == owner_id { | |
| 64 | + return Ok(Access::Admin); | |
| 65 | + } | |
| 66 | + let role: Option<String> = sqlx::query_scalar("select role from org_members where org_id = $1 and user_id = $2") | |
| 67 | + .bind(owner_id) | |
| 68 | + .bind(viewer.id) | |
| 69 | + .fetch_optional(db) | |
| 70 | + .await?; | |
| 71 | + Ok(match role.as_deref() { | |
| 72 | + Some("owner") => Access::Admin, | |
| 73 | + Some("member") => Access::Write, | |
| 74 | + _ => Access::None, | |
| 75 | + }) | |
| 76 | +} | |
| 77 | + | |
| 78 | +/// Whether `viewer` may create repos and images under `owner`. | |
| 79 | +pub async fn can_create_under(db: &PgPool, viewer: &Viewer, owner: &Account) -> sqlx::Result<bool> { | |
| 80 | + Ok(owner_access(db, Some(viewer), owner.id).await?.can_write()) | |
| 81 | +} | |
| 82 | + | |
| 83 | +pub async fn repo_access(db: &PgPool, viewer: Option<&Viewer>, repo: &Repo, area: Area) -> sqlx::Result<Access> { | |
| 84 | + let viewer = scoped(viewer, area); | |
| 85 | + let mut access = owner_access(db, viewer, repo.owner_id).await?; | |
| 86 | + if access < Access::Admin { | |
| 87 | + if let Some(viewer) = viewer { | |
| 88 | + let permission: Option<String> = | |
| 89 | + sqlx::query_scalar("select permission from repo_collaborators where repo_id = $1 and user_id = $2") | |
| 90 | + .bind(repo.id) | |
| 91 | + .bind(viewer.id) | |
| 92 | + .fetch_optional(db) | |
| 93 | + .await?; | |
| 94 | + if let Some(permission) = permission { | |
| 95 | + access = access.max(Access::from_permission(&permission)); | |
| 96 | + } | |
| 97 | + } | |
| 98 | + } | |
| 99 | + if repo.is_public() { | |
| 100 | + access = access.max(Access::Read); | |
| 101 | + } | |
| 102 | + // Archived repos are read-only for everyone; admins can still unarchive. | |
| 103 | + if repo.archived && access == Access::Write { | |
| 104 | + access = Access::Read; | |
| 105 | + } | |
| 106 | + Ok(access) | |
| 107 | +} | |
| 108 | + | |
| 109 | +/// Image access is independent of code visibility. A linked repo only adds | |
| 110 | +/// grants for its owner-side people and collaborators, never "public". | |
| 111 | +pub async fn package_access(db: &PgPool, viewer: Option<&Viewer>, package: &Package) -> sqlx::Result<Access> { | |
| 112 | + let viewer = scoped(viewer, Area::Packages); | |
| 113 | + let mut access = owner_access(db, viewer, package.owner_id).await?; | |
| 114 | + if access < Access::Admin { | |
| 115 | + if let (Some(viewer), Some(repo_id)) = (viewer, package.repo_id) { | |
| 116 | + let permission: Option<String> = | |
| 117 | + sqlx::query_scalar("select permission from repo_collaborators where repo_id = $1 and user_id = $2") | |
| 118 | + .bind(repo_id) | |
| 119 | + .bind(viewer.id) | |
| 120 | + .fetch_optional(db) | |
| 121 | + .await?; | |
| 122 | + if let Some(permission) = permission { | |
| 123 | + access = access.max(Access::from_permission(&permission)); | |
| 124 | + } | |
| 125 | + } | |
| 126 | + } | |
| 127 | + if package.is_public() { | |
| 128 | + access = access.max(Access::Read); | |
| 129 | + } | |
| 130 | + Ok(access) | |
| 131 | +} | |
| 132 | + | |
| 133 | +/// SQL condition limiting `repos r` to what a viewer may read, for listings: | |
| 134 | +/// `concat!(repo_select!(), " where ", visible_repos!(), " and ...")`. | |
| 135 | +/// Binds: $1 = viewer id (or null), $2 = site admin flag. Use `visibility_binds`. | |
| 136 | +#[macro_export] | |
| 137 | +macro_rules! visible_repos { | |
| 138 | + () => { | |
| 139 | + "(r.visibility = 'public' or $2 or r.owner_id = $1 \ | |
| 140 | + or exists (select 1 from org_members m where m.org_id = r.owner_id and m.user_id = $1) \ | |
| 141 | + or exists (select 1 from repo_collaborators c where c.repo_id = r.id and c.user_id = $1))" | |
| 142 | + }; | |
| 143 | +} | |
| 144 | + | |
| 145 | +/// Same as `visible_repos!` for `packages p`. | |
| 146 | +#[macro_export] | |
| 147 | +macro_rules! visible_packages { | |
| 148 | + () => { | |
| 149 | + "(p.visibility = 'public' or $2 or p.owner_id = $1 \ | |
| 150 | + or exists (select 1 from org_members m where m.org_id = p.owner_id and m.user_id = $1) \ | |
| 151 | + or (p.repo_id is not null and exists (select 1 from repo_collaborators c where c.repo_id = p.repo_id and c.user_id = $1)))" | |
| 152 | + }; | |
| 153 | +} | |
| 154 | + | |
| 155 | +/// The ($1, $2) binds for the listing fragments above. | |
| 156 | +pub fn visibility_binds(viewer: Option<&Viewer>, area: Area) -> (Option<i64>, bool) { | |
| 157 | + match scoped(viewer, area) { | |
| 158 | + Some(v) => (Some(v.id), v.site_admin()), | |
| 159 | + None => (None, false), | |
| 160 | + } | |
| 161 | +} |
+193-0backend/src/push.rs
| @@ -0,0 +1,193 @@ | ||
| 1 | +//! What happens after a push lands: activity events, the repo's size and | |
| 2 | +//! default branch, and contribution counts for the heatmap. | |
| 3 | +//! | |
| 4 | +//! Called with the ref snapshot taken before receive-pack ran, so it works | |
| 5 | +//! the same for HTTP and SSH and needs no post-receive hook. | |
| 6 | + | |
| 7 | +use std::collections::{BTreeMap, HashMap}; | |
| 8 | + | |
| 9 | +use serde_json::json; | |
| 10 | + | |
| 11 | +use crate::{ | |
| 12 | + analytics, | |
| 13 | + auth::Viewer, | |
| 14 | + git::{Git, ZERO_SHA}, | |
| 15 | + models::Repo, | |
| 16 | + state::AppState, | |
| 17 | +}; | |
| 18 | + | |
| 19 | +/// Contributions considered per push; a huge first push is capped. | |
| 20 | +const MAX_CONTRIBUTION_COMMITS: usize = 50_000; | |
| 21 | + | |
| 22 | +#[derive(Debug, Clone)] | |
| 23 | +pub struct RefUpdate { | |
| 24 | + pub name: String, | |
| 25 | + pub old: String, | |
| 26 | + pub new: String, | |
| 27 | +} | |
| 28 | + | |
| 29 | +pub fn diff_refs(before: &BTreeMap<String, String>, after: &BTreeMap<String, String>) -> Vec<RefUpdate> { | |
| 30 | + let mut updates = Vec::new(); | |
| 31 | + for (name, new) in after { | |
| 32 | + match before.get(name) { | |
| 33 | + Some(old) if old == new => {} | |
| 34 | + Some(old) => updates.push(RefUpdate { name: name.clone(), old: old.clone(), new: new.clone() }), | |
| 35 | + None => updates.push(RefUpdate { name: name.clone(), old: ZERO_SHA.into(), new: new.clone() }), | |
| 36 | + } | |
| 37 | + } | |
| 38 | + for (name, old) in before { | |
| 39 | + if !after.contains_key(name) { | |
| 40 | + updates.push(RefUpdate { name: name.clone(), old: old.clone(), new: ZERO_SHA.into() }); | |
| 41 | + } | |
| 42 | + } | |
| 43 | + updates | |
| 44 | +} | |
| 45 | + | |
| 46 | +/// Runs in the background after receive-pack exits successfully. | |
| 47 | +pub fn spawn_after_push(state: AppState, repo: Repo, pusher: Option<Viewer>, before: BTreeMap<String, String>, via: &'static str) { | |
| 48 | + tokio::spawn(async move { | |
| 49 | + if let Err(error) = after_push(&state, &repo, pusher.as_ref(), &before, via).await { | |
| 50 | + tracing::error!(repo = %repo.full_name(), error = ?error, "post-push processing failed"); | |
| 51 | + } | |
| 52 | + }); | |
| 53 | +} | |
| 54 | + | |
| 55 | +pub async fn after_push(state: &AppState, repo: &Repo, pusher: Option<&Viewer>, before: &BTreeMap<String, String>, via: &str) -> anyhow::Result<()> { | |
| 56 | + let git = Git::new(repo.disk_path(&state.config)); | |
| 57 | + let after = git.ref_snapshot().await?; | |
| 58 | + let updates = diff_refs(before, &after); | |
| 59 | + if updates.is_empty() { | |
| 60 | + return Ok(()); | |
| 61 | + } | |
| 62 | + | |
| 63 | + // A first push of "master" into a repo whose default is "main" should | |
| 64 | + // make "master" the default, like every other forge does. | |
| 65 | + let mut default_branch = repo.default_branch.clone(); | |
| 66 | + let has_default = after.contains_key(&format!("refs/heads/{default_branch}")); | |
| 67 | + let branches: Vec<&str> = after.keys().filter_map(|k| k.strip_prefix("refs/heads/")).collect(); | |
| 68 | + if !has_default && !branches.is_empty() { | |
| 69 | + let pick = ["main", "master", "trunk", "develop"] | |
| 70 | + .into_iter() | |
| 71 | + .find(|b| branches.contains(b)) | |
| 72 | + .or_else(|| updates.iter().find_map(|u| u.name.strip_prefix("refs/heads/").filter(|_| u.new != ZERO_SHA))) | |
| 73 | + .unwrap_or(branches[0]) | |
| 74 | + .to_string(); | |
| 75 | + git.set_head(&pick).await?; | |
| 76 | + default_branch = pick; | |
| 77 | + } | |
| 78 | + | |
| 79 | + let size = git.disk_size().await.unwrap_or(repo.size_bytes as u64); | |
| 80 | + sqlx::query( | |
| 81 | + "update repos set pushed_at = now(), updated_at = now(), is_empty = $2, size_bytes = $3, default_branch = $4 where id = $1", | |
| 82 | + ) | |
| 83 | + .bind(repo.id) | |
| 84 | + .bind(branches.is_empty()) | |
| 85 | + .bind(size as i64) | |
| 86 | + .bind(&default_branch) | |
| 87 | + .execute(&state.db) | |
| 88 | + .await?; | |
| 89 | + | |
| 90 | + let default_ref = format!("refs/heads/{default_branch}"); | |
| 91 | + let mut total_commits = 0usize; | |
| 92 | + for update in &updates { | |
| 93 | + let (count, subject) = if update.new == ZERO_SHA { | |
| 94 | + (0, String::new()) | |
| 95 | + } else { | |
| 96 | + let commits = git.new_commits(&update.old, &update.new, 10_000).await.unwrap_or_default(); | |
| 97 | + let subject = git.commit(&update.new).await.ok().flatten().map(|c| c.subject).unwrap_or_default(); | |
| 98 | + (commits.len(), subject) | |
| 99 | + }; | |
| 100 | + total_commits += count; | |
| 101 | + sqlx::query( | |
| 102 | + "insert into push_events (repo_id, pusher_id, ref_name, before_sha, after_sha, commit_count, head_message, via) | |
| 103 | + values ($1, $2, $3, $4, $5, $6, $7, $8)", | |
| 104 | + ) | |
| 105 | + .bind(repo.id) | |
| 106 | + .bind(pusher.map(|p| p.id)) | |
| 107 | + .bind(&update.name) | |
| 108 | + .bind(&update.old) | |
| 109 | + .bind(&update.new) | |
| 110 | + .bind(count as i32) | |
| 111 | + .bind(subject.chars().take(300).collect::<String>()) | |
| 112 | + .bind(via) | |
| 113 | + .execute(&state.db) | |
| 114 | + .await?; | |
| 115 | + | |
| 116 | + if update.name == default_ref && update.new != ZERO_SHA { | |
| 117 | + record_contributions(state, repo.id, &git, &update.old, &update.new).await?; | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + tracing::info!(repo = %repo.full_name(), pusher = pusher.map(|p| p.name.as_str()).unwrap_or("-"), refs = updates.len(), commits = total_commits, via, "push processed"); | |
| 122 | + analytics::track( | |
| 123 | + state, | |
| 124 | + "git_push", | |
| 125 | + pusher.map(|p| p.name.as_str()), | |
| 126 | + &repo.url(), | |
| 127 | + json!({ "refs": updates.len(), "commits": total_commits, "via": via, "visibility": repo.visibility }), | |
| 128 | + ); | |
| 129 | + Ok(()) | |
| 130 | +} | |
| 131 | + | |
| 132 | +/// Credits commits that landed on the default branch to users who verified | |
| 133 | +/// the author email. Keyed by (repo, sha), so re-pushes never double count. | |
| 134 | +async fn record_contributions(state: &AppState, repo_id: i64, git: &Git, old: &str, new: &str) -> anyhow::Result<()> { | |
| 135 | + let commits = git.new_commits(old, new, MAX_CONTRIBUTION_COMMITS).await?; | |
| 136 | + if commits.is_empty() { | |
| 137 | + return Ok(()); | |
| 138 | + } | |
| 139 | + let mut emails: Vec<String> = commits.iter().map(|c| c.author_email.to_lowercase()).collect(); | |
| 140 | + emails.sort(); | |
| 141 | + emails.dedup(); | |
| 142 | + let owners: HashMap<String, i64> = sqlx::query_as::<_, (String, i64)>( | |
| 143 | + "select lower(email::text), user_id from emails where verified_at is not null and email = any($1::citext[])", | |
| 144 | + ) | |
| 145 | + .bind(&emails) | |
| 146 | + .fetch_all(&state.db) | |
| 147 | + .await? | |
| 148 | + .into_iter() | |
| 149 | + .collect(); | |
| 150 | + if owners.is_empty() { | |
| 151 | + return Ok(()); | |
| 152 | + } | |
| 153 | + | |
| 154 | + let (mut shas, mut users, mut days) = (Vec::new(), Vec::new(), Vec::new()); | |
| 155 | + for commit in &commits { | |
| 156 | + if let Some(user) = owners.get(&commit.author_email.to_lowercase()) { | |
| 157 | + shas.push(commit.sha.clone()); | |
| 158 | + users.push(*user); | |
| 159 | + days.push(commit.authored_at.date_naive()); | |
| 160 | + } | |
| 161 | + } | |
| 162 | + let inserted = sqlx::query( | |
| 163 | + "insert into contribution_commits (repo_id, sha, user_id, day) | |
| 164 | + select $1, * from unnest($2::text[], $3::bigint[], $4::date[]) | |
| 165 | + on conflict do nothing", | |
| 166 | + ) | |
| 167 | + .bind(repo_id) | |
| 168 | + .bind(&shas) | |
| 169 | + .bind(&users) | |
| 170 | + .bind(&days) | |
| 171 | + .execute(&state.db) | |
| 172 | + .await? | |
| 173 | + .rows_affected(); | |
| 174 | + tracing::info!(repo_id, credited = inserted, "contributions recorded"); | |
| 175 | + Ok(()) | |
| 176 | +} | |
| 177 | + | |
| 178 | +#[cfg(test)] | |
| 179 | +mod tests { | |
| 180 | + use super::*; | |
| 181 | + | |
| 182 | + #[test] | |
| 183 | + fn diffs_created_updated_deleted() { | |
| 184 | + let before = BTreeMap::from([("refs/heads/a".to_string(), "1".to_string()), ("refs/heads/b".to_string(), "2".to_string())]); | |
| 185 | + let after = BTreeMap::from([("refs/heads/a".to_string(), "3".to_string()), ("refs/heads/c".to_string(), "4".to_string())]); | |
| 186 | + let mut updates = diff_refs(&before, &after); | |
| 187 | + updates.sort_by(|x, y| x.name.cmp(&y.name)); | |
| 188 | + assert_eq!(updates.len(), 3); | |
| 189 | + assert_eq!((updates[0].old.as_str(), updates[0].new.as_str()), ("1", "3")); | |
| 190 | + assert_eq!(updates[1].new, ZERO_SHA); | |
| 191 | + assert_eq!(updates[2].old, ZERO_SHA); | |
| 192 | + } | |
| 193 | +} |
+13-0backend/src/registry/mod.rs
| @@ -0,0 +1,13 @@ | ||
| 1 | +//! OCI distribution API (Docker registry) at /v2/. Owned by the registry | |
| 2 | +//! work; this stub only keeps the crate compiling. | |
| 3 | + | |
| 4 | +use axum::Router; | |
| 5 | + | |
| 6 | +use crate::state::AppState; | |
| 7 | + | |
| 8 | +pub fn router() -> Router<AppState> { | |
| 9 | + Router::new() | |
| 10 | +} | |
| 11 | + | |
| 12 | +/// Garbage collection of orphaned blobs and stale uploads. | |
| 13 | +pub fn spawn_gc(_state: AppState) {} |
+67-0backend/src/routes.rs
| @@ -0,0 +1,67 @@ | ||
| 1 | +//! The whole HTTP surface, assembled from each module's router. | |
| 2 | + | |
| 3 | +use std::time::Duration; | |
| 4 | + | |
| 5 | +use axum::{ | |
| 6 | + Json, Router, | |
| 7 | + extract::Request, | |
| 8 | + http::{HeaderValue, header}, | |
| 9 | + middleware::{self, Next}, | |
| 10 | + response::Response, | |
| 11 | + routing::get, | |
| 12 | +}; | |
| 13 | +use serde_json::json; | |
| 14 | +use tower_http::trace::TraceLayer; | |
| 15 | +use tracing::Span; | |
| 16 | + | |
| 17 | +use crate::{api, clientlog, frontend, git_http, lfs, registry, state::AppState, web}; | |
| 18 | + | |
| 19 | +pub fn router(state: AppState) -> Router { | |
| 20 | + let app = Router::new() | |
| 21 | + .route("/api/health", get(health)) | |
| 22 | + .merge(api::router()) | |
| 23 | + .merge(clientlog::router()) | |
| 24 | + .merge(registry::router()) | |
| 25 | + .merge(git_http::router()) | |
| 26 | + .merge(lfs::router()) | |
| 27 | + .merge(web::router()) | |
| 28 | + .route("/_astro/{*path}", get(frontend::serve)); | |
| 29 | + | |
| 30 | + #[cfg(feature = "hot-reload")] | |
| 31 | + let app = app.route("/dev-ws", get(crate::dev::ws_handler)); | |
| 32 | + | |
| 33 | + app.fallback(frontend::serve) | |
| 34 | + .layer(middleware::from_fn(security_headers)) | |
| 35 | + .layer( | |
| 36 | + TraceLayer::new_for_http() | |
| 37 | + .make_span_with(|request: &Request| { | |
| 38 | + tracing::info_span!("http", method = %request.method(), path = %request.uri().path()) | |
| 39 | + }) | |
| 40 | + .on_request(()) | |
| 41 | + .on_response(|response: &Response, latency: Duration, _span: &Span| { | |
| 42 | + let status = response.status().as_u16(); | |
| 43 | + let ms = latency.as_millis() as u64; | |
| 44 | + if status >= 500 { | |
| 45 | + tracing::error!(status, ms, "request"); | |
| 46 | + } else { | |
| 47 | + tracing::info!(status, ms, "request"); | |
| 48 | + } | |
| 49 | + }), | |
| 50 | + ) | |
| 51 | + .with_state(state) | |
| 52 | +} | |
| 53 | + | |
| 54 | +async fn health() -> Json<serde_json::Value> { | |
| 55 | + Json(json!({ "status": "ok", "version": env!("CARGO_PKG_VERSION") })) | |
| 56 | +} | |
| 57 | + | |
| 58 | +/// Defaults every response gets unless a handler set its own (raw file | |
| 59 | +/// views set a stricter CSP). | |
| 60 | +async fn security_headers(request: Request, next: Next) -> Response { | |
| 61 | + let mut response = next.run(request).await; | |
| 62 | + let headers = response.headers_mut(); | |
| 63 | + headers.entry(header::X_CONTENT_TYPE_OPTIONS).or_insert(HeaderValue::from_static("nosniff")); | |
| 64 | + headers.entry("x-frame-options").or_insert(HeaderValue::from_static("DENY")); | |
| 65 | + headers.entry(header::REFERRER_POLICY).or_insert(HeaderValue::from_static("strict-origin-when-cross-origin")); | |
| 66 | + response | |
| 67 | +} |
+64-0backend/src/signed.rs
| @@ -0,0 +1,64 @@ | ||
| 1 | +//! Short-lived signed values: registry bearer tokens, LFS tokens handed out | |
| 2 | +//! over SSH, the OAuth state cookie. `payload.signature`, both base64url, | |
| 3 | +//! HMAC-SHA256 with SECRET_KEY. A `purpose` string is mixed into the MAC so a | |
| 4 | +//! token minted for one use can never be replayed as another. | |
| 5 | + | |
| 6 | +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; | |
| 7 | +use hmac::{KeyInit, Mac}; | |
| 8 | +use serde::{Serialize, de::DeserializeOwned}; | |
| 9 | + | |
| 10 | +type HmacSha256 = hmac::Hmac<sha2::Sha256>; | |
| 11 | + | |
| 12 | +#[derive(serde::Serialize, serde::Deserialize)] | |
| 13 | +struct Envelope<T> { | |
| 14 | + exp: i64, | |
| 15 | + data: T, | |
| 16 | +} | |
| 17 | + | |
| 18 | +fn mac(key: &[u8], purpose: &str, payload: &str) -> Vec<u8> { | |
| 19 | + let mut mac = HmacSha256::new_from_slice(key).expect("hmac accepts any key length"); | |
| 20 | + mac.update(purpose.as_bytes()); | |
| 21 | + mac.update(b"\0"); | |
| 22 | + mac.update(payload.as_bytes()); | |
| 23 | + mac.finalize().into_bytes().to_vec() | |
| 24 | +} | |
| 25 | + | |
| 26 | +pub fn sign<T: Serialize>(key: &[u8], purpose: &str, data: &T, ttl_secs: i64) -> String { | |
| 27 | + let envelope = Envelope { exp: chrono::Utc::now().timestamp() + ttl_secs, data }; | |
| 28 | + let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&envelope).expect("serializable")); | |
| 29 | + let signature = URL_SAFE_NO_PAD.encode(mac(key, purpose, &payload)); | |
| 30 | + format!("{payload}.{signature}") | |
| 31 | +} | |
| 32 | + | |
| 33 | +pub fn verify<T: DeserializeOwned>(key: &[u8], purpose: &str, token: &str) -> Option<T> { | |
| 34 | + let (payload, signature) = token.split_once('.')?; | |
| 35 | + let signature = URL_SAFE_NO_PAD.decode(signature).ok()?; | |
| 36 | + let mut check = HmacSha256::new_from_slice(key).ok()?; | |
| 37 | + check.update(purpose.as_bytes()); | |
| 38 | + check.update(b"\0"); | |
| 39 | + check.update(payload.as_bytes()); | |
| 40 | + check.verify_slice(&signature).ok()?; | |
| 41 | + let envelope: Envelope<T> = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(payload).ok()?).ok()?; | |
| 42 | + (envelope.exp > chrono::Utc::now().timestamp()).then_some(envelope.data) | |
| 43 | +} | |
| 44 | + | |
| 45 | +#[cfg(test)] | |
| 46 | +mod tests { | |
| 47 | + use super::*; | |
| 48 | + | |
| 49 | + #[test] | |
| 50 | + fn roundtrip_and_purpose_binding() { | |
| 51 | + let key = [7u8; 32]; | |
| 52 | + let token = sign(&key, "lfs", &("alice", 3), 60); | |
| 53 | + assert_eq!(verify::<(String, i32)>(&key, "lfs", &token), Some(("alice".into(), 3))); | |
| 54 | + assert_eq!(verify::<(String, i32)>(&key, "registry", &token), None); | |
| 55 | + assert_eq!(verify::<(String, i32)>(&[8u8; 32], "lfs", &token), None); | |
| 56 | + } | |
| 57 | + | |
| 58 | + #[test] | |
| 59 | + fn expired_tokens_fail() { | |
| 60 | + let key = [1u8; 32]; | |
| 61 | + let token = sign(&key, "x", &1, -5); | |
| 62 | + assert_eq!(verify::<i32>(&key, "x", &token), None); | |
| 63 | + } | |
| 64 | +} |
+5-0backend/src/ssh.rs
| @@ -0,0 +1,5 @@ | ||
| 1 | +//! Built-in SSH server for git. Stub. | |
| 2 | + | |
| 3 | +use crate::state::AppState; | |
| 4 | + | |
| 5 | +pub fn spawn(_state: AppState) {} |
+49-0backend/src/sshkeys.rs
| @@ -0,0 +1,49 @@ | ||
| 1 | +//! Parsing user-supplied SSH public keys (settings page and API). | |
| 2 | + | |
| 3 | +use russh::keys::{HashAlg, PublicKey}; | |
| 4 | + | |
| 5 | +pub struct ParsedKey { | |
| 6 | + /// "algorithm base64", without the comment. | |
| 7 | + pub normalized: String, | |
| 8 | + /// "SHA256:...", what `ssh-keygen -lf` prints and what the SSH server looks up. | |
| 9 | + pub fingerprint: String, | |
| 10 | + pub algorithm: String, | |
| 11 | + pub comment: String, | |
| 12 | +} | |
| 13 | + | |
| 14 | +pub fn parse_public_key(text: &str) -> Result<ParsedKey, String> { | |
| 15 | + let line = text.lines().map(str::trim).find(|l| !l.is_empty() && !l.starts_with('#')).ok_or("Paste a public key.")?; | |
| 16 | + if line.contains("PRIVATE KEY") { | |
| 17 | + return Err("That is a private key. Paste the .pub file instead, and keep the private key secret.".into()); | |
| 18 | + } | |
| 19 | + let key = PublicKey::from_openssh(line).map_err(|e| format!("Not a valid OpenSSH public key ({e})."))?; | |
| 20 | + let algorithm = key.algorithm().to_string(); | |
| 21 | + if algorithm == "ssh-dss" { | |
| 22 | + return Err("DSA keys are not accepted; use ed25519.".into()); | |
| 23 | + } | |
| 24 | + let comment = key.comment().to_string(); | |
| 25 | + let mut bare = key.clone(); | |
| 26 | + bare.set_comment(""); | |
| 27 | + let normalized = bare.to_openssh().map_err(|e| e.to_string())?.trim().to_string(); | |
| 28 | + Ok(ParsedKey { fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), normalized, algorithm, comment }) | |
| 29 | +} | |
| 30 | + | |
| 31 | +#[cfg(test)] | |
| 32 | +mod tests { | |
| 33 | + use super::*; | |
| 34 | + | |
| 35 | + #[test] | |
| 36 | + fn parses_ed25519() { | |
| 37 | + let key = parse_public_key("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJdD7y3aLq454yWBdwLWbieU1ebz9/cu7/QEXn9OIeZJ alice@laptop\n").unwrap(); | |
| 38 | + assert!(key.fingerprint.starts_with("SHA256:")); | |
| 39 | + assert_eq!(key.comment, "alice@laptop"); | |
| 40 | + assert!(key.normalized.starts_with("ssh-ed25519 AAAA")); | |
| 41 | + assert!(!key.normalized.contains("alice")); | |
| 42 | + } | |
| 43 | + | |
| 44 | + #[test] | |
| 45 | + fn rejects_garbage_and_private_keys() { | |
| 46 | + assert!(parse_public_key("hello").is_err()); | |
| 47 | + assert!(parse_public_key("-----BEGIN OPENSSH PRIVATE KEY-----").is_err()); | |
| 48 | + } | |
| 49 | +} |
+21-0backend/src/state.rs
| @@ -0,0 +1,21 @@ | ||
| 1 | +//! Shared application state, cloned into every handler. | |
| 2 | + | |
| 3 | +use std::sync::Arc; | |
| 4 | + | |
| 5 | +use sqlx::PgPool; | |
| 6 | +use tokio::sync::broadcast; | |
| 7 | + | |
| 8 | +use crate::{config::Config, storage::Storage}; | |
| 9 | + | |
| 10 | +#[derive(Clone)] | |
| 11 | +pub struct AppState { | |
| 12 | + pub db: PgPool, | |
| 13 | + pub config: Arc<Config>, | |
| 14 | + pub storage: Storage, | |
| 15 | + /// Outbound HTTP (analytics, email APIs). R2 has its own client. | |
| 16 | + pub http: reqwest::Client, | |
| 17 | + /// Fan-out to the browsers connected to `/dev-ws`. Debug builds only; | |
| 18 | + /// nothing sends on it in a release build. | |
| 19 | + #[allow(dead_code)] | |
| 20 | + pub reload_tx: broadcast::Sender<()>, | |
| 21 | +} |
+254-0backend/src/storage.rs
| @@ -0,0 +1,254 @@ | ||
| 1 | +//! R2 through its S3 API. Requests are presigned with rusty-s3 and sent with | |
| 2 | +//! reqwest, so the same signer produces both the URLs the server calls and | |
| 3 | +//! the short-lived URLs it hands to clients (LFS, image layer pulls, CLI | |
| 4 | +//! downloads) so their bytes never pass through this server. | |
| 5 | + | |
| 6 | +use std::{path::Path, time::Duration}; | |
| 7 | + | |
| 8 | +use anyhow::{Context, bail}; | |
| 9 | +use bytes::Bytes; | |
| 10 | +use futures::{StreamExt, TryStreamExt, stream}; | |
| 11 | +use reqwest::{Body, StatusCode}; | |
| 12 | +use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle, actions::ListObjectsV2}; | |
| 13 | +use tokio::io::AsyncReadExt; | |
| 14 | +use url::Url; | |
| 15 | + | |
| 16 | +use crate::config::R2Config; | |
| 17 | + | |
| 18 | +/// Objects up to this size go up in one PUT; larger ones use multipart. | |
| 19 | +const SINGLE_PUT_MAX: u64 = 64 * 1024 * 1024; | |
| 20 | +/// R2 requires every part except the last to be the same size. | |
| 21 | +const PART_SIZE: u64 = 32 * 1024 * 1024; | |
| 22 | +const PART_CONCURRENCY: usize = 4; | |
| 23 | +/// How long URLs the server signs for its own requests stay valid. | |
| 24 | +const SELF_TTL: Duration = Duration::from_secs(15 * 60); | |
| 25 | + | |
| 26 | +#[derive(Clone)] | |
| 27 | +pub struct Storage { | |
| 28 | + bucket: Bucket, | |
| 29 | + credentials: Credentials, | |
| 30 | + http: reqwest::Client, | |
| 31 | +} | |
| 32 | + | |
| 33 | +pub struct ObjectInfo { | |
| 34 | + pub key: String, | |
| 35 | + pub size: u64, | |
| 36 | + pub last_modified: String, | |
| 37 | +} | |
| 38 | + | |
| 39 | +impl Storage { | |
| 40 | + pub fn new(config: &R2Config) -> anyhow::Result<Self> { | |
| 41 | + let endpoint: Url = config.endpoint.parse().context("R2 endpoint is not a URL")?; | |
| 42 | + let bucket = Bucket::new(endpoint, UrlStyle::Path, config.bucket.clone(), "auto") | |
| 43 | + .context("invalid R2 bucket configuration")?; | |
| 44 | + let credentials = Credentials::new(config.access_key_id.clone(), config.secret_access_key.clone()); | |
| 45 | + let http = reqwest::Client::builder() | |
| 46 | + .connect_timeout(Duration::from_secs(10)) | |
| 47 | + .build()?; | |
| 48 | + Ok(Self { bucket, credentials, http }) | |
| 49 | + } | |
| 50 | + | |
| 51 | + pub fn bucket_name(&self) -> &str { | |
| 52 | + self.bucket.name() | |
| 53 | + } | |
| 54 | + | |
| 55 | + /// A URL anyone can GET for `ttl`. `filename` sets Content-Disposition. | |
| 56 | + pub fn presign_get(&self, key: &str, ttl: Duration, filename: Option<&str>) -> Url { | |
| 57 | + let mut action = self.bucket.get_object(Some(&self.credentials), key); | |
| 58 | + if let Some(name) = filename { | |
| 59 | + action | |
| 60 | + .query_mut() | |
| 61 | + .insert("response-content-disposition", format!("attachment; filename=\"{name}\"")); | |
| 62 | + } | |
| 63 | + action.sign(ttl) | |
| 64 | + } | |
| 65 | + | |
| 66 | + /// A URL anyone can PUT a body to for `ttl`. | |
| 67 | + pub fn presign_put(&self, key: &str, ttl: Duration) -> Url { | |
| 68 | + self.bucket.put_object(Some(&self.credentials), key).sign(ttl) | |
| 69 | + } | |
| 70 | + | |
| 71 | + /// Size of an object, or None when it does not exist. | |
| 72 | + pub async fn head(&self, key: &str) -> anyhow::Result<Option<u64>> { | |
| 73 | + let url = self.bucket.head_object(Some(&self.credentials), key).sign(SELF_TTL); | |
| 74 | + let response = self.http.head(url).send().await?; | |
| 75 | + match response.status() { | |
| 76 | + StatusCode::NOT_FOUND => Ok(None), | |
| 77 | + // content_length() reports the (empty) HEAD body, so read the header. | |
| 78 | + status if status.is_success() => Ok(response | |
| 79 | + .headers() | |
| 80 | + .get(reqwest::header::CONTENT_LENGTH) | |
| 81 | + .and_then(|v| v.to_str().ok()?.parse().ok())), | |
| 82 | + status => bail!("R2 HEAD {key} failed: {status}"), | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + pub async fn get_bytes(&self, key: &str) -> anyhow::Result<Option<Bytes>> { | |
| 87 | + let url = self.bucket.get_object(Some(&self.credentials), key).sign(SELF_TTL); | |
| 88 | + let response = self.http.get(url).send().await?; | |
| 89 | + match response.status() { | |
| 90 | + StatusCode::NOT_FOUND => Ok(None), | |
| 91 | + status if status.is_success() => Ok(Some(response.bytes().await?)), | |
| 92 | + status => bail!("R2 GET {key} failed: {status}"), | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + pub async fn put_bytes(&self, key: &str, body: Bytes, content_type: &str) -> anyhow::Result<()> { | |
| 97 | + let url = self.bucket.put_object(Some(&self.credentials), key).sign(SELF_TTL); | |
| 98 | + let response = self | |
| 99 | + .http | |
| 100 | + .put(url) | |
| 101 | + .header(reqwest::header::CONTENT_TYPE, content_type) | |
| 102 | + .body(body) | |
| 103 | + .send() | |
| 104 | + .await?; | |
| 105 | + if !response.status().is_success() { | |
| 106 | + bail!("R2 PUT {key} failed: {} {}", response.status(), response.text().await.unwrap_or_default()); | |
| 107 | + } | |
| 108 | + Ok(()) | |
| 109 | + } | |
| 110 | + | |
| 111 | + /// Uploads a local file, switching to multipart above 64 MiB. | |
| 112 | + pub async fn put_file(&self, key: &str, path: &Path) -> anyhow::Result<u64> { | |
| 113 | + let size = tokio::fs::metadata(path).await?.len(); | |
| 114 | + if size <= SINGLE_PUT_MAX { | |
| 115 | + let file = tokio::fs::File::open(path).await?; | |
| 116 | + let url = self.bucket.put_object(Some(&self.credentials), key).sign(SELF_TTL); | |
| 117 | + let response = self | |
| 118 | + .http | |
| 119 | + .put(url) | |
| 120 | + .header(reqwest::header::CONTENT_LENGTH, size) | |
| 121 | + .body(Body::wrap_stream(tokio_util::io::ReaderStream::new(file))) | |
| 122 | + .send() | |
| 123 | + .await?; | |
| 124 | + if !response.status().is_success() { | |
| 125 | + bail!("R2 PUT {key} failed: {} {}", response.status(), response.text().await.unwrap_or_default()); | |
| 126 | + } | |
| 127 | + return Ok(size); | |
| 128 | + } | |
| 129 | + self.put_file_multipart(key, path, size).await?; | |
| 130 | + Ok(size) | |
| 131 | + } | |
| 132 | + | |
| 133 | + async fn put_file_multipart(&self, key: &str, path: &Path, size: u64) -> anyhow::Result<()> { | |
| 134 | + let url = self.bucket.create_multipart_upload(Some(&self.credentials), key).sign(SELF_TTL); | |
| 135 | + let response = self.http.post(url).send().await?.error_for_status()?; | |
| 136 | + let created = rusty_s3::actions::CreateMultipartUpload::parse_response(&response.text().await?) | |
| 137 | + .map_err(|e| anyhow::anyhow!("bad CreateMultipartUpload response: {e}"))?; | |
| 138 | + let upload_id = created.upload_id().to_string(); | |
| 139 | + | |
| 140 | + let parts = size.div_ceil(PART_SIZE); | |
| 141 | + let result: anyhow::Result<Vec<String>> = stream::iter(0..parts) | |
| 142 | + .map(|index| { | |
| 143 | + let upload_id = upload_id.clone(); | |
| 144 | + async move { | |
| 145 | + let offset = index * PART_SIZE; | |
| 146 | + let length = PART_SIZE.min(size - offset); | |
| 147 | + let mut file = tokio::fs::File::open(path).await?; | |
| 148 | + tokio::io::AsyncSeekExt::seek(&mut file, std::io::SeekFrom::Start(offset)).await?; | |
| 149 | + let mut buffer = Vec::with_capacity(length as usize); | |
| 150 | + file.take(length).read_to_end(&mut buffer).await?; | |
| 151 | + let url = self | |
| 152 | + .bucket | |
| 153 | + .upload_part(Some(&self.credentials), key, (index + 1) as u16, &upload_id) | |
| 154 | + .sign(SELF_TTL); | |
| 155 | + let response = self.http.put(url).body(buffer).send().await?.error_for_status()?; | |
| 156 | + let etag = response | |
| 157 | + .headers() | |
| 158 | + .get(reqwest::header::ETAG) | |
| 159 | + .and_then(|v| v.to_str().ok()) | |
| 160 | + .context("R2 part upload returned no ETag")? | |
| 161 | + .to_string(); | |
| 162 | + anyhow::Ok(etag) | |
| 163 | + } | |
| 164 | + }) | |
| 165 | + .buffered(PART_CONCURRENCY) | |
| 166 | + .try_collect() | |
| 167 | + .await; | |
| 168 | + | |
| 169 | + let etags = match result { | |
| 170 | + Ok(etags) => etags, | |
| 171 | + Err(error) => { | |
| 172 | + let abort = self | |
| 173 | + .bucket | |
| 174 | + .abort_multipart_upload(Some(&self.credentials), key, &upload_id) | |
| 175 | + .sign(SELF_TTL); | |
| 176 | + let _ = self.http.delete(abort).send().await; | |
| 177 | + return Err(error); | |
| 178 | + } | |
| 179 | + }; | |
| 180 | + | |
| 181 | + let complete = self.bucket.complete_multipart_upload( | |
| 182 | + Some(&self.credentials), | |
| 183 | + key, | |
| 184 | + &upload_id, | |
| 185 | + etags.iter().map(String::as_str), | |
| 186 | + ); | |
| 187 | + let url = complete.sign(SELF_TTL); | |
| 188 | + let response = self.http.post(url).body(complete.body()).send().await?; | |
| 189 | + if !response.status().is_success() { | |
| 190 | + bail!("R2 complete multipart {key} failed: {} {}", response.status(), response.text().await.unwrap_or_default()); | |
| 191 | + } | |
| 192 | + Ok(()) | |
| 193 | + } | |
| 194 | + | |
| 195 | + pub async fn delete(&self, key: &str) -> anyhow::Result<()> { | |
| 196 | + let url = self.bucket.delete_object(Some(&self.credentials), key).sign(SELF_TTL); | |
| 197 | + let response = self.http.delete(url).send().await?; | |
| 198 | + if !(response.status().is_success() || response.status() == StatusCode::NOT_FOUND) { | |
| 199 | + bail!("R2 DELETE {key} failed: {}", response.status()); | |
| 200 | + } | |
| 201 | + Ok(()) | |
| 202 | + } | |
| 203 | + | |
| 204 | + /// Every object under `prefix`, following continuation tokens. | |
| 205 | + pub async fn list(&self, prefix: &str) -> anyhow::Result<Vec<ObjectInfo>> { | |
| 206 | + let mut out = Vec::new(); | |
| 207 | + let mut token: Option<String> = None; | |
| 208 | + loop { | |
| 209 | + let mut action = self.bucket.list_objects_v2(Some(&self.credentials)); | |
| 210 | + action.with_prefix(prefix); | |
| 211 | + if let Some(token) = &token { | |
| 212 | + action.with_continuation_token(token.clone()); | |
| 213 | + } | |
| 214 | + let response = self.http.get(action.sign(SELF_TTL)).send().await?.error_for_status()?; | |
| 215 | + let parsed = ListObjectsV2::parse_response(&response.text().await?) | |
| 216 | + .map_err(|e| anyhow::anyhow!("bad ListObjectsV2 response: {e}"))?; | |
| 217 | + out.extend(parsed.contents.into_iter().map(|c| ObjectInfo { | |
| 218 | + key: c.key, | |
| 219 | + size: c.size, | |
| 220 | + last_modified: c.last_modified, | |
| 221 | + })); | |
| 222 | + match parsed.next_continuation_token { | |
| 223 | + Some(next) => token = Some(next), | |
| 224 | + None => break, | |
| 225 | + } | |
| 226 | + } | |
| 227 | + Ok(out) | |
| 228 | + } | |
| 229 | +} | |
| 230 | + | |
| 231 | +/// R2 key layout, in one place so nothing writes outside its prefix. | |
| 232 | +pub mod keys { | |
| 233 | + pub fn lfs(oid: &str) -> String { | |
| 234 | + format!("lfs/{}/{}/{oid}", &oid[..2], &oid[2..4]) | |
| 235 | + } | |
| 236 | + | |
| 237 | + /// `digest` is "sha256:<hex>". | |
| 238 | + pub fn blob(digest: &str) -> String { | |
| 239 | + let hex = digest.trim_start_matches("sha256:"); | |
| 240 | + format!("registry/blobs/sha256/{}/{hex}", &hex[..2]) | |
| 241 | + } | |
| 242 | + | |
| 243 | + pub fn avatar(account_id: i64, hash: &str) -> String { | |
| 244 | + format!("avatars/{account_id}/{hash}") | |
| 245 | + } | |
| 246 | + | |
| 247 | + pub fn cli_release(version: &str, target: &str) -> String { | |
| 248 | + format!("releases/cli/{version}/ig-{target}") | |
| 249 | + } | |
| 250 | + | |
| 251 | + pub fn backup(stamp: &str, name: &str) -> String { | |
| 252 | + format!("backups/{stamp}/{name}") | |
| 253 | + } | |
| 254 | +} |
+90-0backend/src/transport.rs
| @@ -0,0 +1,90 @@ | ||
| 1 | +//! Spawning `git upload-pack` / `git receive-pack` for a repository, shared | |
| 2 | +//! by smart HTTP and SSH so both enforce the same limits and hooks. | |
| 3 | + | |
| 4 | +use tokio::process::Command; | |
| 5 | + | |
| 6 | +use crate::{auth::Viewer, git::git_command, hook, models::Repo, ops, state::AppState}; | |
| 7 | + | |
| 8 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 9 | +pub enum Service { | |
| 10 | + UploadPack, | |
| 11 | + ReceivePack, | |
| 12 | +} | |
| 13 | + | |
| 14 | +impl Service { | |
| 15 | + pub fn from_name(name: &str) -> Option<Self> { | |
| 16 | + match name { | |
| 17 | + "git-upload-pack" => Some(Self::UploadPack), | |
| 18 | + "git-receive-pack" => Some(Self::ReceivePack), | |
| 19 | + _ => None, | |
| 20 | + } | |
| 21 | + } | |
| 22 | + | |
| 23 | + pub fn name(self) -> &'static str { | |
| 24 | + match self { | |
| 25 | + Self::UploadPack => "git-upload-pack", | |
| 26 | + Self::ReceivePack => "git-receive-pack", | |
| 27 | + } | |
| 28 | + } | |
| 29 | + | |
| 30 | + fn subcommand(self) -> &'static str { | |
| 31 | + match self { | |
| 32 | + Self::UploadPack => "upload-pack", | |
| 33 | + Self::ReceivePack => "receive-pack", | |
| 34 | + } | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +pub struct ServiceOptions<'a> { | |
| 39 | + pub stateless_rpc: bool, | |
| 40 | + pub advertise_refs: bool, | |
| 41 | + /// Value of the client's Git-Protocol header / GIT_PROTOCOL env. | |
| 42 | + pub protocol: Option<&'a str>, | |
| 43 | +} | |
| 44 | + | |
| 45 | +/// Bytes the repo's owner may still add before hitting their quota. | |
| 46 | +pub async fn quota_remaining(state: &AppState, repo: &Repo) -> anyhow::Result<u64> { | |
| 47 | + let owner = crate::models::Account::by_id(&state.db, repo.owner_id).await?.ok_or_else(|| anyhow::anyhow!("owner missing"))?; | |
| 48 | + let used = ops::account_usage(state, owner.id).await.map_err(|e| anyhow::anyhow!("{e}"))?; | |
| 49 | + Ok(owner.quota(&state.config).saturating_sub(used)) | |
| 50 | +} | |
| 51 | + | |
| 52 | +/// The git command for `service`, with hooks and limits wired in. The caller | |
| 53 | +/// sets stdio and spawns it. | |
| 54 | +pub async fn service_command( | |
| 55 | + state: &AppState, | |
| 56 | + repo: &Repo, | |
| 57 | + service: Service, | |
| 58 | + pusher: Option<&Viewer>, | |
| 59 | + options: ServiceOptions<'_>, | |
| 60 | +) -> anyhow::Result<Command> { | |
| 61 | + let path = repo.disk_path(&state.config); | |
| 62 | + let mut command = git_command(&path); | |
| 63 | + if let Some(protocol) = options.protocol.filter(|p| p.len() < 200) { | |
| 64 | + command.env("GIT_PROTOCOL", protocol); | |
| 65 | + } | |
| 66 | + if service == Service::ReceivePack { | |
| 67 | + let remaining = quota_remaining(state, repo).await?; | |
| 68 | + // receive.maxInputSize = 0 means unlimited, so never pass 0. | |
| 69 | + let max_input = remaining.clamp(1, 8 * 1024 * 1024 * 1024); | |
| 70 | + command | |
| 71 | + .arg("-c") | |
| 72 | + .arg(format!("core.hooksPath={}", state.config.hooks_dir().display())) | |
| 73 | + .arg("-c") | |
| 74 | + .arg(format!("receive.maxInputSize={max_input}")) | |
| 75 | + .args(["-c", "receive.fsckObjects=true", "-c", "receive.advertisePushOptions=false"]) | |
| 76 | + .env(hook::ENV_MAX_FILE, state.config.limits.max_file_bytes.to_string()) | |
| 77 | + .env(hook::ENV_QUOTA_REMAINING, remaining.to_string()) | |
| 78 | + .env(hook::ENV_REPO, repo.full_name()) | |
| 79 | + .env(hook::ENV_PUSHER, pusher.map(|p| p.name.as_str()).unwrap_or("")); | |
| 80 | + } | |
| 81 | + command.arg(service.subcommand()); | |
| 82 | + if options.stateless_rpc { | |
| 83 | + command.arg("--stateless-rpc"); | |
| 84 | + } | |
| 85 | + if options.advertise_refs { | |
| 86 | + command.arg("--advertise-refs"); | |
| 87 | + } | |
| 88 | + command.arg(&path); | |
| 89 | + Ok(command) | |
| 90 | +} |
+9-0backend/src/web/admin.rs
| @@ -0,0 +1,9 @@ | ||
| 1 | +//! Site admin panel at /admin. Stub; filled in by the admin work. | |
| 2 | + | |
| 3 | +use axum::Router; | |
| 4 | + | |
| 5 | +use crate::state::AppState; | |
| 6 | + | |
| 7 | +pub fn router() -> Router<AppState> { | |
| 8 | + Router::new() | |
| 9 | +} |
+53-0backend/src/web/avatars.rs
| @@ -0,0 +1,53 @@ | ||
| 1 | +//! /avatars/{name}: the uploaded picture from R2, or a generated pattern. | |
| 2 | + | |
| 3 | +use std::time::Duration; | |
| 4 | + | |
| 5 | +use axum::{ | |
| 6 | + extract::{Path, State}, | |
| 7 | + http::{StatusCode, header}, | |
| 8 | + response::{IntoResponse, Response}, | |
| 9 | +}; | |
| 10 | + | |
| 11 | +use crate::{models::Account, state::AppState, web::ui}; | |
| 12 | + | |
| 13 | +pub async fn serve(State(state): State<AppState>, Path(name): Path<String>) -> Response { | |
| 14 | + let account = match Account::by_name(&state.db, &name).await { | |
| 15 | + Ok(account) => account, | |
| 16 | + Err(error) => { | |
| 17 | + tracing::error!(%error, "avatar lookup failed"); | |
| 18 | + None | |
| 19 | + } | |
| 20 | + }; | |
| 21 | + if let Some(key) = account.as_ref().and_then(|a| a.avatar_key.as_deref()) { | |
| 22 | + // Pictures are small and change rarely: proxy them with long caching | |
| 23 | + // (the URL carries a version) instead of redirecting to R2. | |
| 24 | + match state.storage.get_bytes(key).await { | |
| 25 | + Ok(Some(bytes)) => { | |
| 26 | + let mime = if bytes.starts_with(b"\x89PNG") { | |
| 27 | + "image/png" | |
| 28 | + } else if bytes.starts_with(b"RIFF") { | |
| 29 | + "image/webp" | |
| 30 | + } else if bytes.starts_with(b"GIF8") { | |
| 31 | + "image/gif" | |
| 32 | + } else { | |
| 33 | + "image/jpeg" | |
| 34 | + }; | |
| 35 | + return ( | |
| 36 | + StatusCode::OK, | |
| 37 | + [(header::CONTENT_TYPE, mime), (header::CACHE_CONTROL, "public, max-age=86400")], | |
| 38 | + bytes, | |
| 39 | + ) | |
| 40 | + .into_response(); | |
| 41 | + } | |
| 42 | + Ok(None) => tracing::warn!(key, "avatar object missing in R2"), | |
| 43 | + Err(error) => tracing::error!(%error, key, "avatar fetch failed"), | |
| 44 | + } | |
| 45 | + } | |
| 46 | + let _ = Duration::ZERO; | |
| 47 | + ( | |
| 48 | + StatusCode::OK, | |
| 49 | + [(header::CONTENT_TYPE, "image/svg+xml"), (header::CACHE_CONTROL, "public, max-age=3600")], | |
| 50 | + ui::identicon_svg(&name), | |
| 51 | + ) | |
| 52 | + .into_response() | |
| 53 | +} |
+234-0backend/src/web/layout.rs
| @@ -0,0 +1,234 @@ | ||
| 1 | +//! Wraps server-rendered markup in the Astro-built shell (dist/app-shell). | |
| 2 | +//! | |
| 3 | +//! The shell carries the stylesheet, scripts and footer; this fills its three | |
| 4 | +//! markers: `<meta name="ig-head">`, `<ig-nav></ig-nav>`, `<ig-body></ig-body>`. | |
| 5 | +//! Static Astro pages get the same head and nav through `inject`. | |
| 6 | + | |
| 7 | +use axum::{ | |
| 8 | + extract::FromRequestParts, | |
| 9 | + http::{StatusCode, header, request::Parts}, | |
| 10 | + response::{Html, IntoResponse, Response}, | |
| 11 | +}; | |
| 12 | +use maud::{DOCTYPE, Markup, PreEscaped, html}; | |
| 13 | + | |
| 14 | +use crate::{ | |
| 15 | + auth::{MaybeViewer, Viewer}, | |
| 16 | + error::AppError, | |
| 17 | + frontend, | |
| 18 | + state::AppState, | |
| 19 | + web::ui, | |
| 20 | +}; | |
| 21 | + | |
| 22 | +const HEAD_MARKER: &str = r#"<meta name="ig-head">"#; | |
| 23 | +const NAV_MARKER: &str = "<ig-nav></ig-nav>"; | |
| 24 | +const BODY_MARKER: &str = "<ig-body></ig-body>"; | |
| 25 | + | |
| 26 | +/// Request context for pages: who is looking, and the app state. | |
| 27 | +pub struct Ctx { | |
| 28 | + pub viewer: Option<Viewer>, | |
| 29 | + pub state: AppState, | |
| 30 | + pub path: String, | |
| 31 | +} | |
| 32 | + | |
| 33 | +impl FromRequestParts<AppState> for Ctx { | |
| 34 | + type Rejection = AppError; | |
| 35 | + | |
| 36 | + async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> { | |
| 37 | + let MaybeViewer(viewer) = MaybeViewer::from_request_parts(parts, state).await?; | |
| 38 | + Ok(Ctx { viewer, state: state.clone(), path: parts.uri.path().to_string() }) | |
| 39 | + } | |
| 40 | +} | |
| 41 | + | |
| 42 | +impl Ctx { | |
| 43 | + pub fn viewer_id(&self) -> Option<i64> { | |
| 44 | + self.viewer.as_ref().map(|v| v.id) | |
| 45 | + } | |
| 46 | + | |
| 47 | + pub fn render(&self, page: Page) -> Response { | |
| 48 | + render_page(&self.state, self.viewer.as_ref(), &self.path, page) | |
| 49 | + } | |
| 50 | +} | |
| 51 | + | |
| 52 | +pub struct Page { | |
| 53 | + pub title: String, | |
| 54 | + pub description: Option<String>, | |
| 55 | + pub body: Markup, | |
| 56 | + pub status: StatusCode, | |
| 57 | + pub noindex: bool, | |
| 58 | + pub og_image: Option<String>, | |
| 59 | +} | |
| 60 | + | |
| 61 | +impl Page { | |
| 62 | + pub fn new(title: impl Into<String>, body: Markup) -> Self { | |
| 63 | + Self { title: title.into(), description: None, body, status: StatusCode::OK, noindex: false, og_image: None } | |
| 64 | + } | |
| 65 | + | |
| 66 | + pub fn description(mut self, description: impl Into<String>) -> Self { | |
| 67 | + self.description = Some(description.into()); | |
| 68 | + self | |
| 69 | + } | |
| 70 | + | |
| 71 | + pub fn status(mut self, status: StatusCode) -> Self { | |
| 72 | + self.status = status; | |
| 73 | + self | |
| 74 | + } | |
| 75 | + | |
| 76 | + /// Keep settings, forms and private pages out of search engines. | |
| 77 | + pub fn noindex(mut self) -> Self { | |
| 78 | + self.noindex = true; | |
| 79 | + self | |
| 80 | + } | |
| 81 | + | |
| 82 | + pub fn og_image(mut self, url: impl Into<String>) -> Self { | |
| 83 | + self.og_image = Some(url.into()); | |
| 84 | + self | |
| 85 | + } | |
| 86 | +} | |
| 87 | + | |
| 88 | +pub fn render_page(state: &AppState, viewer: Option<&Viewer>, path: &str, page: Page) -> Response { | |
| 89 | + let Some(shell) = frontend::shell() else { | |
| 90 | + tracing::error!("frontend/dist/app-shell/index.html is missing; run `bun run build` in frontend/"); | |
| 91 | + return (StatusCode::INTERNAL_SERVER_ERROR, "frontend build missing").into_response(); | |
| 92 | + }; | |
| 93 | + let title = if page.title.is_empty() { "irongit".to_string() } else { format!("{} · irongit", page.title) }; | |
| 94 | + let head = head_markup(state, viewer, path, &title, page.description.as_deref(), page.noindex, page.og_image.as_deref()); | |
| 95 | + let nav = nav_markup(viewer, path); | |
| 96 | + let html = fill(&shell, &head.0, &nav.0, Some(&page.body.0)); | |
| 97 | + let mut response = (page.status, Html(html)).into_response(); | |
| 98 | + response.headers_mut().insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-store")); | |
| 99 | + response | |
| 100 | +} | |
| 101 | + | |
| 102 | +/// Head and nav for a static Astro page (landing, docs, 404). | |
| 103 | +pub fn inject(state: &AppState, viewer: Option<&Viewer>, path: &str, html: &str) -> String { | |
| 104 | + let has_title = html.contains("<title>"); | |
| 105 | + let head = head_markup(state, viewer, path, if has_title { "" } else { "irongit" }, None, false, None); | |
| 106 | + let nav = nav_markup(viewer, path); | |
| 107 | + fill(html, &head.0, &nav.0, None) | |
| 108 | +} | |
| 109 | + | |
| 110 | +fn fill(shell: &str, head: &str, nav: &str, body: Option<&str>) -> String { | |
| 111 | + let mut out = shell.replacen(HEAD_MARKER, head, 1).replacen(NAV_MARKER, nav, 1); | |
| 112 | + if let Some(body) = body { | |
| 113 | + out = out.replacen(BODY_MARKER, body, 1); | |
| 114 | + } | |
| 115 | + out | |
| 116 | +} | |
| 117 | + | |
| 118 | +fn head_markup( | |
| 119 | + state: &AppState, | |
| 120 | + viewer: Option<&Viewer>, | |
| 121 | + path: &str, | |
| 122 | + title: &str, | |
| 123 | + description: Option<&str>, | |
| 124 | + noindex: bool, | |
| 125 | + og_image: Option<&str>, | |
| 126 | +) -> Markup { | |
| 127 | + let base = state.config.base_url(); | |
| 128 | + let rybbit = state.config.rybbit.as_ref(); | |
| 129 | + html! { | |
| 130 | + @if !title.is_empty() { | |
| 131 | + title { (title) } | |
| 132 | + meta property="og:title" content=(title); | |
| 133 | + } | |
| 134 | + @if let Some(description) = description { | |
| 135 | + meta name="description" content=(description); | |
| 136 | + meta property="og:description" content=(description); | |
| 137 | + } | |
| 138 | + meta property="og:site_name" content="irongit"; | |
| 139 | + meta property="og:url" content={ (base) (path) }; | |
| 140 | + link rel="canonical" href={ (base) (path) }; | |
| 141 | + @if let Some(image) = og_image { | |
| 142 | + meta property="og:image" content=(image); | |
| 143 | + } | |
| 144 | + @if noindex { | |
| 145 | + meta name="robots" content="noindex"; | |
| 146 | + } | |
| 147 | + @if let Some(viewer) = viewer { | |
| 148 | + meta name="ig-user" content=(viewer.name); | |
| 149 | + } | |
| 150 | + @if let Some(rybbit) = rybbit { | |
| 151 | + script src={ (rybbit.host.trim_end_matches('/')) "/api/script.js" } data-site-id=(rybbit.site_id) defer {} | |
| 152 | + } | |
| 153 | + } | |
| 154 | +} | |
| 155 | + | |
| 156 | +fn nav_markup(viewer: Option<&Viewer>, path: &str) -> Markup { | |
| 157 | + let active = |prefix: &str| path == prefix || path.starts_with(&format!("{prefix}/")); | |
| 158 | + html! { | |
| 159 | + header class="border-b border-edge bg-surface-raised" { | |
| 160 | + div class="mx-auto flex h-11 max-w-[1280px] items-center gap-3 px-4" { | |
| 161 | + a href="/" class="flex items-center gap-2 font-semibold text-ink no-underline hover:no-underline" { | |
| 162 | + (ui::logo(20)) | |
| 163 | + span class="tracking-tight" { "irongit" } | |
| 164 | + } | |
| 165 | + nav class="flex items-center gap-0.5 text-[13px]" { | |
| 166 | + a href="/explore" class={ "rounded-[4px] px-2 py-1 no-underline hover:bg-surface-hover hover:no-underline " @if active("/explore") { "text-ink" } @else { "text-ink-dim" } } { "Explore" } | |
| 167 | + a href="/docs" class={ "rounded-[4px] px-2 py-1 no-underline hover:bg-surface-hover hover:no-underline " @if active("/docs") { "text-ink" } @else { "text-ink-dim" } } { "Docs" } | |
| 168 | + } | |
| 169 | + div class="ml-auto flex items-center gap-2" { | |
| 170 | + @if let Some(viewer) = viewer { | |
| 171 | + a href="/new" class="btn btn-sm" data-track="nav_new_repo_clicked" { "+ New" } | |
| 172 | + details class="relative" { | |
| 173 | + summary class="flex cursor-pointer list-none items-center gap-1 rounded-[4px] p-0.5 hover:bg-surface-hover [&::-webkit-details-marker]:hidden" { | |
| 174 | + (ui::avatar(&viewer.name, viewer.avatar_key.as_deref(), 24)) | |
| 175 | + span class="text-[10px] text-ink-faint" { "▾" } | |
| 176 | + } | |
| 177 | + div class="absolute right-0 z-30 mt-1 w-52 overflow-hidden rounded-[4px] border border-edge-strong bg-surface-raised py-1 text-[13px] shadow-lg" { | |
| 178 | + div class="border-b border-edge px-3 py-1.5 text-xs text-ink-dim" { "Signed in as " strong class="text-ink" { (viewer.name) } } | |
| 179 | + a href={ "/" (viewer.name) } class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "Your profile" } | |
| 180 | + a href={ "/" (viewer.name) "?tab=repositories" } class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "Your repositories" } | |
| 181 | + a href={ "/" (viewer.name) "?tab=packages" } class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "Your images" } | |
| 182 | + a href="/organizations/new" class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "New organization" } | |
| 183 | + a href="/settings/profile" class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "Settings" } | |
| 184 | + @if viewer.site_admin() { | |
| 185 | + a href="/admin" class="block px-3 py-1 text-ink no-underline hover:bg-surface-hover" { "Site admin" } | |
| 186 | + } | |
| 187 | + form method="post" action="/logout" class="border-t border-edge mt-1 pt-1" data-track-submit="logout" { | |
| 188 | + button type="submit" class="block w-full cursor-pointer px-3 py-1 text-left text-ink hover:bg-surface-hover" { "Sign out" } | |
| 189 | + } | |
| 190 | + } | |
| 191 | + } | |
| 192 | + } @else { | |
| 193 | + a href={ "/login?next=" (crate::auth::urlencode(path)) } class="btn btn-sm" { "Sign in" } | |
| 194 | + a href="/register" class="btn btn-sm btn-primary" data-track="nav_register_clicked" { "Register" } | |
| 195 | + } | |
| 196 | + } | |
| 197 | + } | |
| 198 | + } | |
| 199 | + } | |
| 200 | +} | |
| 201 | + | |
| 202 | +/// Error page with an anonymous nav (no state is available here). | |
| 203 | +pub fn error_page(status: StatusCode, message: &str) -> Response { | |
| 204 | + let body = html! { | |
| 205 | + div class="mx-auto max-w-[720px] px-4 py-16" { | |
| 206 | + p class="font-mono text-xs text-ember" { (status.as_u16()) } | |
| 207 | + h1 class="mt-1 text-xl font-semibold" { (status.canonical_reason().unwrap_or("Error")) } | |
| 208 | + p class="mt-2 text-ink-dim" { (message) } | |
| 209 | + p class="mt-4" { a href="/" { "Go home" } } | |
| 210 | + } | |
| 211 | + }; | |
| 212 | + let title = status.canonical_reason().unwrap_or("Error"); | |
| 213 | + let html = match frontend::shell() { | |
| 214 | + Some(shell) => fill( | |
| 215 | + &shell, | |
| 216 | + &html! { title { (title) " · irongit" } meta name="robots" content="noindex"; }.0, | |
| 217 | + &html! { header class="border-b border-edge bg-surface-raised" { div class="mx-auto flex h-11 max-w-[1280px] items-center gap-2 px-4" { a href="/" class="flex items-center gap-2 font-semibold text-ink no-underline" { (ui::logo(20)) "irongit" } } } }.0, | |
| 218 | + Some(&body.0), | |
| 219 | + ), | |
| 220 | + None => html! { (DOCTYPE) html { body { (body) } } }.0, | |
| 221 | + }; | |
| 222 | + (status, Html(html)).into_response() | |
| 223 | +} | |
| 224 | + | |
| 225 | +/// For fragments loaded by `data-lazy`: markup without the shell. | |
| 226 | +pub fn fragment(markup: Markup) -> Response { | |
| 227 | + let mut response = Html(markup.0).into_response(); | |
| 228 | + response.headers_mut().insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-store")); | |
| 229 | + response | |
| 230 | +} | |
| 231 | + | |
| 232 | +pub fn raw_html(s: &str) -> PreEscaped<String> { | |
| 233 | + PreEscaped(s.to_string()) | |
| 234 | +} |
+14-0backend/src/web/mod.rs
| @@ -0,0 +1,14 @@ | ||
| 1 | +//! Server-rendered pages (maud), poured into the Astro shell. | |
| 2 | + | |
| 3 | +pub mod admin; | |
| 4 | +pub mod avatars; | |
| 5 | +pub mod layout; | |
| 6 | +pub mod ui; | |
| 7 | + | |
| 8 | +use axum::{Router, routing::get}; | |
| 9 | + | |
| 10 | +use crate::state::AppState; | |
| 11 | + | |
| 12 | +pub fn router() -> Router<AppState> { | |
| 13 | + Router::new().route("/avatars/{name}", get(avatars::serve)).merge(admin::router()) | |
| 14 | +} |
+207-0backend/src/web/ui.rs
| @@ -0,0 +1,207 @@ | ||
| 1 | +//! Small shared pieces of markup. | |
| 2 | + | |
| 3 | +use chrono::{DateTime, Utc}; | |
| 4 | +use maud::{Markup, PreEscaped, html}; | |
| 5 | +use sha2::{Digest, Sha256}; | |
| 6 | + | |
| 7 | +pub fn logo(size: u32) -> Markup { | |
| 8 | + html! { | |
| 9 | + svg width=(size) height=(size) viewBox="0 0 32 32" aria-hidden="true" { | |
| 10 | + rect width="32" height="32" rx="4" fill="#0b0f16" {} | |
| 11 | + rect x="6" y="20" width="20" height="5" rx="1" fill="#93a0b4" {} | |
| 12 | + rect x="10" y="15" width="12" height="5" fill="#93a0b4" {} | |
| 13 | + rect x="13" y="6" width="6" height="9" fill="#f07a1a" {} | |
| 14 | + } | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +/// URL of an account's avatar. The key's last segment busts caches when the | |
| 19 | +/// picture changes. | |
| 20 | +pub fn avatar_url(name: &str, avatar_key: Option<&str>) -> String { | |
| 21 | + match avatar_key.and_then(|k| k.rsplit('/').next()) { | |
| 22 | + Some(version) => format!("/avatars/{name}?v={}", &version[..version.len().min(12)]), | |
| 23 | + None => format!("/avatars/{name}"), | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +/// Avatars are square with 4px corners, the same as everything else. | |
| 28 | +pub fn avatar(name: &str, avatar_key: Option<&str>, size: u32) -> Markup { | |
| 29 | + html! { | |
| 30 | + img src=(avatar_url(name, avatar_key)) width=(size) height=(size) alt=(name) loading="lazy" | |
| 31 | + class="shrink-0 rounded-[4px] bg-surface-hover object-cover" style={ "width:" (size) "px;height:" (size) "px" }; | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 35 | +/// Generated avatar for accounts without an upload: a mirrored 5x5 pattern | |
| 36 | +/// in two tones of a hue picked from the name, like a maker's mark. | |
| 37 | +pub fn identicon_svg(name: &str) -> String { | |
| 38 | + let hash = Sha256::digest(name.to_ascii_lowercase().as_bytes()); | |
| 39 | + let hue = u16::from_be_bytes([hash[0], hash[1]]) % 360; | |
| 40 | + let saturation = 55 + hash[2] % 25; | |
| 41 | + let background = format!("hsl({hue} {saturation}% 16%)"); | |
| 42 | + let foreground = format!("hsl({hue} {}% 62%)", saturation + 10); | |
| 43 | + let accent = format!("hsl({} 90% 60%)", (hue + 150) % 360); | |
| 44 | + let mut cells = String::new(); | |
| 45 | + for row in 0..5u32 { | |
| 46 | + for column in 0..3u32 { | |
| 47 | + let bit = hash[(3 + row * 3 + column) as usize]; | |
| 48 | + if bit % 2 == 0 { | |
| 49 | + continue; | |
| 50 | + } | |
| 51 | + let fill = if bit % 7 == 0 { &accent } else { &foreground }; | |
| 52 | + for x in [column, 4 - column] { | |
| 53 | + cells.push_str(&format!(r#"<rect x="{}" y="{}" width="10" height="10" fill="{fill}"/>"#, 10 + x * 10, 10 + row * 10)); | |
| 54 | + if column == 2 { | |
| 55 | + break; | |
| 56 | + } | |
| 57 | + } | |
| 58 | + } | |
| 59 | + } | |
| 60 | + format!( | |
| 61 | + r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70" shape-rendering="crispEdges"><rect width="70" height="70" fill="{background}"/>{cells}</svg>"# | |
| 62 | + ) | |
| 63 | +} | |
| 64 | + | |
| 65 | +pub fn ago(at: DateTime<Utc>) -> String { | |
| 66 | + let seconds = (Utc::now() - at).num_seconds(); | |
| 67 | + let (n, unit) = match seconds { | |
| 68 | + s if s < 45 => return "just now".into(), | |
| 69 | + s if s < 3600 => ((s + 30) / 60, "minute"), | |
| 70 | + s if s < 86_400 => ((s + 1800) / 3600, "hour"), | |
| 71 | + s if s < 30 * 86_400 => ((s + 43_200) / 86_400, "day"), | |
| 72 | + s if s < 365 * 86_400 => (s / (30 * 86_400), "month"), | |
| 73 | + s => (s / (365 * 86_400), "year"), | |
| 74 | + }; | |
| 75 | + let n = n.max(1); | |
| 76 | + format!("{n} {unit}{} ago", if n == 1 { "" } else { "s" }) | |
| 77 | +} | |
| 78 | + | |
| 79 | +/// Relative time with the exact time on hover. | |
| 80 | +pub fn time(at: DateTime<Utc>) -> Markup { | |
| 81 | + html! { time datetime=(at.to_rfc3339()) title=(at.format("%Y-%m-%d %H:%M UTC")) { (ago(at)) } } | |
| 82 | +} | |
| 83 | + | |
| 84 | +pub fn bytes(n: u64) -> String { | |
| 85 | + const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"]; | |
| 86 | + let mut value = n as f64; | |
| 87 | + let mut unit = 0; | |
| 88 | + while value >= 1024.0 && unit < UNITS.len() - 1 { | |
| 89 | + value /= 1024.0; | |
| 90 | + unit += 1; | |
| 91 | + } | |
| 92 | + if unit == 0 { format!("{n} B") } else if value < 10.0 { format!("{value:.1} {}", UNITS[unit]) } else { format!("{value:.0} {}", UNITS[unit]) } | |
| 93 | +} | |
| 94 | + | |
| 95 | +pub fn plural(n: i64, one: &str, many: &str) -> String { | |
| 96 | + format!("{n} {}", if n == 1 { one } else { many }) | |
| 97 | +} | |
| 98 | + | |
| 99 | +pub fn visibility_tag(visibility: &str) -> Markup { | |
| 100 | + html! { | |
| 101 | + @if visibility == "private" { | |
| 102 | + span class="tag border-warn/50 text-warn" { "Private" } | |
| 103 | + } @else { | |
| 104 | + span class="tag" { "Public" } | |
| 105 | + } | |
| 106 | + } | |
| 107 | +} | |
| 108 | + | |
| 109 | +pub fn alert_error(message: Option<&str>) -> Markup { | |
| 110 | + html! { | |
| 111 | + @if let Some(message) = message { | |
| 112 | + div class="alert alert-error mb-3" role="alert" { (message) } | |
| 113 | + } | |
| 114 | + } | |
| 115 | +} | |
| 116 | + | |
| 117 | +pub fn alert_ok(message: Option<&str>) -> Markup { | |
| 118 | + html! { | |
| 119 | + @if let Some(message) = message { | |
| 120 | + div class="alert alert-ok mb-3" role="status" { (message) } | |
| 121 | + } | |
| 122 | + } | |
| 123 | +} | |
| 124 | + | |
| 125 | +/// A box with a copyable value: clone URLs, tokens, pull commands. | |
| 126 | +pub fn copy_field(id: &str, value: &str, track: &str) -> Markup { | |
| 127 | + html! { | |
| 128 | + div class="flex" { | |
| 129 | + input id=(id) class="input rounded-r-none font-mono text-xs" readonly value=(value) onclick="this.select()"; | |
| 130 | + button type="button" class="btn rounded-l-none border-l-0 text-xs" data-copy-target={ "#" (id) } data-track=(track) { "Copy" } | |
| 131 | + } | |
| 132 | + } | |
| 133 | +} | |
| 134 | + | |
| 135 | +pub fn empty_state(title: &str, body: Markup) -> Markup { | |
| 136 | + html! { | |
| 137 | + div class="box px-6 py-10 text-center" { | |
| 138 | + p class="font-semibold" { (title) } | |
| 139 | + div class="mt-1 text-ink-dim" { (body) } | |
| 140 | + } | |
| 141 | + } | |
| 142 | +} | |
| 143 | + | |
| 144 | +/// Skeleton rows shown until a `data-lazy` fragment loads. | |
| 145 | +pub fn skeleton_rows(rows: usize) -> Markup { | |
| 146 | + html! { | |
| 147 | + div class="space-y-2 p-3" aria-hidden="true" { | |
| 148 | + @for i in 0..rows { | |
| 149 | + div class="skeleton h-4" style={ "width:" (60 + (i * 37) % 35) "%" } {} | |
| 150 | + } | |
| 151 | + } | |
| 152 | + } | |
| 153 | +} | |
| 154 | + | |
| 155 | +pub fn lazy(url: &str, rows: usize) -> Markup { | |
| 156 | + html! { div data-lazy=(url) aria-busy="true" { (skeleton_rows(rows)) } } | |
| 157 | +} | |
| 158 | + | |
| 159 | +pub fn pager(base: &str, page: i64, has_next: bool) -> Markup { | |
| 160 | + let sep = if base.contains('?') { '&' } else { '?' }; | |
| 161 | + html! { | |
| 162 | + @if page > 1 || has_next { | |
| 163 | + div class="mt-3 flex justify-center gap-2" { | |
| 164 | + @if page > 1 { | |
| 165 | + a class="btn btn-sm" href={ (base) (sep) "page=" (page - 1) } { "Newer" } | |
| 166 | + } | |
| 167 | + @if has_next { | |
| 168 | + a class="btn btn-sm" href={ (base) (sep) "page=" (page + 1) } { "Older" } | |
| 169 | + } | |
| 170 | + } | |
| 171 | + } | |
| 172 | + } | |
| 173 | +} | |
| 174 | + | |
| 175 | +pub fn icon_folder() -> Markup { | |
| 176 | + PreEscaped(r#"<svg width="16" height="16" viewBox="0 0 16 16" class="shrink-0 fill-accent" aria-hidden="true"><path d="M1.75 2A1.75 1.75 0 0 0 0 3.75v8.5C0 13.216.784 14 1.75 14h12.5A1.75 1.75 0 0 0 16 12.25v-7A1.75 1.75 0 0 0 14.25 3.5H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.76 5.55 1.5 5 1.5H1.75Z"/></svg>"#.into()) | |
| 177 | +} | |
| 178 | + | |
| 179 | +pub fn icon_file() -> Markup { | |
| 180 | + PreEscaped(r#"<svg width="16" height="16" viewBox="0 0 16 16" class="shrink-0 fill-ink-faint" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>"#.into()) | |
| 181 | +} | |
| 182 | + | |
| 183 | +pub fn icon_repo() -> Markup { | |
| 184 | + PreEscaped(r#"<svg width="16" height="16" viewBox="0 0 16 16" class="shrink-0 fill-ink-dim" aria-hidden="true"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"/></svg>"#.into()) | |
| 185 | +} | |
| 186 | + | |
| 187 | +pub fn icon_package() -> Markup { | |
| 188 | + PreEscaped(r#"<svg width="16" height="16" viewBox="0 0 16 16" class="shrink-0 fill-ink-dim" aria-hidden="true"><path d="m8.878.392 5.25 3.045c.54.314.872.89.872 1.514v6.098a1.75 1.75 0 0 1-.872 1.514l-5.25 3.045a1.75 1.75 0 0 1-1.756 0l-5.25-3.045A1.75 1.75 0 0 1 1 11.049V4.951c0-.624.332-1.201.872-1.514L7.122.392a1.75 1.75 0 0 1 1.756 0ZM7.875 1.69l-4.63 2.685L8 7.133l4.755-2.758-4.63-2.685a.248.248 0 0 0-.25 0ZM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432Zm6.25 8.271 4.625-2.683a.25.25 0 0 0 .125-.216V5.677L8.75 8.432Z"/></svg>"#.into()) | |
| 189 | +} | |
| 190 | + | |
| 191 | +#[cfg(test)] | |
| 192 | +mod tests { | |
| 193 | + use super::*; | |
| 194 | + | |
| 195 | + #[test] | |
| 196 | + fn byte_formatting() { | |
| 197 | + assert_eq!(bytes(512), "512 B"); | |
| 198 | + assert_eq!(bytes(1536), "1.5 KB"); | |
| 199 | + assert_eq!(bytes(50 * 1024 * 1024), "50 MB"); | |
| 200 | + } | |
| 201 | + | |
| 202 | + #[test] | |
| 203 | + fn identicons_are_stable_and_distinct() { | |
| 204 | + assert_eq!(identicon_svg("alice"), identicon_svg("Alice")); | |
| 205 | + assert_ne!(identicon_svg("alice"), identicon_svg("bob")); | |
| 206 | + } | |
| 207 | +} |
+14-0build.sh
| @@ -0,0 +1,14 @@ | ||
| 1 | +#!/bin/sh | |
| 2 | +# Production build: the server and the ig CLI, both x86_64 Linux. | |
| 3 | +# The frontend is rebuilt without PUBLIC_HOT_RELOAD first: dev builds set it, | |
| 4 | +# and whatever is in frontend/dist at compile time is what gets embedded. | |
| 5 | +set -e | |
| 6 | + | |
| 7 | +cd "$(dirname "$0")" | |
| 8 | + | |
| 9 | +(cd frontend && bun install --frozen-lockfile && bun run build) | |
| 10 | +cargo build --release -p irongit --no-default-features | |
| 11 | +cargo build --release -p ig --target x86_64-unknown-linux-musl | |
| 12 | + | |
| 13 | +echo "server: target/release/irongit" | |
| 14 | +echo "cli: target/x86_64-unknown-linux-musl/release/ig" |
+16-0cli/Cargo.toml
| @@ -0,0 +1,16 @@ | ||
| 1 | +[package] | |
| 2 | +name = "ig" | |
| 3 | +version = "0.1.0" | |
| 4 | +edition = "2024" | |
| 5 | + | |
| 6 | +[dependencies] | |
| 7 | +anyhow = "1.0.104" | |
| 8 | +clap = { version = "4.6.7", features = ["derive", "env"] } | |
| 9 | +dirs = "7.0.0" | |
| 10 | +hex = "0.4.3" | |
| 11 | +irongit-shared = { path = "../shared" } | |
| 12 | +reqwest = { version = "0.13.5", default-features = false, features = ["rustls", "json", "blocking"] } | |
| 13 | +serde = { version = "1.0.229", features = ["derive"] } | |
| 14 | +serde_json = "1.0.151" | |
| 15 | +sha2 = "0.11.0" | |
| 16 | +toml = "1.1.7" |
+1-0cli/src/main.rs
| @@ -0,0 +1 @@ | ||
| 1 | +fn main() {} |
+14-0dev.sh
| @@ -0,0 +1,14 @@ | ||
| 1 | +#!/bin/sh | |
| 2 | +# Development server on PORT (7878). Installs frontend dependencies if needed | |
| 3 | +# (rust-embed needs frontend/dist to exist before the server compiles), then | |
| 4 | +# restarts the server on Rust or SQL changes. The server itself rebuilds the | |
| 5 | +# frontend and reloads open browsers. | |
| 6 | +set -e | |
| 7 | + | |
| 8 | +cd "$(dirname "$0")" | |
| 9 | + | |
| 10 | +[ -f .env ] || { echo "missing .env (see secrets.md)"; exit 1; } | |
| 11 | +[ -d frontend/node_modules ] || (cd frontend && bun install) | |
| 12 | +[ -d frontend/dist ] || (cd frontend && bun run build) | |
| 13 | + | |
| 14 | +exec watchexec -r -e rs,toml,sql -w backend -w shared -- cargo run -p irongit |
+11-0frontend/astro.config.mjs
| @@ -0,0 +1,11 @@ | ||
| 1 | +// @ts-check | |
| 2 | +import { defineConfig } from "astro/config"; | |
| 3 | +import tailwindcss from "@tailwindcss/vite"; | |
| 4 | + | |
| 5 | +export default defineConfig({ | |
| 6 | + // `directory` emits about/index.html, which the Rust server resolves for | |
| 7 | + // /about, /about/ and /about.html alike. There is no `astro dev` here: the | |
| 8 | + // Rust server is the only server, and it rebuilds this on change. | |
| 9 | + build: { format: "directory" }, | |
| 10 | + vite: { plugins: [tailwindcss()] }, | |
| 11 | +}); |
+671-0frontend/bun.lock
| @@ -0,0 +1,671 @@ | ||
| 1 | +{ | |
| 2 | + "lockfileVersion": 2, | |
| 3 | + "configVersion": 1, | |
| 4 | + "workspaces": { | |
| 5 | + "": { | |
| 6 | + "name": "astrum-frontend", | |
| 7 | + "dependencies": { | |
| 8 | + "@tailwindcss/vite": "^4.1.18", | |
| 9 | + "astro": "^7.3.3", | |
| 10 | + "tailwindcss": "^4.1.18", | |
| 11 | + }, | |
| 12 | + }, | |
| 13 | + }, | |
| 14 | + "packages": { | |
| 15 | + "@astrojs/compiler-binding": ["@astrojs/compiler-binding@0.4.1", "", { "optionalDependencies": { "@astrojs/compiler-binding-android-arm64": "0.4.1", "@astrojs/compiler-binding-darwin-arm64": "0.4.1", "@astrojs/compiler-binding-darwin-x64": "0.4.1", "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.1", "@astrojs/compiler-binding-linux-arm64-musl": "0.4.1", "@astrojs/compiler-binding-linux-x64-gnu": "0.4.1", "@astrojs/compiler-binding-linux-x64-musl": "0.4.1", "@astrojs/compiler-binding-wasm32-wasi": "0.4.1", "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.1", "@astrojs/compiler-binding-win32-x64-msvc": "0.4.1" } }, "sha512-ZYVDW1P58OXyxqZbOMp+/2U9KA7y2esIx9r4pWdpZ/l6fCZhaw4ul+D7EgpRVxYLkyTWqzANbTTUFGPFP8zDHA=="], | |
| 16 | + | |
| 17 | + "@astrojs/compiler-binding-android-arm64": ["@astrojs/compiler-binding-android-arm64@0.4.1", "", { "os": "android", "cpu": "arm64" }, "sha512-Wtx2DZORuNTLASLLZ8hlrjFaJe1LUloatcCn9EPq7knBhRpjMGXeJANIJMeBZG4igvz7x+3nyAJM2iwCSW9xog=="], | |
| 18 | + | |
| 19 | + "@astrojs/compiler-binding-darwin-arm64": ["@astrojs/compiler-binding-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7iwXcU+hB60Y1SvOfnWHNioT72gDsDQKaDipXh2Onwp/TvQMozhbvhy13wJR8Af940a0h6PbddCeJFpj791UDA=="], | |
| 20 | + | |
| 21 | + "@astrojs/compiler-binding-darwin-x64": ["@astrojs/compiler-binding-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pifjICos49RXhSVKSDu5gDB0/8j0O7DmxSp1DaWW2PyLG5ekwSkcrfOZto5JqhdI7QYMZB3XOiXpsdOXpS4npA=="], | |
| 22 | + | |
| 23 | + "@astrojs/compiler-binding-linux-arm64-gnu": ["@astrojs/compiler-binding-linux-arm64-gnu@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hsZMlNWc6LqWHh9LlSwTq1/XCY7pxA5rkkKIdZRV8mgbOz9OwLvjEoe5Jry13eIZjMUQO8XnQyzk8o/Pjw2YFA=="], | |
| 24 | + | |
| 25 | + "@astrojs/compiler-binding-linux-arm64-musl": ["@astrojs/compiler-binding-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a3EW27f+Z9uf57vYmpLgUriA1y9wDhKz57PT8D8XlFZt2HZEWhUxKuxI1jvKVI9p8okYee11WXEm/FHY1G9yoQ=="], | |
| 26 | + | |
| 27 | + "@astrojs/compiler-binding-linux-x64-gnu": ["@astrojs/compiler-binding-linux-x64-gnu@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XruPTKB/SS4DFzeAjQ1ZyM0ieHDjapHkOSnnzRApZ4FXY2YAI7UY3mETjfJiIS/acyZyrnPH5jObv1gATTdUJQ=="], | |
| 28 | + | |
| 29 | + "@astrojs/compiler-binding-linux-x64-musl": ["@astrojs/compiler-binding-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-E8j7lGanqNpudtkUbsFaOLJhmwFe4Ecqux7K9hkj4wULIPNZRM2bYZNcMSj8r1oWvaUiwZl8LaaNwv3+14jqhw=="], | |
| 30 | + | |
| 31 | + "@astrojs/compiler-binding-wasm32-wasi": ["@astrojs/compiler-binding-wasm32-wasi@0.4.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.2.4" }, "cpu": "none" }, "sha512-vNLxIv41ZE+nmPFtw24cRW5ZXG+ZPcAGtgigtjCP8lvcxKI7u2GrXoMsCgEH5fMw8WCEJpwLvsVBg15rJKAkyA=="], | |
| 32 | + | |
| 33 | + "@astrojs/compiler-binding-win32-arm64-msvc": ["@astrojs/compiler-binding-win32-arm64-msvc@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-/xTDgRsFDoTh0/+KFXJ74DY4Jqn8CoSHwa8UP5RAblHfnRHkenwfSl4zYDET61O9vXKLk09UzHjjq09yP20/AQ=="], | |
| 34 | + | |
| 35 | + "@astrojs/compiler-binding-win32-x64-msvc": ["@astrojs/compiler-binding-win32-x64-msvc@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-0x5J6iHZO0Fjo/CVzoIbXKzWnGESftDbEUQ/oXQ61HhUAFoR7+T1jfToZSNAS+gYyBXHSTpMWYC7lzarbhClcg=="], | |
| 36 | + | |
| 37 | + "@astrojs/compiler-rs": ["@astrojs/compiler-rs@0.4.1", "", { "dependencies": { "@astrojs/compiler-binding": "0.4.1" } }, "sha512-//NmAuRhy7eU9iP0ceI5Pymy55M+nUZ//kS6XeOyhBWdDib2KbJOyFaVJ52k7sDkJwTKVox7oPu3SpYiVnGm5A=="], | |
| 38 | + | |
| 39 | + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.11.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA=="], | |
| 40 | + | |
| 41 | + "@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.4.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "satteri": "^0.10.3" } }, "sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg=="], | |
| 42 | + | |
| 43 | + "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], | |
| 44 | + | |
| 45 | + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.3", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "package-manager-detector": "^1.6.0" } }, "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ=="], | |
| 46 | + | |
| 47 | + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], | |
| 48 | + | |
| 49 | + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], | |
| 50 | + | |
| 51 | + "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], | |
| 52 | + | |
| 53 | + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], | |
| 54 | + | |
| 55 | + "@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA=="], | |
| 56 | + | |
| 57 | + "@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="], | |
| 58 | + | |
| 59 | + "@bruits/satteri-linux-arm64-gnu": ["@bruits/satteri-linux-arm64-gnu@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w=="], | |
| 60 | + | |
| 61 | + "@bruits/satteri-linux-arm64-musl": ["@bruits/satteri-linux-arm64-musl@0.10.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA=="], | |
| 62 | + | |
| 63 | + "@bruits/satteri-linux-x64-gnu": ["@bruits/satteri-linux-x64-gnu@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw=="], | |
| 64 | + | |
| 65 | + "@bruits/satteri-linux-x64-musl": ["@bruits/satteri-linux-x64-musl@0.10.5", "", { "os": "linux", "cpu": "x64" }, "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg=="], | |
| 66 | + | |
| 67 | + "@bruits/satteri-wasm32-wasi": ["@bruits/satteri-wasm32-wasi@0.10.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.2.3" }, "cpu": "none" }, "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw=="], | |
| 68 | + | |
| 69 | + "@bruits/satteri-win32-arm64-msvc": ["@bruits/satteri-win32-arm64-msvc@0.10.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw=="], | |
| 70 | + | |
| 71 | + "@bruits/satteri-win32-x64-msvc": ["@bruits/satteri-win32-x64-msvc@0.10.5", "", { "os": "win32", "cpu": "x64" }, "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ=="], | |
| 72 | + | |
| 73 | + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], | |
| 74 | + | |
| 75 | + "@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="], | |
| 76 | + | |
| 77 | + "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], | |
| 78 | + | |
| 79 | + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], | |
| 80 | + | |
| 81 | + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], | |
| 82 | + | |
| 83 | + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], | |
| 84 | + | |
| 85 | + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], | |
| 86 | + | |
| 87 | + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], | |
| 88 | + | |
| 89 | + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], | |
| 90 | + | |
| 91 | + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], | |
| 92 | + | |
| 93 | + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], | |
| 94 | + | |
| 95 | + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], | |
| 96 | + | |
| 97 | + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], | |
| 98 | + | |
| 99 | + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], | |
| 100 | + | |
| 101 | + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], | |
| 102 | + | |
| 103 | + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], | |
| 104 | + | |
| 105 | + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], | |
| 106 | + | |
| 107 | + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], | |
| 108 | + | |
| 109 | + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], | |
| 110 | + | |
| 111 | + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], | |
| 112 | + | |
| 113 | + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], | |
| 114 | + | |
| 115 | + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], | |
| 116 | + | |
| 117 | + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], | |
| 118 | + | |
| 119 | + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], | |
| 120 | + | |
| 121 | + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], | |
| 122 | + | |
| 123 | + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], | |
| 124 | + | |
| 125 | + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], | |
| 126 | + | |
| 127 | + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], | |
| 128 | + | |
| 129 | + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], | |
| 130 | + | |
| 131 | + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], | |
| 132 | + | |
| 133 | + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], | |
| 134 | + | |
| 135 | + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], | |
| 136 | + | |
| 137 | + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], | |
| 138 | + | |
| 139 | + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="], | |
| 140 | + | |
| 141 | + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="], | |
| 142 | + | |
| 143 | + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="], | |
| 144 | + | |
| 145 | + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="], | |
| 146 | + | |
| 147 | + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="], | |
| 148 | + | |
| 149 | + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="], | |
| 150 | + | |
| 151 | + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="], | |
| 152 | + | |
| 153 | + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="], | |
| 154 | + | |
| 155 | + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="], | |
| 156 | + | |
| 157 | + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="], | |
| 158 | + | |
| 159 | + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="], | |
| 160 | + | |
| 161 | + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="], | |
| 162 | + | |
| 163 | + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="], | |
| 164 | + | |
| 165 | + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="], | |
| 166 | + | |
| 167 | + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="], | |
| 168 | + | |
| 169 | + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="], | |
| 170 | + | |
| 171 | + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="], | |
| 172 | + | |
| 173 | + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="], | |
| 174 | + | |
| 175 | + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="], | |
| 176 | + | |
| 177 | + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="], | |
| 178 | + | |
| 179 | + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="], | |
| 180 | + | |
| 181 | + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="], | |
| 182 | + | |
| 183 | + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="], | |
| 184 | + | |
| 185 | + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="], | |
| 186 | + | |
| 187 | + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="], | |
| 188 | + | |
| 189 | + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="], | |
| 190 | + | |
| 191 | + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], | |
| 192 | + | |
| 193 | + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], | |
| 194 | + | |
| 195 | + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | |
| 196 | + | |
| 197 | + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], | |
| 198 | + | |
| 199 | + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], | |
| 200 | + | |
| 201 | + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], | |
| 202 | + | |
| 203 | + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], | |
| 204 | + | |
| 205 | + "@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="], | |
| 206 | + | |
| 207 | + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.9", "", { "os": "android", "cpu": "arm" }, "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw=="], | |
| 208 | + | |
| 209 | + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg=="], | |
| 210 | + | |
| 211 | + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw=="], | |
| 212 | + | |
| 213 | + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw=="], | |
| 214 | + | |
| 215 | + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g=="], | |
| 216 | + | |
| 217 | + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.9", "", { "os": "linux", "cpu": "arm" }, "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw=="], | |
| 218 | + | |
| 219 | + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw=="], | |
| 220 | + | |
| 221 | + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA=="], | |
| 222 | + | |
| 223 | + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw=="], | |
| 224 | + | |
| 225 | + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ=="], | |
| 226 | + | |
| 227 | + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q=="], | |
| 228 | + | |
| 229 | + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ=="], | |
| 230 | + | |
| 231 | + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.9", "", { "os": "none", "cpu": "arm64" }, "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg=="], | |
| 232 | + | |
| 233 | + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww=="], | |
| 234 | + | |
| 235 | + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.9", "", { "os": "win32", "cpu": "x64" }, "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g=="], | |
| 236 | + | |
| 237 | + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], | |
| 238 | + | |
| 239 | + "@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="], | |
| 240 | + | |
| 241 | + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="], | |
| 242 | + | |
| 243 | + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="], | |
| 244 | + | |
| 245 | + "@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="], | |
| 246 | + | |
| 247 | + "@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="], | |
| 248 | + | |
| 249 | + "@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="], | |
| 250 | + | |
| 251 | + "@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="], | |
| 252 | + | |
| 253 | + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], | |
| 254 | + | |
| 255 | + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], | |
| 256 | + | |
| 257 | + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], | |
| 258 | + | |
| 259 | + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], | |
| 260 | + | |
| 261 | + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], | |
| 262 | + | |
| 263 | + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], | |
| 264 | + | |
| 265 | + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], | |
| 266 | + | |
| 267 | + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], | |
| 268 | + | |
| 269 | + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], | |
| 270 | + | |
| 271 | + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], | |
| 272 | + | |
| 273 | + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], | |
| 274 | + | |
| 275 | + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], | |
| 276 | + | |
| 277 | + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], | |
| 278 | + | |
| 279 | + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], | |
| 280 | + | |
| 281 | + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], | |
| 282 | + | |
| 283 | + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], | |
| 284 | + | |
| 285 | + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], | |
| 286 | + | |
| 287 | + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], | |
| 288 | + | |
| 289 | + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], | |
| 290 | + | |
| 291 | + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], | |
| 292 | + | |
| 293 | + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], | |
| 294 | + | |
| 295 | + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], | |
| 296 | + | |
| 297 | + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], | |
| 298 | + | |
| 299 | + "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], | |
| 300 | + | |
| 301 | + "am-i-vibing": ["am-i-vibing@0.4.0", "", { "dependencies": { "process-ancestry": "^0.1.0" }, "bin": { "am-i-vibing": "dist/cli.mjs" } }, "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg=="], | |
| 302 | + | |
| 303 | + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], | |
| 304 | + | |
| 305 | + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], | |
| 306 | + | |
| 307 | + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], | |
| 308 | + | |
| 309 | + "astro": ["astro@7.3.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.4.0", "@astrojs/internal-helpers": "0.11.0", "@astrojs/markdown-satteri": "0.4.1", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "find-proc": "0.2.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.2", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.5", "unstorage": "^1.17.5", "verkit": "^0.4.0", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.5.4" }, "optionalDependencies": { "sharp": "^0.35.4" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-NF08hk3edFkVmr9avf9HW/rQyf6NrpbVDyYj2cVN3JfijDhfJYh1ivWVTY5PUd5+rRZn/Vtu4iaeyNlP0Iv6mg=="], | |
| 310 | + | |
| 311 | + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], | |
| 312 | + | |
| 313 | + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], | |
| 314 | + | |
| 315 | + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], | |
| 316 | + | |
| 317 | + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], | |
| 318 | + | |
| 319 | + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], | |
| 320 | + | |
| 321 | + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], | |
| 322 | + | |
| 323 | + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], | |
| 324 | + | |
| 325 | + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], | |
| 326 | + | |
| 327 | + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | |
| 328 | + | |
| 329 | + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], | |
| 330 | + | |
| 331 | + "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], | |
| 332 | + | |
| 333 | + "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], | |
| 334 | + | |
| 335 | + "cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="], | |
| 336 | + | |
| 337 | + "cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], | |
| 338 | + | |
| 339 | + "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], | |
| 340 | + | |
| 341 | + "css-select": ["css-select@6.0.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^7.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "nth-check": "^2.1.1" } }, "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw=="], | |
| 342 | + | |
| 343 | + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], | |
| 344 | + | |
| 345 | + "css-what": ["css-what@7.0.0", "", {}, "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ=="], | |
| 346 | + | |
| 347 | + "csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="], | |
| 348 | + | |
| 349 | + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], | |
| 350 | + | |
| 351 | + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], | |
| 352 | + | |
| 353 | + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], | |
| 354 | + | |
| 355 | + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], | |
| 356 | + | |
| 357 | + "devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="], | |
| 358 | + | |
| 359 | + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], | |
| 360 | + | |
| 361 | + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], | |
| 362 | + | |
| 363 | + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], | |
| 364 | + | |
| 365 | + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], | |
| 366 | + | |
| 367 | + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], | |
| 368 | + | |
| 369 | + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], | |
| 370 | + | |
| 371 | + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], | |
| 372 | + | |
| 373 | + "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], | |
| 374 | + | |
| 375 | + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], | |
| 376 | + | |
| 377 | + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], | |
| 378 | + | |
| 379 | + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], | |
| 380 | + | |
| 381 | + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], | |
| 382 | + | |
| 383 | + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], | |
| 384 | + | |
| 385 | + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], | |
| 386 | + | |
| 387 | + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], | |
| 388 | + | |
| 389 | + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], | |
| 390 | + | |
| 391 | + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], | |
| 392 | + | |
| 393 | + "find-proc": ["find-proc@0.2.0", "", {}, "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w=="], | |
| 394 | + | |
| 395 | + "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], | |
| 396 | + | |
| 397 | + "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], | |
| 398 | + | |
| 399 | + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], | |
| 400 | + | |
| 401 | + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], | |
| 402 | + | |
| 403 | + "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], | |
| 404 | + | |
| 405 | + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], | |
| 406 | + | |
| 407 | + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | |
| 408 | + | |
| 409 | + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], | |
| 410 | + | |
| 411 | + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], | |
| 412 | + | |
| 413 | + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], | |
| 414 | + | |
| 415 | + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], | |
| 416 | + | |
| 417 | + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], | |
| 418 | + | |
| 419 | + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], | |
| 420 | + | |
| 421 | + "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], | |
| 422 | + | |
| 423 | + "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], | |
| 424 | + | |
| 425 | + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], | |
| 426 | + | |
| 427 | + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], | |
| 428 | + | |
| 429 | + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], | |
| 430 | + | |
| 431 | + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], | |
| 432 | + | |
| 433 | + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], | |
| 434 | + | |
| 435 | + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], | |
| 436 | + | |
| 437 | + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], | |
| 438 | + | |
| 439 | + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], | |
| 440 | + | |
| 441 | + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], | |
| 442 | + | |
| 443 | + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], | |
| 444 | + | |
| 445 | + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], | |
| 446 | + | |
| 447 | + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], | |
| 448 | + | |
| 449 | + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], | |
| 450 | + | |
| 451 | + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], | |
| 452 | + | |
| 453 | + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], | |
| 454 | + | |
| 455 | + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], | |
| 456 | + | |
| 457 | + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], | |
| 458 | + | |
| 459 | + "magic-string": ["magic-string@1.4.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow=="], | |
| 460 | + | |
| 461 | + "magicast": ["magicast@0.5.5", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA=="], | |
| 462 | + | |
| 463 | + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], | |
| 464 | + | |
| 465 | + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], | |
| 466 | + | |
| 467 | + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], | |
| 468 | + | |
| 469 | + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], | |
| 470 | + | |
| 471 | + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], | |
| 472 | + | |
| 473 | + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], | |
| 474 | + | |
| 475 | + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], | |
| 476 | + | |
| 477 | + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], | |
| 478 | + | |
| 479 | + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], | |
| 480 | + | |
| 481 | + "neotraverse": ["neotraverse@1.0.1", "", {}, "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w=="], | |
| 482 | + | |
| 483 | + "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], | |
| 484 | + | |
| 485 | + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], | |
| 486 | + | |
| 487 | + "node-mock-http": ["node-mock-http@1.0.5", "", {}, "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw=="], | |
| 488 | + | |
| 489 | + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], | |
| 490 | + | |
| 491 | + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], | |
| 492 | + | |
| 493 | + "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], | |
| 494 | + | |
| 495 | + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], | |
| 496 | + | |
| 497 | + "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], | |
| 498 | + | |
| 499 | + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], | |
| 500 | + | |
| 501 | + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], | |
| 502 | + | |
| 503 | + "p-limit": ["p-limit@7.3.3", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ=="], | |
| 504 | + | |
| 505 | + "p-queue": ["p-queue@9.3.3", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA=="], | |
| 506 | + | |
| 507 | + "p-timeout": ["p-timeout@7.0.2", "", {}, "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw=="], | |
| 508 | + | |
| 509 | + "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], | |
| 510 | + | |
| 511 | + "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], | |
| 512 | + | |
| 513 | + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | |
| 514 | + | |
| 515 | + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], | |
| 516 | + | |
| 517 | + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], | |
| 518 | + | |
| 519 | + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], | |
| 520 | + | |
| 521 | + "process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="], | |
| 522 | + | |
| 523 | + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], | |
| 524 | + | |
| 525 | + "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], | |
| 526 | + | |
| 527 | + "readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], | |
| 528 | + | |
| 529 | + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], | |
| 530 | + | |
| 531 | + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], | |
| 532 | + | |
| 533 | + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], | |
| 534 | + | |
| 535 | + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], | |
| 536 | + | |
| 537 | + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], | |
| 538 | + | |
| 539 | + "rolldown": ["rolldown@1.2.9", "", { "dependencies": { "@oxc-project/types": "=0.150.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.9", "@rolldown/binding-android-arm64": "1.2.9", "@rolldown/binding-darwin-arm64": "1.2.9", "@rolldown/binding-darwin-x64": "1.2.9", "@rolldown/binding-freebsd-x64": "1.2.9", "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", "@rolldown/binding-linux-arm64-gnu": "1.2.9", "@rolldown/binding-linux-arm64-musl": "1.2.9", "@rolldown/binding-linux-ppc64-gnu": "1.2.9", "@rolldown/binding-linux-s390x-gnu": "1.2.9", "@rolldown/binding-linux-x64-gnu": "1.2.9", "@rolldown/binding-linux-x64-musl": "1.2.9", "@rolldown/binding-openharmony-arm64": "1.2.9", "@rolldown/binding-win32-arm64-msvc": "1.2.9", "@rolldown/binding-win32-x64-msvc": "1.2.9" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg=="], | |
| 540 | + | |
| 541 | + "satteri": ["satteri@0.10.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.10.5", "@bruits/satteri-darwin-x64": "0.10.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5", "@bruits/satteri-linux-arm64-musl": "0.10.5", "@bruits/satteri-linux-x64-gnu": "0.10.5", "@bruits/satteri-linux-x64-musl": "0.10.5", "@bruits/satteri-wasm32-wasi": "0.10.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5", "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ=="], | |
| 542 | + | |
| 543 | + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], | |
| 544 | + | |
| 545 | + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], | |
| 546 | + | |
| 547 | + "sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="], | |
| 548 | + | |
| 549 | + "shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="], | |
| 550 | + | |
| 551 | + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], | |
| 552 | + | |
| 553 | + "smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="], | |
| 554 | + | |
| 555 | + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | |
| 556 | + | |
| 557 | + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], | |
| 558 | + | |
| 559 | + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], | |
| 560 | + | |
| 561 | + "svgo": ["svgo@4.1.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^6.0.0", "css-tree": "^3.0.1", "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" } }, "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q=="], | |
| 562 | + | |
| 563 | + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], | |
| 564 | + | |
| 565 | + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], | |
| 566 | + | |
| 567 | + "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], | |
| 568 | + | |
| 569 | + "tinyclip": ["tinyclip@0.1.15", "", {}, "sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A=="], | |
| 570 | + | |
| 571 | + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], | |
| 572 | + | |
| 573 | + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], | |
| 574 | + | |
| 575 | + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], | |
| 576 | + | |
| 577 | + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], | |
| 578 | + | |
| 579 | + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | |
| 580 | + | |
| 581 | + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], | |
| 582 | + | |
| 583 | + "ultrahtml": ["ultrahtml@1.7.0", "", {}, "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g=="], | |
| 584 | + | |
| 585 | + "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], | |
| 586 | + | |
| 587 | + "undici": ["undici@8.10.2", "", {}, "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ=="], | |
| 588 | + | |
| 589 | + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], | |
| 590 | + | |
| 591 | + "unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="], | |
| 592 | + | |
| 593 | + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], | |
| 594 | + | |
| 595 | + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], | |
| 596 | + | |
| 597 | + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], | |
| 598 | + | |
| 599 | + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], | |
| 600 | + | |
| 601 | + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], | |
| 602 | + | |
| 603 | + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], | |
| 604 | + | |
| 605 | + "verkit": ["verkit@0.4.1", "", {}, "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ=="], | |
| 606 | + | |
| 607 | + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], | |
| 608 | + | |
| 609 | + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], | |
| 610 | + | |
| 611 | + "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], | |
| 612 | + | |
| 613 | + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], | |
| 614 | + | |
| 615 | + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], | |
| 616 | + | |
| 617 | + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], | |
| 618 | + | |
| 619 | + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], | |
| 620 | + | |
| 621 | + "zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], | |
| 622 | + | |
| 623 | + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], | |
| 624 | + | |
| 625 | + "@bruits/satteri-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], | |
| 626 | + | |
| 627 | + "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], | |
| 628 | + | |
| 629 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], | |
| 630 | + | |
| 631 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], | |
| 632 | + | |
| 633 | + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], | |
| 634 | + | |
| 635 | + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], | |
| 636 | + | |
| 637 | + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], | |
| 638 | + | |
| 639 | + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | |
| 640 | + | |
| 641 | + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], | |
| 642 | + | |
| 643 | + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], | |
| 644 | + | |
| 645 | + "vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], | |
| 646 | + | |
| 647 | + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], | |
| 648 | + | |
| 649 | + "vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], | |
| 650 | + | |
| 651 | + "vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], | |
| 652 | + | |
| 653 | + "vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], | |
| 654 | + | |
| 655 | + "vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], | |
| 656 | + | |
| 657 | + "vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], | |
| 658 | + | |
| 659 | + "vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], | |
| 660 | + | |
| 661 | + "vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], | |
| 662 | + | |
| 663 | + "vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], | |
| 664 | + | |
| 665 | + "vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], | |
| 666 | + | |
| 667 | + "vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], | |
| 668 | + | |
| 669 | + "vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], | |
| 670 | + } | |
| 671 | +} |
+16-0frontend/package.json
| @@ -0,0 +1,16 @@ | ||
| 1 | +{ | |
| 2 | + "name": "irongit-frontend", | |
| 3 | + "type": "module", | |
| 4 | + "version": "0.1.0", | |
| 5 | + "private": true, | |
| 6 | + "scripts": { | |
| 7 | + "build": "astro build", | |
| 8 | + "check": "astro check", | |
| 9 | + "astro": "astro" | |
| 10 | + }, | |
| 11 | + "dependencies": { | |
| 12 | + "@tailwindcss/vite": "^4.1.18", | |
| 13 | + "astro": "^7.3.3", | |
| 14 | + "tailwindcss": "^4.1.18" | |
| 15 | + } | |
| 16 | +} |
+6-0frontend/public/favicon.svg
| @@ -0,0 +1,6 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> | |
| 2 | + <rect width="32" height="32" rx="4" fill="#0b0f16"/> | |
| 3 | + <rect x="6" y="20" width="20" height="5" rx="1" fill="#93a0b4"/> | |
| 4 | + <rect x="10" y="15" width="12" height="5" fill="#93a0b4"/> | |
| 5 | + <rect x="13" y="6" width="6" height="9" fill="#f07a1a"/> | |
| 6 | +</svg> |
+6-0frontend/public/robots.txt
| @@ -0,0 +1,6 @@ | ||
| 1 | +User-agent: * | |
| 2 | +Disallow: /admin | |
| 3 | +Disallow: /settings | |
| 4 | +Disallow: /api/ | |
| 5 | +Disallow: /v2/ | |
| 6 | +Allow: / |
+58-0frontend/src/components/DevReload.astro
| @@ -0,0 +1,58 @@ | ||
| 1 | +--- | |
| 2 | +// Emitted only when the build ran with PUBLIC_HOT_RELOAD=true, which | |
| 3 | +// backend/src/dev.rs sets. A production build contains none of this. | |
| 4 | +const hotReload = import.meta.env.PUBLIC_HOT_RELOAD === "true"; | |
| 5 | +--- | |
| 6 | + | |
| 7 | +{ | |
| 8 | + hotReload && ( | |
| 9 | + <script is:inline> | |
| 10 | + (() => { | |
| 11 | + const protocol = location.protocol === "https:" ? "wss:" : "ws:"; | |
| 12 | + let delay = 250; | |
| 13 | + let reconnectTimer; | |
| 14 | + let stableTimer; | |
| 15 | + let reloading = false; | |
| 16 | + let socket; | |
| 17 | + | |
| 18 | + function connect() { | |
| 19 | + if ( | |
| 20 | + reloading || | |
| 21 | + document.visibilityState === "hidden" || | |
| 22 | + socket?.readyState === WebSocket.OPEN || | |
| 23 | + socket?.readyState === WebSocket.CONNECTING | |
| 24 | + ) | |
| 25 | + return; | |
| 26 | + | |
| 27 | + const ws = (socket = new WebSocket(`${protocol}//${location.host}/dev-ws`)); | |
| 28 | + ws.onopen = () => { | |
| 29 | + console.info("[reload] connected"); | |
| 30 | + // Only treat the backoff as recovered once it has held. | |
| 31 | + stableTimer = setTimeout(() => { delay = 250; }, 5000); | |
| 32 | + }; | |
| 33 | + ws.onmessage = (event) => { | |
| 34 | + if (event.data === "reload" && !reloading) { | |
| 35 | + reloading = true; | |
| 36 | + ws.close(); | |
| 37 | + location.reload(); | |
| 38 | + } | |
| 39 | + }; | |
| 40 | + ws.onclose = () => { | |
| 41 | + clearTimeout(stableTimer); | |
| 42 | + if (reloading) return; | |
| 43 | + // The server is usually just restarting; back off and retry. | |
| 44 | + clearTimeout(reconnectTimer); | |
| 45 | + reconnectTimer = setTimeout(connect, delay + Math.random() * 250); | |
| 46 | + delay = Math.min(delay * 2, 5000); | |
| 47 | + }; | |
| 48 | + } | |
| 49 | + | |
| 50 | + document.addEventListener("visibilitychange", () => { | |
| 51 | + if (document.visibilityState === "visible") connect(); | |
| 52 | + }); | |
| 53 | + window.addEventListener("online", connect); | |
| 54 | + connect(); | |
| 55 | + })(); | |
| 56 | + </script> | |
| 57 | + ) | |
| 58 | +} |
+63-0frontend/src/layouts/Layout.astro
| @@ -0,0 +1,63 @@ | ||
| 1 | +--- | |
| 2 | +// One shell for everything. Static pages fill <slot />; the Rust server | |
| 3 | +// fills the markers at request time: | |
| 4 | +// <meta name="ig-head"> title, description, Open Graph, analytics | |
| 5 | +// <ig-nav></ig-nav> top bar with the signed-in user | |
| 6 | +// <ig-body></ig-body> page content (app-shell.astro only) | |
| 7 | +import DevReload from "../components/DevReload.astro"; | |
| 8 | +import "../styles/global.css"; | |
| 9 | + | |
| 10 | +interface Props { | |
| 11 | + title?: string; | |
| 12 | + description?: string; | |
| 13 | + /** Static pages get a centered column; the app shell sizes itself. */ | |
| 14 | + contained?: boolean; | |
| 15 | +} | |
| 16 | + | |
| 17 | +const { title, description, contained = true } = Astro.props; | |
| 18 | +--- | |
| 19 | + | |
| 20 | +<!doctype html> | |
| 21 | +<html lang="en"> | |
| 22 | + <head> | |
| 23 | + <meta charset="utf-8" /> | |
| 24 | + <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 25 | + <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> | |
| 26 | + <script is:inline> | |
| 27 | + try { | |
| 28 | + const theme = localStorage.getItem("ig-theme"); | |
| 29 | + if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme; | |
| 30 | + } catch {} | |
| 31 | + </script> | |
| 32 | + {title && <title>{title}</title>} | |
| 33 | + {description && <meta name="description" content={description} />} | |
| 34 | + <meta name="ig-head" /> | |
| 35 | + </head> | |
| 36 | + <body class="flex min-h-screen flex-col"> | |
| 37 | + <ig-nav></ig-nav> | |
| 38 | + <main class="flex-1"> | |
| 39 | + { | |
| 40 | + contained ? ( | |
| 41 | + <div class="mx-auto w-full max-w-[1100px] px-4 py-6"> | |
| 42 | + <slot /> | |
| 43 | + </div> | |
| 44 | + ) : ( | |
| 45 | + <slot /> | |
| 46 | + ) | |
| 47 | + } | |
| 48 | + </main> | |
| 49 | + <footer class="border-t border-edge"> | |
| 50 | + <div class="mx-auto flex max-w-[1280px] flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3 text-xs text-ink-faint"> | |
| 51 | + <span>irongit</span> | |
| 52 | + <a href="/docs" class="text-ink-faint">Docs</a> | |
| 53 | + <a href="/docs/cli" class="text-ink-faint">CLI</a> | |
| 54 | + <a href="/explore" class="text-ink-faint">Explore</a> | |
| 55 | + <button type="button" data-theme-toggle class="cursor-pointer text-ink-faint hover:text-ink">Toggle theme</button> | |
| 56 | + </div> | |
| 57 | + </footer> | |
| 58 | + <DevReload /> | |
| 59 | + <script> | |
| 60 | + import "../scripts/app.ts"; | |
| 61 | + </script> | |
| 62 | + </body> | |
| 63 | +</html> |
+10-0frontend/src/pages/404.astro
| @@ -0,0 +1,10 @@ | ||
| 1 | +--- | |
| 2 | +import Layout from "../layouts/Layout.astro"; | |
| 3 | +--- | |
| 4 | + | |
| 5 | +<Layout title="Not found · irongit"> | |
| 6 | + <h1 class="text-xl font-semibold">Not found</h1> | |
| 7 | + <p class="mt-2 text-ink-dim"> | |
| 8 | + That page isn't here, or you don't have access to it. <a href="/">Go home</a>. | |
| 9 | + </p> | |
| 10 | +</Layout> |
+6-0frontend/src/pages/app-shell.astro
| @@ -0,0 +1,6 @@ | ||
| 1 | +--- | |
| 2 | +// Template for pages the Rust server renders. Never served directly. | |
| 3 | +import Layout from "../layouts/Layout.astro"; | |
| 4 | +--- | |
| 5 | + | |
| 6 | +<Layout contained={false}><ig-body></ig-body></Layout> |
+62-0frontend/src/pages/index.astro
| @@ -0,0 +1,62 @@ | ||
| 1 | +--- | |
| 2 | +import Layout from "../layouts/Layout.astro"; | |
| 3 | +--- | |
| 4 | + | |
| 5 | +<Layout | |
| 6 | + title="irongit: git hosting and a container registry" | |
| 7 | + description="Host git repositories and Docker images together. Public or private, per repo and per image. One CLI." | |
| 8 | + contained={false} | |
| 9 | +> | |
| 10 | + <section class="border-b border-edge"> | |
| 11 | + <div class="mx-auto grid max-w-[1100px] gap-8 px-4 py-12 md:grid-cols-[1.1fr_1fr] md:py-16"> | |
| 12 | + <div> | |
| 13 | + <p class="font-mono text-xs tracking-widest text-ember uppercase">Self-hosted forge</p> | |
| 14 | + <h1 class="mt-3 text-3xl leading-tight font-semibold md:text-4xl"> | |
| 15 | + Your code and your images,<br />in one place you control. | |
| 16 | + </h1> | |
| 17 | + <p class="mt-4 max-w-prose text-[15px] text-ink-dim"> | |
| 18 | + Git over HTTPS and SSH, a Docker registry, Git LFS, organizations and public profiles. Code privacy and image | |
| 19 | + privacy are set separately, so a private repo can ship a public image. | |
| 20 | + </p> | |
| 21 | + <div class="mt-6 flex flex-wrap gap-2"> | |
| 22 | + <a href="/register" class="btn btn-primary px-4 py-1.5 text-sm" data-track="landing_register_clicked">Create an account</a> | |
| 23 | + <a href="/explore" class="btn px-4 py-1.5 text-sm" data-track="landing_explore_clicked">Explore repositories</a> | |
| 24 | + </div> | |
| 25 | + </div> | |
| 26 | + <div class="box overflow-hidden font-mono text-[12.5px] leading-6"> | |
| 27 | + <div class="box-head text-ink-dim">terminal</div> | |
| 28 | + <pre class="overflow-x-auto bg-surface-sunken p-3"><span class="text-ink-faint">$</span> curl -fsSL <span id="host">this-site</span>/install.sh | sh | |
| 29 | +<span class="text-ink-faint">$</span> ig login | |
| 30 | +<span class="text-ink-faint">$</span> ig repo create api --private | |
| 31 | +<span class="text-ink-faint">$</span> git push origin main | |
| 32 | +<span class="text-ink-faint">$</span> docker push <span class="host">this-site</span>/you/api:1.0 | |
| 33 | +<span class="text-ink-faint">$</span> ig image visibility you/api public</pre> | |
| 34 | + </div> | |
| 35 | + </div> | |
| 36 | + </section> | |
| 37 | + | |
| 38 | + <section class="mx-auto grid max-w-[1100px] gap-px overflow-hidden px-4 py-10 sm:grid-cols-2 lg:grid-cols-3"> | |
| 39 | + { | |
| 40 | + [ | |
| 41 | + ["Git, the real thing", "Smart HTTP and a built-in SSH server. Push, clone, fetch with any git client. Files over the size limit go to Git LFS."], | |
| 42 | + ["Container registry", "OCI distribution API. Pulls stream straight from object storage, so big images cost the server nothing."], | |
| 43 | + ["Separate privacy", "Every repo and every image is public or private on its own. Link an image to its repo, or don't."], | |
| 44 | + ["Organizations", "Shared namespaces with owners and members, plus per-repo collaborators for everyone else."], | |
| 45 | + ["Profiles", "A public page per person with a contribution heatmap built from commits on default branches."], | |
| 46 | + ["One CLI", "ig handles login, repos, images, SSH keys and the git and docker credential helpers."], | |
| 47 | + ].map(([title, body]) => ( | |
| 48 | + <div class="border border-edge p-4"> | |
| 49 | + <h2 class="text-[14px] font-semibold">{title}</h2> | |
| 50 | + <p class="mt-1.5 text-[13px] text-ink-dim">{body}</p> | |
| 51 | + </div> | |
| 52 | + )) | |
| 53 | + } | |
| 54 | + </section> | |
| 55 | +</Layout> | |
| 56 | + | |
| 57 | +<script> | |
| 58 | + const host = location.host; | |
| 59 | + document.querySelectorAll("#host, .host").forEach((el) => (el.textContent = host)); | |
| 60 | + const install = document.getElementById("host"); | |
| 61 | + if (install) install.textContent = `${location.protocol}//${host}`; | |
| 62 | +</script> |
+228-0frontend/src/scripts/app.ts
| @@ -0,0 +1,228 @@ | ||
| 1 | +// Behaviour shared by every page, static or rendered by the Rust server. | |
| 2 | +// Everything hangs off data attributes so server templates need no JS. | |
| 3 | + | |
| 4 | +type Props = Record<string, string | number | boolean>; | |
| 5 | + | |
| 6 | +declare global { | |
| 7 | + interface Window { | |
| 8 | + rybbit?: { | |
| 9 | + event: (name: string, props?: Props) => void; | |
| 10 | + identify?: (id: string) => void; | |
| 11 | + }; | |
| 12 | + } | |
| 13 | +} | |
| 14 | + | |
| 15 | +// --- Analytics -------------------------------------------------------------- | |
| 16 | + | |
| 17 | +export function track(name: string, props?: Props) { | |
| 18 | + try { | |
| 19 | + window.rybbit?.event(name, props); | |
| 20 | + } catch { | |
| 21 | + /* analytics must never break the page */ | |
| 22 | + } | |
| 23 | +} | |
| 24 | + | |
| 25 | +function initAnalytics() { | |
| 26 | + const user = document.querySelector<HTMLMetaElement>('meta[name="ig-user"]')?.content; | |
| 27 | + if (user) { | |
| 28 | + const identify = () => window.rybbit?.identify?.(user); | |
| 29 | + if (window.rybbit) identify(); | |
| 30 | + else window.addEventListener("load", identify, { once: true }); | |
| 31 | + } | |
| 32 | + | |
| 33 | + // <a data-track="repo_clone_copied" data-track-props='{"repo":"a/b"}'> | |
| 34 | + document.addEventListener("click", (event) => { | |
| 35 | + const el = (event.target as Element)?.closest<HTMLElement>("[data-track]"); | |
| 36 | + if (!el) return; | |
| 37 | + let props: Props | undefined; | |
| 38 | + try { | |
| 39 | + props = el.dataset.trackProps ? JSON.parse(el.dataset.trackProps) : undefined; | |
| 40 | + } catch { | |
| 41 | + props = undefined; | |
| 42 | + } | |
| 43 | + track(el.dataset.track!, props); | |
| 44 | + }); | |
| 45 | + | |
| 46 | + // Forms report their submission: <form data-track-submit="repo_created">. | |
| 47 | + document.addEventListener("submit", (event) => { | |
| 48 | + const form = event.target as HTMLFormElement; | |
| 49 | + if (form?.dataset.trackSubmit) track(form.dataset.trackSubmit); | |
| 50 | + }); | |
| 51 | +} | |
| 52 | + | |
| 53 | +// --- Client logs: browser errors land in DATA_DIR/logs/frontend.log --------- | |
| 54 | + | |
| 55 | +type LogEntry = { level: string; message: string; url: string; at: string; stack?: string }; | |
| 56 | +const pending: LogEntry[] = []; | |
| 57 | +let flushTimer: number | undefined; | |
| 58 | +let sent = 0; | |
| 59 | + | |
| 60 | +function queueLog(level: string, message: string, stack?: string) { | |
| 61 | + if (sent > 200) return; // a broken page should not flood the server | |
| 62 | + pending.push({ level, message: message.slice(0, 2000), stack: stack?.slice(0, 4000), url: location.href, at: new Date().toISOString() }); | |
| 63 | + clearTimeout(flushTimer); | |
| 64 | + flushTimer = window.setTimeout(flushLogs, 1000); | |
| 65 | +} | |
| 66 | + | |
| 67 | +function flushLogs() { | |
| 68 | + if (!pending.length) return; | |
| 69 | + const batch = pending.splice(0, pending.length); | |
| 70 | + sent += batch.length; | |
| 71 | + const body = JSON.stringify({ entries: batch }); | |
| 72 | + if (!navigator.sendBeacon?.("/api/v1/client-logs", new Blob([body], { type: "application/json" }))) { | |
| 73 | + fetch("/api/v1/client-logs", { method: "POST", body, headers: { "content-type": "application/json" }, keepalive: true }).catch(() => {}); | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +function initLogging() { | |
| 78 | + window.addEventListener("error", (e) => queueLog("error", e.message || "error", e.error?.stack)); | |
| 79 | + window.addEventListener("unhandledrejection", (e) => { | |
| 80 | + const reason = e.reason; | |
| 81 | + queueLog("error", `unhandled rejection: ${reason?.message ?? String(reason)}`, reason?.stack); | |
| 82 | + }); | |
| 83 | + for (const level of ["error", "warn"] as const) { | |
| 84 | + const original = console[level].bind(console); | |
| 85 | + console[level] = (...args: unknown[]) => { | |
| 86 | + original(...args); | |
| 87 | + queueLog(level, args.map((a) => (a instanceof Error ? a.message : typeof a === "string" ? a : safeJson(a))).join(" ")); | |
| 88 | + }; | |
| 89 | + } | |
| 90 | + window.addEventListener("pagehide", flushLogs); | |
| 91 | +} | |
| 92 | + | |
| 93 | +function safeJson(value: unknown) { | |
| 94 | + try { | |
| 95 | + return JSON.stringify(value); | |
| 96 | + } catch { | |
| 97 | + return String(value); | |
| 98 | + } | |
| 99 | +} | |
| 100 | + | |
| 101 | +// --- Theme ------------------------------------------------------------------ | |
| 102 | + | |
| 103 | +function initTheme() { | |
| 104 | + document.addEventListener("click", (event) => { | |
| 105 | + const button = (event.target as Element)?.closest("[data-theme-toggle]"); | |
| 106 | + if (!button) return; | |
| 107 | + const root = document.documentElement; | |
| 108 | + const current = root.dataset.theme ?? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"); | |
| 109 | + const next = current === "light" ? "dark" : "light"; | |
| 110 | + root.dataset.theme = next; | |
| 111 | + try { | |
| 112 | + localStorage.setItem("ig-theme", next); | |
| 113 | + } catch { | |
| 114 | + /* private mode */ | |
| 115 | + } | |
| 116 | + track("theme_changed", { theme: next }); | |
| 117 | + }); | |
| 118 | +} | |
| 119 | + | |
| 120 | +// --- Copy buttons: <button data-copy="text"> or data-copy-target="#id" ----- | |
| 121 | + | |
| 122 | +function initCopy() { | |
| 123 | + document.addEventListener("click", async (event) => { | |
| 124 | + const button = (event.target as Element)?.closest<HTMLElement>("[data-copy], [data-copy-target]"); | |
| 125 | + if (!button) return; | |
| 126 | + event.preventDefault(); | |
| 127 | + let text = button.dataset.copy ?? ""; | |
| 128 | + if (button.dataset.copyTarget) { | |
| 129 | + const target = document.querySelector<HTMLInputElement | HTMLElement>(button.dataset.copyTarget); | |
| 130 | + text = target && "value" in target ? (target as HTMLInputElement).value : (target?.textContent ?? ""); | |
| 131 | + } | |
| 132 | + try { | |
| 133 | + await navigator.clipboard.writeText(text); | |
| 134 | + const original = button.innerHTML; | |
| 135 | + button.textContent = "Copied"; | |
| 136 | + setTimeout(() => (button.innerHTML = original), 1200); | |
| 137 | + } catch (error) { | |
| 138 | + console.warn("copy failed", error); | |
| 139 | + } | |
| 140 | + }); | |
| 141 | +} | |
| 142 | + | |
| 143 | +// --- Lazy fragments: <div data-lazy="/admin/fragments/users">skeleton</div> -- | |
| 144 | + | |
| 145 | +async function loadLazy(el: HTMLElement) { | |
| 146 | + const url = el.dataset.lazy!; | |
| 147 | + const started = performance.now(); | |
| 148 | + try { | |
| 149 | + const response = await fetch(url, { headers: { "x-requested-with": "fetch" } }); | |
| 150 | + if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); | |
| 151 | + el.innerHTML = await response.text(); | |
| 152 | + el.removeAttribute("aria-busy"); | |
| 153 | + el.querySelectorAll<HTMLElement>("[data-lazy]").forEach(loadLazy); | |
| 154 | + } catch (error) { | |
| 155 | + el.innerHTML = `<div class="alert alert-error">Could not load this section (${(error as Error).message}). <button class="btn btn-sm" data-retry>Retry</button></div>`; | |
| 156 | + el.querySelector("[data-retry]")?.addEventListener("click", () => loadLazy(el)); | |
| 157 | + console.error(`lazy load failed: ${url}`, error); | |
| 158 | + } finally { | |
| 159 | + const ms = Math.round(performance.now() - started); | |
| 160 | + if (ms > 1500) console.warn(`slow fragment ${url}: ${ms}ms`); | |
| 161 | + } | |
| 162 | +} | |
| 163 | + | |
| 164 | +function initLazy() { | |
| 165 | + document.querySelectorAll<HTMLElement>("[data-lazy]").forEach(loadLazy); | |
| 166 | +} | |
| 167 | + | |
| 168 | +// --- Sidebar collapse (admin) ---------------------------------------------- | |
| 169 | + | |
| 170 | +function initSidebar() { | |
| 171 | + const shell = document.getElementById("admin-shell"); | |
| 172 | + if (!shell) return; | |
| 173 | + try { | |
| 174 | + if (localStorage.getItem("ig-sidebar") === "collapsed") shell.dataset.collapsed = "true"; | |
| 175 | + } catch { | |
| 176 | + /* ignore */ | |
| 177 | + } | |
| 178 | + document.addEventListener("click", (event) => { | |
| 179 | + if (!(event.target as Element)?.closest("[data-sidebar-toggle]")) return; | |
| 180 | + const collapsed = shell.dataset.collapsed !== "true"; | |
| 181 | + shell.dataset.collapsed = String(collapsed); | |
| 182 | + try { | |
| 183 | + localStorage.setItem("ig-sidebar", collapsed ? "collapsed" : "open"); | |
| 184 | + } catch { | |
| 185 | + /* ignore */ | |
| 186 | + } | |
| 187 | + track("sidebar_toggled", { collapsed }); | |
| 188 | + }); | |
| 189 | +} | |
| 190 | + | |
| 191 | +// --- Dangerous forms: the submit button unlocks when the name is typed ------ | |
| 192 | + | |
| 193 | +function initConfirmInputs() { | |
| 194 | + document.querySelectorAll<HTMLInputElement>("input[data-confirm-value]").forEach((input) => { | |
| 195 | + const form = input.form; | |
| 196 | + const button = form?.querySelector<HTMLButtonElement>("button[type=submit]"); | |
| 197 | + if (!button) return; | |
| 198 | + const update = () => (button.disabled = input.value.trim() !== input.dataset.confirmValue); | |
| 199 | + input.addEventListener("input", update); | |
| 200 | + update(); | |
| 201 | + }); | |
| 202 | +} | |
| 203 | + | |
| 204 | +// --- Auto-submitting selects (filters, branch switchers) -------------------- | |
| 205 | + | |
| 206 | +function initAutoSubmit() { | |
| 207 | + document.addEventListener("change", (event) => { | |
| 208 | + const el = event.target as HTMLSelectElement; | |
| 209 | + if (el?.matches?.("[data-autosubmit]")) { | |
| 210 | + if (el.dataset.navigate !== undefined) location.href = el.value; | |
| 211 | + else el.form?.requestSubmit(); | |
| 212 | + } | |
| 213 | + }); | |
| 214 | +} | |
| 215 | + | |
| 216 | +function init() { | |
| 217 | + initLogging(); | |
| 218 | + initAnalytics(); | |
| 219 | + initTheme(); | |
| 220 | + initCopy(); | |
| 221 | + initLazy(); | |
| 222 | + initSidebar(); | |
| 223 | + initConfirmInputs(); | |
| 224 | + initAutoSubmit(); | |
| 225 | +} | |
| 226 | + | |
| 227 | +if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); | |
| 228 | +else init(); |
+290-0frontend/src/styles/global.css
| @@ -0,0 +1,290 @@ | ||
| 1 | +@import "tailwindcss"; | |
| 2 | + | |
| 3 | +/* Pages rendered by the Rust server use Tailwind classes too; scan them. */ | |
| 4 | +@source "../../../backend/src"; | |
| 5 | + | |
| 6 | +@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); | |
| 7 | + | |
| 8 | +@theme { | |
| 9 | + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| 10 | + --font-mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; | |
| 11 | + | |
| 12 | + --color-surface: #0b0f16; | |
| 13 | + --color-surface-raised: #121823; | |
| 14 | + --color-surface-sunken: #080b10; | |
| 15 | + --color-surface-hover: #1a2230; | |
| 16 | + --color-edge: #232c3a; | |
| 17 | + --color-edge-strong: #334052; | |
| 18 | + --color-ink: #e7ecf3; | |
| 19 | + --color-ink-dim: #93a0b4; | |
| 20 | + --color-ink-faint: #637087; | |
| 21 | + --color-accent: #4f8cff; | |
| 22 | + --color-accent-ink: #ffffff; | |
| 23 | + --color-ember: #f07a1a; | |
| 24 | + --color-ok: #3fb950; | |
| 25 | + --color-danger: #f85149; | |
| 26 | + --color-warn: #d29922; | |
| 27 | + | |
| 28 | + /* Contribution heatmap: forge heat, cold to white-hot. */ | |
| 29 | + --color-heat-0: #161c27; | |
| 30 | + --color-heat-1: #4a2a12; | |
| 31 | + --color-heat-2: #8a4413; | |
| 32 | + --color-heat-3: #d0661a; | |
| 33 | + --color-heat-4: #ff9a3c; | |
| 34 | + | |
| 35 | + --radius-card: 4px; | |
| 36 | +} | |
| 37 | + | |
| 38 | +/* Light theme: the system preference unless dark was chosen, or explicit. */ | |
| 39 | +@media (prefers-color-scheme: light) { | |
| 40 | + :root:not([data-theme="dark"]) { | |
| 41 | + --color-surface: #ffffff; | |
| 42 | + --color-surface-raised: #f6f8fa; | |
| 43 | + --color-surface-sunken: #eef1f4; | |
| 44 | + --color-surface-hover: #eaeef2; | |
| 45 | + --color-edge: #d8dee6; | |
| 46 | + --color-edge-strong: #b9c2cd; | |
| 47 | + --color-ink: #111827; | |
| 48 | + --color-ink-dim: #556070; | |
| 49 | + --color-ink-faint: #8590a0; | |
| 50 | + --color-accent: #2563eb; | |
| 51 | + --color-heat-0: #eceff3; | |
| 52 | + --color-heat-1: #ffd8b0; | |
| 53 | + --color-heat-2: #ffa860; | |
| 54 | + --color-heat-3: #f07a1a; | |
| 55 | + --color-heat-4: #b44c00; | |
| 56 | + color-scheme: light; | |
| 57 | + } | |
| 58 | +} | |
| 59 | +:root[data-theme="light"] { | |
| 60 | + --color-surface: #ffffff; | |
| 61 | + --color-surface-raised: #f6f8fa; | |
| 62 | + --color-surface-sunken: #eef1f4; | |
| 63 | + --color-surface-hover: #eaeef2; | |
| 64 | + --color-edge: #d8dee6; | |
| 65 | + --color-edge-strong: #b9c2cd; | |
| 66 | + --color-ink: #111827; | |
| 67 | + --color-ink-dim: #556070; | |
| 68 | + --color-ink-faint: #8590a0; | |
| 69 | + --color-accent: #2563eb; | |
| 70 | + --color-heat-0: #eceff3; | |
| 71 | + --color-heat-1: #ffd8b0; | |
| 72 | + --color-heat-2: #ffa860; | |
| 73 | + --color-heat-3: #f07a1a; | |
| 74 | + --color-heat-4: #b44c00; | |
| 75 | + color-scheme: light; | |
| 76 | +} | |
| 77 | +:root { | |
| 78 | + color-scheme: dark; | |
| 79 | +} | |
| 80 | + | |
| 81 | +@layer base { | |
| 82 | + html { | |
| 83 | + -webkit-text-size-adjust: 100%; | |
| 84 | + } | |
| 85 | + body { | |
| 86 | + @apply bg-surface text-ink font-sans text-sm; | |
| 87 | + } | |
| 88 | + a { | |
| 89 | + @apply text-accent; | |
| 90 | + } | |
| 91 | + a:hover { | |
| 92 | + text-decoration: underline; | |
| 93 | + } | |
| 94 | + code, | |
| 95 | + pre, | |
| 96 | + kbd { | |
| 97 | + @apply font-mono; | |
| 98 | + } | |
| 99 | + ::selection { | |
| 100 | + background: color-mix(in srgb, var(--color-accent) 35%, transparent); | |
| 101 | + } | |
| 102 | +} | |
| 103 | + | |
| 104 | +@layer components { | |
| 105 | + /* Buttons: square-ish, dense, no pills. */ | |
| 106 | + .btn { | |
| 107 | + @apply inline-flex items-center justify-center gap-1.5 rounded-[4px] border border-edge-strong bg-surface-raised px-2.5 py-1 text-[13px] font-medium text-ink no-underline select-none cursor-pointer; | |
| 108 | + } | |
| 109 | + .btn:hover { | |
| 110 | + @apply bg-surface-hover no-underline; | |
| 111 | + } | |
| 112 | + .btn:disabled { | |
| 113 | + @apply opacity-50 cursor-not-allowed; | |
| 114 | + } | |
| 115 | + .btn-primary { | |
| 116 | + @apply border-accent bg-accent text-accent-ink; | |
| 117 | + } | |
| 118 | + .btn-primary:hover { | |
| 119 | + background: color-mix(in srgb, var(--color-accent) 85%, black); | |
| 120 | + } | |
| 121 | + .btn-danger { | |
| 122 | + @apply border-danger/60 text-danger; | |
| 123 | + } | |
| 124 | + .btn-danger:hover { | |
| 125 | + @apply bg-danger text-white; | |
| 126 | + } | |
| 127 | + .btn-sm { | |
| 128 | + @apply px-2 py-0.5 text-xs; | |
| 129 | + } | |
| 130 | + | |
| 131 | + .input { | |
| 132 | + @apply w-full rounded-[4px] border border-edge-strong bg-surface-sunken px-2 py-1 text-[13px] text-ink outline-none; | |
| 133 | + } | |
| 134 | + .input:focus { | |
| 135 | + @apply border-accent; | |
| 136 | + box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent) 30%, transparent); | |
| 137 | + } | |
| 138 | + .label { | |
| 139 | + @apply mb-1 block text-xs font-semibold text-ink-dim; | |
| 140 | + } | |
| 141 | + .hint { | |
| 142 | + @apply mt-1 text-xs text-ink-faint; | |
| 143 | + } | |
| 144 | + | |
| 145 | + .box { | |
| 146 | + @apply rounded-[4px] border border-edge bg-surface; | |
| 147 | + } | |
| 148 | + .box-head { | |
| 149 | + @apply flex items-center gap-2 border-b border-edge bg-surface-raised px-3 py-1.5 text-[13px]; | |
| 150 | + } | |
| 151 | + | |
| 152 | + /* Small square label: visibility, roles, kinds. */ | |
| 153 | + .tag { | |
| 154 | + @apply inline-flex items-center rounded-[3px] border border-edge-strong px-1 text-[11px] leading-4 font-medium text-ink-dim; | |
| 155 | + } | |
| 156 | + | |
| 157 | + .tabs { | |
| 158 | + @apply flex gap-0 overflow-x-auto border-b border-edge text-[13px]; | |
| 159 | + } | |
| 160 | + .tab { | |
| 161 | + @apply -mb-px flex items-center gap-1.5 border-b-2 border-transparent px-3 py-1.5 whitespace-nowrap text-ink-dim no-underline; | |
| 162 | + } | |
| 163 | + .tab:hover { | |
| 164 | + @apply text-ink no-underline; | |
| 165 | + } | |
| 166 | + .tab[aria-current="page"] { | |
| 167 | + @apply border-ember text-ink font-semibold; | |
| 168 | + } | |
| 169 | + | |
| 170 | + .skeleton { | |
| 171 | + @apply animate-pulse rounded-[3px] bg-surface-hover; | |
| 172 | + } | |
| 173 | + | |
| 174 | + .alert { | |
| 175 | + @apply rounded-[4px] border px-3 py-2 text-[13px]; | |
| 176 | + } | |
| 177 | + .alert-error { | |
| 178 | + @apply border-danger/50 bg-danger/10 text-ink; | |
| 179 | + } | |
| 180 | + .alert-ok { | |
| 181 | + @apply border-ok/50 bg-ok/10 text-ink; | |
| 182 | + } | |
| 183 | + .alert-info { | |
| 184 | + @apply border-accent/40 bg-accent/10 text-ink; | |
| 185 | + } | |
| 186 | +} | |
| 187 | + | |
| 188 | +/* Rendered READMEs and other markdown. */ | |
| 189 | +.markdown { | |
| 190 | + @apply text-[14px] leading-relaxed text-ink; | |
| 191 | + overflow-wrap: anywhere; | |
| 192 | +} | |
| 193 | +.markdown > * + * { | |
| 194 | + margin-top: 0.75em; | |
| 195 | +} | |
| 196 | +.markdown h1, | |
| 197 | +.markdown h2, | |
| 198 | +.markdown h3, | |
| 199 | +.markdown h4 { | |
| 200 | + @apply font-semibold text-ink; | |
| 201 | + margin-top: 1.25em; | |
| 202 | +} | |
| 203 | +.markdown h1 { | |
| 204 | + @apply border-b border-edge pb-1 text-xl; | |
| 205 | +} | |
| 206 | +.markdown h2 { | |
| 207 | + @apply border-b border-edge pb-1 text-lg; | |
| 208 | +} | |
| 209 | +.markdown h3 { | |
| 210 | + @apply text-base; | |
| 211 | +} | |
| 212 | +.markdown > :first-child { | |
| 213 | + margin-top: 0; | |
| 214 | +} | |
| 215 | +.markdown ul { | |
| 216 | + @apply list-disc pl-6; | |
| 217 | +} | |
| 218 | +.markdown ol { | |
| 219 | + @apply list-decimal pl-6; | |
| 220 | +} | |
| 221 | +.markdown li + li { | |
| 222 | + margin-top: 0.2em; | |
| 223 | +} | |
| 224 | +.markdown code { | |
| 225 | + @apply rounded-[3px] bg-surface-hover px-1 py-px text-[12.5px]; | |
| 226 | +} | |
| 227 | +.markdown pre { | |
| 228 | + @apply overflow-x-auto rounded-[4px] border border-edge bg-surface-sunken p-3 text-[12.5px] leading-snug; | |
| 229 | +} | |
| 230 | +.markdown pre code { | |
| 231 | + @apply bg-transparent p-0; | |
| 232 | +} | |
| 233 | +.markdown blockquote { | |
| 234 | + @apply border-l-2 border-edge-strong pl-3 text-ink-dim; | |
| 235 | +} | |
| 236 | +.markdown table { | |
| 237 | + @apply block overflow-x-auto border-collapse text-[13px]; | |
| 238 | +} | |
| 239 | +.markdown th, | |
| 240 | +.markdown td { | |
| 241 | + @apply border border-edge px-2 py-1; | |
| 242 | +} | |
| 243 | +.markdown th { | |
| 244 | + @apply bg-surface-raised font-semibold; | |
| 245 | +} | |
| 246 | +.markdown img { | |
| 247 | + @apply inline max-w-full; | |
| 248 | +} | |
| 249 | +.markdown hr { | |
| 250 | + @apply border-edge; | |
| 251 | +} | |
| 252 | +.markdown input[type="checkbox"] { | |
| 253 | + @apply mr-1 align-middle; | |
| 254 | +} | |
| 255 | + | |
| 256 | +/* Source views: line numbers in a column that is not selected with the code. */ | |
| 257 | +.code-table { | |
| 258 | + @apply w-full border-collapse font-mono text-[12.5px] leading-[19px]; | |
| 259 | +} | |
| 260 | +.code-table td.ln { | |
| 261 | + @apply w-[1%] min-w-[44px] select-none border-r border-edge pr-2 pl-3 text-right align-top text-ink-faint; | |
| 262 | +} | |
| 263 | +.code-table td.ln a { | |
| 264 | + @apply text-ink-faint no-underline; | |
| 265 | +} | |
| 266 | +.code-table td.lc { | |
| 267 | + @apply pl-3 pr-3 align-top whitespace-pre; | |
| 268 | +} | |
| 269 | +.code-table tr:target td { | |
| 270 | + background: color-mix(in srgb, var(--color-ember) 14%, transparent); | |
| 271 | +} | |
| 272 | + | |
| 273 | +/* Unified diffs. */ | |
| 274 | +.diff-add { | |
| 275 | + background: color-mix(in srgb, var(--color-ok) 14%, transparent); | |
| 276 | +} | |
| 277 | +.diff-del { | |
| 278 | + background: color-mix(in srgb, var(--color-danger) 14%, transparent); | |
| 279 | +} | |
| 280 | +.diff-hunk { | |
| 281 | + @apply bg-accent/10 text-ink-dim; | |
| 282 | +} | |
| 283 | + | |
| 284 | +/* Admin shell: collapsible sidebar. */ | |
| 285 | +#admin-shell[data-collapsed="true"] .sidebar-label { | |
| 286 | + display: none; | |
| 287 | +} | |
| 288 | +#admin-shell[data-collapsed="true"] aside { | |
| 289 | + width: 44px; | |
| 290 | +} |