An ATProtocol powered blogging engine.

feature: its a blahg

Nick Gerakines 9cd417f4

+1
.gitignore
···
··· 1 + /target
+57
CLAUDE.md
···
··· 1 + # Blahg Project Context 2 + 3 + ## Overview 4 + Blahg is a Rust-based ATProtocol AppView that renders personal blog content. It consumes ATProtocol records to create a blog-like experience by discovering and displaying relevant posts. 5 + 6 + ## Architecture 7 + - **Core Dependencies**: Uses the atproto ecosystem crates (atproto-identity, atproto-record, atproto-client, atproto-jetstream) 8 + - **Purpose**: Acts as an AppView to find and store references to ATProtocol records for blog rendering 9 + - **Language**: Rust 10 + 11 + ## Key Components 12 + - ATProtocol record discovery and storage 13 + - Blog post rendering and templating 14 + - Integration with the ATProtocol ecosystem 15 + 16 + ## Development Guidelines 17 + - Follow Rust idioms and best practices 18 + - Ensure proper error handling for network operations 19 + - Maintain compatibility with the atproto crate ecosystem 20 + - Use async/await patterns for ATProtocol operations 21 + 22 + ## Visibility 23 + 24 + Types and methods should have the lowest visibility necessary, defaulting to `private`. If `public` visibility is necessary, attempt to make it public to the crate only. Using completely public visibility should be a last resort. 25 + 26 + ## Error Handling 27 + 28 + All error strings must use this format: 29 + 30 + error-blahg-<domain>-<number> <message>: <details> 31 + 32 + Example errors: 33 + 34 + * error-blahg-resolve-1 Multiple DIDs resolved for method 35 + * error-blahg-plc-1 HTTP request failed: https://google.com/ Not Found 36 + * error-blahg-key-1 Error decoding key: invalid 37 + 38 + Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. 39 + 40 + Avoid creating new errors with the `anyhow!(...)` macro. 41 + 42 + ## Testing 43 + ```bash 44 + cargo test 45 + ``` 46 + 47 + ## Linting and Type Checking 48 + ```bash 49 + cargo clippy 50 + cargo check 51 + ``` 52 + 53 + ## Building 54 + ```bash 55 + cargo build 56 + cargo build --release 57 + ```
+5584
Cargo.lock
···
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "addr2line" 7 + version = "0.24.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 + dependencies = [ 11 + "gimli", 12 + ] 13 + 14 + [[package]] 15 + name = "adler2" 16 + version = "2.0.1" 17 + source = "registry+https://github.com/rust-lang/crates.io-index" 18 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 + 20 + [[package]] 21 + name = "aho-corasick" 22 + version = "1.1.3" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 + dependencies = [ 26 + "memchr", 27 + ] 28 + 29 + [[package]] 30 + name = "aligned-vec" 31 + version = "0.6.4" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" 34 + dependencies = [ 35 + "equator", 36 + ] 37 + 38 + [[package]] 39 + name = "allocator-api2" 40 + version = "0.2.21" 41 + source = "registry+https://github.com/rust-lang/crates.io-index" 42 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 43 + 44 + [[package]] 45 + name = "android-tzdata" 46 + version = "0.1.1" 47 + source = "registry+https://github.com/rust-lang/crates.io-index" 48 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 49 + 50 + [[package]] 51 + name = "android_system_properties" 52 + version = "0.1.5" 53 + source = "registry+https://github.com/rust-lang/crates.io-index" 54 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 55 + dependencies = [ 56 + "libc", 57 + ] 58 + 59 + [[package]] 60 + name = "anstream" 61 + version = "0.6.19" 62 + source = "registry+https://github.com/rust-lang/crates.io-index" 63 + checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 64 + dependencies = [ 65 + "anstyle", 66 + "anstyle-parse", 67 + "anstyle-query", 68 + "anstyle-wincon", 69 + "colorchoice", 70 + "is_terminal_polyfill", 71 + "utf8parse", 72 + ] 73 + 74 + [[package]] 75 + name = "anstyle" 76 + version = "1.0.11" 77 + source = "registry+https://github.com/rust-lang/crates.io-index" 78 + checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 79 + 80 + [[package]] 81 + name = "anstyle-parse" 82 + version = "0.2.7" 83 + source = "registry+https://github.com/rust-lang/crates.io-index" 84 + checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 85 + dependencies = [ 86 + "utf8parse", 87 + ] 88 + 89 + [[package]] 90 + name = "anstyle-query" 91 + version = "1.1.3" 92 + source = "registry+https://github.com/rust-lang/crates.io-index" 93 + checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 94 + dependencies = [ 95 + "windows-sys 0.59.0", 96 + ] 97 + 98 + [[package]] 99 + name = "anstyle-wincon" 100 + version = "3.0.9" 101 + source = "registry+https://github.com/rust-lang/crates.io-index" 102 + checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 103 + dependencies = [ 104 + "anstyle", 105 + "once_cell_polyfill", 106 + "windows-sys 0.59.0", 107 + ] 108 + 109 + [[package]] 110 + name = "anyhow" 111 + version = "1.0.98" 112 + source = "registry+https://github.com/rust-lang/crates.io-index" 113 + checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 114 + 115 + [[package]] 116 + name = "arbitrary" 117 + version = "1.4.1" 118 + source = "registry+https://github.com/rust-lang/crates.io-index" 119 + checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 120 + 121 + [[package]] 122 + name = "arg_enum_proc_macro" 123 + version = "0.3.4" 124 + source = "registry+https://github.com/rust-lang/crates.io-index" 125 + checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" 126 + dependencies = [ 127 + "proc-macro2", 128 + "quote", 129 + "syn 2.0.104", 130 + ] 131 + 132 + [[package]] 133 + name = "arrayvec" 134 + version = "0.7.6" 135 + source = "registry+https://github.com/rust-lang/crates.io-index" 136 + checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 137 + 138 + [[package]] 139 + name = "async-recursion" 140 + version = "1.1.1" 141 + source = "registry+https://github.com/rust-lang/crates.io-index" 142 + checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 143 + dependencies = [ 144 + "proc-macro2", 145 + "quote", 146 + "syn 2.0.104", 147 + ] 148 + 149 + [[package]] 150 + name = "async-trait" 151 + version = "0.1.88" 152 + source = "registry+https://github.com/rust-lang/crates.io-index" 153 + checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 154 + dependencies = [ 155 + "proc-macro2", 156 + "quote", 157 + "syn 2.0.104", 158 + ] 159 + 160 + [[package]] 161 + name = "atoi" 162 + version = "2.0.0" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 165 + dependencies = [ 166 + "num-traits", 167 + ] 168 + 169 + [[package]] 170 + name = "atomic-waker" 171 + version = "1.1.2" 172 + source = "registry+https://github.com/rust-lang/crates.io-index" 173 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 174 + 175 + [[package]] 176 + name = "atproto-client" 177 + version = "0.9.3" 178 + source = "registry+https://github.com/rust-lang/crates.io-index" 179 + checksum = "39bb72476cc82f1ec957c999366d75fc2de17625e2e2230ac32a145ee3e43431" 180 + dependencies = [ 181 + "anyhow", 182 + "atproto-identity", 183 + "atproto-oauth", 184 + "atproto-record", 185 + "bytes", 186 + "reqwest", 187 + "reqwest-chain", 188 + "reqwest-middleware", 189 + "serde", 190 + "serde_json", 191 + "thiserror 2.0.12", 192 + "tokio", 193 + "tracing", 194 + "urlencoding", 195 + ] 196 + 197 + [[package]] 198 + name = "atproto-identity" 199 + version = "0.9.3" 200 + source = "registry+https://github.com/rust-lang/crates.io-index" 201 + checksum = "af9c670b1082a66c128be205fdfdd1ccc51529076104677d2914bb8089c0d6bf" 202 + dependencies = [ 203 + "anyhow", 204 + "async-trait", 205 + "axum", 206 + "ecdsa", 207 + "elliptic-curve", 208 + "hickory-resolver", 209 + "http", 210 + "k256", 211 + "lru", 212 + "multibase", 213 + "p256", 214 + "p384", 215 + "rand 0.8.5", 216 + "reqwest", 217 + "serde", 218 + "serde_ipld_dagcbor", 219 + "serde_json", 220 + "thiserror 2.0.12", 221 + "tokio", 222 + "tracing", 223 + ] 224 + 225 + [[package]] 226 + name = "atproto-jetstream" 227 + version = "0.9.3" 228 + source = "registry+https://github.com/rust-lang/crates.io-index" 229 + checksum = "41a61cbcdb44064d33e69cc38e1ec516cb0d534113339f0ed51f22b8a188f098" 230 + dependencies = [ 231 + "anyhow", 232 + "async-trait", 233 + "atproto-identity", 234 + "futures", 235 + "http", 236 + "serde", 237 + "serde_json", 238 + "thiserror 2.0.12", 239 + "tokio", 240 + "tokio-util", 241 + "tokio-websockets", 242 + "tracing", 243 + "tracing-subscriber", 244 + "urlencoding", 245 + "zstd", 246 + ] 247 + 248 + [[package]] 249 + name = "atproto-oauth" 250 + version = "0.9.3" 251 + source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "20cf08578f77fe77a0bc02a09cfe1a46f709603a70b3467b65368d82461d39ed" 253 + dependencies = [ 254 + "anyhow", 255 + "async-trait", 256 + "atproto-identity", 257 + "axum", 258 + "base64", 259 + "chrono", 260 + "ecdsa", 261 + "elliptic-curve", 262 + "http", 263 + "k256", 264 + "lru", 265 + "multibase", 266 + "p256", 267 + "p384", 268 + "rand 0.8.5", 269 + "reqwest", 270 + "reqwest-chain", 271 + "reqwest-middleware", 272 + "serde", 273 + "serde_ipld_dagcbor", 274 + "serde_json", 275 + "sha2", 276 + "thiserror 2.0.12", 277 + "tokio", 278 + "tracing", 279 + "ulid", 280 + ] 281 + 282 + [[package]] 283 + name = "atproto-record" 284 + version = "0.9.3" 285 + source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "97bab917de984b2c5cf8cec9d0dd7c0119217e776b647a99dc631f065e74f61c" 287 + dependencies = [ 288 + "anyhow", 289 + "atproto-identity", 290 + "chrono", 291 + "ecdsa", 292 + "k256", 293 + "multibase", 294 + "p256", 295 + "serde", 296 + "serde_ipld_dagcbor", 297 + "serde_json", 298 + "thiserror 2.0.12", 299 + "tokio", 300 + "tracing", 301 + ] 302 + 303 + [[package]] 304 + name = "autocfg" 305 + version = "1.5.0" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 308 + 309 + [[package]] 310 + name = "av1-grain" 311 + version = "0.2.4" 312 + source = "registry+https://github.com/rust-lang/crates.io-index" 313 + checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" 314 + dependencies = [ 315 + "anyhow", 316 + "arrayvec", 317 + "log", 318 + "nom", 319 + "num-rational", 320 + "v_frame", 321 + ] 322 + 323 + [[package]] 324 + name = "avif-serialize" 325 + version = "0.8.5" 326 + source = "registry+https://github.com/rust-lang/crates.io-index" 327 + checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" 328 + dependencies = [ 329 + "arrayvec", 330 + ] 331 + 332 + [[package]] 333 + name = "axum" 334 + version = "0.8.4" 335 + source = "registry+https://github.com/rust-lang/crates.io-index" 336 + checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 337 + dependencies = [ 338 + "axum-core", 339 + "axum-macros", 340 + "bytes", 341 + "form_urlencoded", 342 + "futures-util", 343 + "http", 344 + "http-body", 345 + "http-body-util", 346 + "hyper", 347 + "hyper-util", 348 + "itoa", 349 + "matchit", 350 + "memchr", 351 + "mime", 352 + "percent-encoding", 353 + "pin-project-lite", 354 + "rustversion", 355 + "serde", 356 + "serde_json", 357 + "serde_path_to_error", 358 + "serde_urlencoded", 359 + "sync_wrapper", 360 + "tokio", 361 + "tower", 362 + "tower-layer", 363 + "tower-service", 364 + "tracing", 365 + ] 366 + 367 + [[package]] 368 + name = "axum-core" 369 + version = "0.5.2" 370 + source = "registry+https://github.com/rust-lang/crates.io-index" 371 + checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 372 + dependencies = [ 373 + "bytes", 374 + "futures-core", 375 + "http", 376 + "http-body", 377 + "http-body-util", 378 + "mime", 379 + "pin-project-lite", 380 + "rustversion", 381 + "sync_wrapper", 382 + "tower-layer", 383 + "tower-service", 384 + "tracing", 385 + ] 386 + 387 + [[package]] 388 + name = "axum-macros" 389 + version = "0.5.0" 390 + source = "registry+https://github.com/rust-lang/crates.io-index" 391 + checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" 392 + dependencies = [ 393 + "proc-macro2", 394 + "quote", 395 + "syn 2.0.104", 396 + ] 397 + 398 + [[package]] 399 + name = "axum-template" 400 + version = "3.0.0" 401 + source = "registry+https://github.com/rust-lang/crates.io-index" 402 + checksum = "3df50f7d669bfc3a8c348f08f536fe37e7acfbeded3cfdffd2ad3d76725fc40c" 403 + dependencies = [ 404 + "axum", 405 + "minijinja", 406 + "minijinja-autoreload", 407 + "serde", 408 + "thiserror 2.0.12", 409 + ] 410 + 411 + [[package]] 412 + name = "backtrace" 413 + version = "0.3.75" 414 + source = "registry+https://github.com/rust-lang/crates.io-index" 415 + checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 416 + dependencies = [ 417 + "addr2line", 418 + "cfg-if", 419 + "libc", 420 + "miniz_oxide", 421 + "object", 422 + "rustc-demangle", 423 + "windows-targets 0.52.6", 424 + ] 425 + 426 + [[package]] 427 + name = "base-x" 428 + version = "0.2.11" 429 + source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 431 + 432 + [[package]] 433 + name = "base16ct" 434 + version = "0.2.0" 435 + source = "registry+https://github.com/rust-lang/crates.io-index" 436 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 437 + 438 + [[package]] 439 + name = "base64" 440 + version = "0.22.1" 441 + source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 443 + 444 + [[package]] 445 + name = "base64ct" 446 + version = "1.8.0" 447 + source = "registry+https://github.com/rust-lang/crates.io-index" 448 + checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 449 + 450 + [[package]] 451 + name = "bincode" 452 + version = "1.3.3" 453 + source = "registry+https://github.com/rust-lang/crates.io-index" 454 + checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 455 + dependencies = [ 456 + "serde", 457 + ] 458 + 459 + [[package]] 460 + name = "bit-set" 461 + version = "0.5.3" 462 + source = "registry+https://github.com/rust-lang/crates.io-index" 463 + checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" 464 + dependencies = [ 465 + "bit-vec 0.6.3", 466 + ] 467 + 468 + [[package]] 469 + name = "bit-vec" 470 + version = "0.6.3" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" 473 + 474 + [[package]] 475 + name = "bit-vec" 476 + version = "0.7.0" 477 + source = "registry+https://github.com/rust-lang/crates.io-index" 478 + checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" 479 + 480 + [[package]] 481 + name = "bit_field" 482 + version = "0.10.2" 483 + source = "registry+https://github.com/rust-lang/crates.io-index" 484 + checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 485 + 486 + [[package]] 487 + name = "bitflags" 488 + version = "1.3.2" 489 + source = "registry+https://github.com/rust-lang/crates.io-index" 490 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 491 + 492 + [[package]] 493 + name = "bitflags" 494 + version = "2.9.1" 495 + source = "registry+https://github.com/rust-lang/crates.io-index" 496 + checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 497 + dependencies = [ 498 + "serde", 499 + ] 500 + 501 + [[package]] 502 + name = "bitstream-io" 503 + version = "2.6.0" 504 + source = "registry+https://github.com/rust-lang/crates.io-index" 505 + checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" 506 + 507 + [[package]] 508 + name = "blahg" 509 + version = "0.1.0" 510 + dependencies = [ 511 + "anyhow", 512 + "async-trait", 513 + "atproto-client", 514 + "atproto-identity", 515 + "atproto-jetstream", 516 + "atproto-record", 517 + "axum", 518 + "axum-template", 519 + "base64", 520 + "bloomfilter", 521 + "bytes", 522 + "chrono", 523 + "comrak", 524 + "duration-str", 525 + "ecdsa", 526 + "elliptic-curve", 527 + "hickory-resolver", 528 + "image", 529 + "k256", 530 + "lru", 531 + "minijinja", 532 + "minijinja-autoreload", 533 + "minijinja-embed", 534 + "minio", 535 + "multibase", 536 + "p256", 537 + "reqwest", 538 + "rust-embed", 539 + "serde", 540 + "serde_ipld_dagcbor", 541 + "serde_json", 542 + "sha2", 543 + "slugify", 544 + "sqlx", 545 + "tempfile", 546 + "thiserror 2.0.12", 547 + "tokio", 548 + "tokio-util", 549 + "tower-http 0.5.2", 550 + "tracing", 551 + "tracing-subscriber", 552 + ] 553 + 554 + [[package]] 555 + name = "block-buffer" 556 + version = "0.10.4" 557 + source = "registry+https://github.com/rust-lang/crates.io-index" 558 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 559 + dependencies = [ 560 + "generic-array", 561 + ] 562 + 563 + [[package]] 564 + name = "bloomfilter" 565 + version = "1.0.16" 566 + source = "registry+https://github.com/rust-lang/crates.io-index" 567 + checksum = "c541c70a910b485670304fd420f0eab8f7bde68439db6a8d98819c3d2774d7e2" 568 + dependencies = [ 569 + "bit-vec 0.7.0", 570 + "getrandom 0.2.16", 571 + "siphasher", 572 + ] 573 + 574 + [[package]] 575 + name = "bon" 576 + version = "3.6.4" 577 + source = "registry+https://github.com/rust-lang/crates.io-index" 578 + checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" 579 + dependencies = [ 580 + "bon-macros", 581 + "rustversion", 582 + ] 583 + 584 + [[package]] 585 + name = "bon-macros" 586 + version = "3.6.4" 587 + source = "registry+https://github.com/rust-lang/crates.io-index" 588 + checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" 589 + dependencies = [ 590 + "darling", 591 + "ident_case", 592 + "prettyplease", 593 + "proc-macro2", 594 + "quote", 595 + "rustversion", 596 + "syn 2.0.104", 597 + ] 598 + 599 + [[package]] 600 + name = "built" 601 + version = "0.7.7" 602 + source = "registry+https://github.com/rust-lang/crates.io-index" 603 + checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" 604 + 605 + [[package]] 606 + name = "bumpalo" 607 + version = "3.19.0" 608 + source = "registry+https://github.com/rust-lang/crates.io-index" 609 + checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 610 + 611 + [[package]] 612 + name = "bytemuck" 613 + version = "1.23.1" 614 + source = "registry+https://github.com/rust-lang/crates.io-index" 615 + checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" 616 + 617 + [[package]] 618 + name = "byteorder" 619 + version = "1.5.0" 620 + source = "registry+https://github.com/rust-lang/crates.io-index" 621 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 622 + 623 + [[package]] 624 + name = "byteorder-lite" 625 + version = "0.1.0" 626 + source = "registry+https://github.com/rust-lang/crates.io-index" 627 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 628 + 629 + [[package]] 630 + name = "bytes" 631 + version = "1.10.1" 632 + source = "registry+https://github.com/rust-lang/crates.io-index" 633 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 634 + 635 + [[package]] 636 + name = "caseless" 637 + version = "0.2.2" 638 + source = "registry+https://github.com/rust-lang/crates.io-index" 639 + checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" 640 + dependencies = [ 641 + "unicode-normalization", 642 + ] 643 + 644 + [[package]] 645 + name = "cbor4ii" 646 + version = "0.2.14" 647 + source = "registry+https://github.com/rust-lang/crates.io-index" 648 + checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" 649 + dependencies = [ 650 + "serde", 651 + ] 652 + 653 + [[package]] 654 + name = "cc" 655 + version = "1.2.29" 656 + source = "registry+https://github.com/rust-lang/crates.io-index" 657 + checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" 658 + dependencies = [ 659 + "jobserver", 660 + "libc", 661 + "shlex", 662 + ] 663 + 664 + [[package]] 665 + name = "cfg-expr" 666 + version = "0.15.8" 667 + source = "registry+https://github.com/rust-lang/crates.io-index" 668 + checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" 669 + dependencies = [ 670 + "smallvec", 671 + "target-lexicon", 672 + ] 673 + 674 + [[package]] 675 + name = "cfg-if" 676 + version = "1.0.1" 677 + source = "registry+https://github.com/rust-lang/crates.io-index" 678 + checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 679 + 680 + [[package]] 681 + name = "cfg_aliases" 682 + version = "0.2.1" 683 + source = "registry+https://github.com/rust-lang/crates.io-index" 684 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 685 + 686 + [[package]] 687 + name = "chrono" 688 + version = "0.4.41" 689 + source = "registry+https://github.com/rust-lang/crates.io-index" 690 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 691 + dependencies = [ 692 + "android-tzdata", 693 + "iana-time-zone", 694 + "js-sys", 695 + "num-traits", 696 + "serde", 697 + "wasm-bindgen", 698 + "windows-link", 699 + ] 700 + 701 + [[package]] 702 + name = "cid" 703 + version = "0.11.1" 704 + source = "registry+https://github.com/rust-lang/crates.io-index" 705 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 706 + dependencies = [ 707 + "core2", 708 + "multibase", 709 + "multihash", 710 + "serde", 711 + "serde_bytes", 712 + "unsigned-varint", 713 + ] 714 + 715 + [[package]] 716 + name = "clap" 717 + version = "4.5.40" 718 + source = "registry+https://github.com/rust-lang/crates.io-index" 719 + checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 720 + dependencies = [ 721 + "clap_builder", 722 + "clap_derive", 723 + ] 724 + 725 + [[package]] 726 + name = "clap_builder" 727 + version = "4.5.40" 728 + source = "registry+https://github.com/rust-lang/crates.io-index" 729 + checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 730 + dependencies = [ 731 + "anstream", 732 + "anstyle", 733 + "clap_lex", 734 + "strsim", 735 + "terminal_size", 736 + ] 737 + 738 + [[package]] 739 + name = "clap_derive" 740 + version = "4.5.40" 741 + source = "registry+https://github.com/rust-lang/crates.io-index" 742 + checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 743 + dependencies = [ 744 + "heck", 745 + "proc-macro2", 746 + "quote", 747 + "syn 2.0.104", 748 + ] 749 + 750 + [[package]] 751 + name = "clap_lex" 752 + version = "0.7.5" 753 + source = "registry+https://github.com/rust-lang/crates.io-index" 754 + checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 755 + 756 + [[package]] 757 + name = "color_quant" 758 + version = "1.1.0" 759 + source = "registry+https://github.com/rust-lang/crates.io-index" 760 + checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 761 + 762 + [[package]] 763 + name = "colorchoice" 764 + version = "1.0.4" 765 + source = "registry+https://github.com/rust-lang/crates.io-index" 766 + checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 767 + 768 + [[package]] 769 + name = "comrak" 770 + version = "0.39.1" 771 + source = "registry+https://github.com/rust-lang/crates.io-index" 772 + checksum = "2fefab951771fc3beeed0773ce66a4f7b706273fc6c4c95b08dd1615744abcf5" 773 + dependencies = [ 774 + "bon", 775 + "caseless", 776 + "clap", 777 + "entities", 778 + "memchr", 779 + "shell-words", 780 + "slug", 781 + "syntect", 782 + "typed-arena", 783 + "unicode_categories", 784 + "xdg", 785 + ] 786 + 787 + [[package]] 788 + name = "concurrent-queue" 789 + version = "2.5.0" 790 + source = "registry+https://github.com/rust-lang/crates.io-index" 791 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 792 + dependencies = [ 793 + "crossbeam-utils", 794 + ] 795 + 796 + [[package]] 797 + name = "const-oid" 798 + version = "0.9.6" 799 + source = "registry+https://github.com/rust-lang/crates.io-index" 800 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 801 + 802 + [[package]] 803 + name = "core-foundation" 804 + version = "0.9.4" 805 + source = "registry+https://github.com/rust-lang/crates.io-index" 806 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 807 + dependencies = [ 808 + "core-foundation-sys", 809 + "libc", 810 + ] 811 + 812 + [[package]] 813 + name = "core-foundation" 814 + version = "0.10.1" 815 + source = "registry+https://github.com/rust-lang/crates.io-index" 816 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 817 + dependencies = [ 818 + "core-foundation-sys", 819 + "libc", 820 + ] 821 + 822 + [[package]] 823 + name = "core-foundation-sys" 824 + version = "0.8.7" 825 + source = "registry+https://github.com/rust-lang/crates.io-index" 826 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 827 + 828 + [[package]] 829 + name = "core2" 830 + version = "0.4.0" 831 + source = "registry+https://github.com/rust-lang/crates.io-index" 832 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 833 + dependencies = [ 834 + "memchr", 835 + ] 836 + 837 + [[package]] 838 + name = "cpufeatures" 839 + version = "0.2.17" 840 + source = "registry+https://github.com/rust-lang/crates.io-index" 841 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 842 + dependencies = [ 843 + "libc", 844 + ] 845 + 846 + [[package]] 847 + name = "crc" 848 + version = "3.3.0" 849 + source = "registry+https://github.com/rust-lang/crates.io-index" 850 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 851 + dependencies = [ 852 + "crc-catalog", 853 + ] 854 + 855 + [[package]] 856 + name = "crc-catalog" 857 + version = "2.4.0" 858 + source = "registry+https://github.com/rust-lang/crates.io-index" 859 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 860 + 861 + [[package]] 862 + name = "crc32fast" 863 + version = "1.4.2" 864 + source = "registry+https://github.com/rust-lang/crates.io-index" 865 + checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 866 + dependencies = [ 867 + "cfg-if", 868 + ] 869 + 870 + [[package]] 871 + name = "critical-section" 872 + version = "1.2.0" 873 + source = "registry+https://github.com/rust-lang/crates.io-index" 874 + checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" 875 + 876 + [[package]] 877 + name = "crossbeam-channel" 878 + version = "0.5.15" 879 + source = "registry+https://github.com/rust-lang/crates.io-index" 880 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 881 + dependencies = [ 882 + "crossbeam-utils", 883 + ] 884 + 885 + [[package]] 886 + name = "crossbeam-deque" 887 + version = "0.8.6" 888 + source = "registry+https://github.com/rust-lang/crates.io-index" 889 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 890 + dependencies = [ 891 + "crossbeam-epoch", 892 + "crossbeam-utils", 893 + ] 894 + 895 + [[package]] 896 + name = "crossbeam-epoch" 897 + version = "0.9.18" 898 + source = "registry+https://github.com/rust-lang/crates.io-index" 899 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 900 + dependencies = [ 901 + "crossbeam-utils", 902 + ] 903 + 904 + [[package]] 905 + name = "crossbeam-queue" 906 + version = "0.3.12" 907 + source = "registry+https://github.com/rust-lang/crates.io-index" 908 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 909 + dependencies = [ 910 + "crossbeam-utils", 911 + ] 912 + 913 + [[package]] 914 + name = "crossbeam-utils" 915 + version = "0.8.21" 916 + source = "registry+https://github.com/rust-lang/crates.io-index" 917 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 918 + 919 + [[package]] 920 + name = "crunchy" 921 + version = "0.2.4" 922 + source = "registry+https://github.com/rust-lang/crates.io-index" 923 + checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 924 + 925 + [[package]] 926 + name = "crypto-bigint" 927 + version = "0.5.5" 928 + source = "registry+https://github.com/rust-lang/crates.io-index" 929 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 930 + dependencies = [ 931 + "generic-array", 932 + "rand_core 0.6.4", 933 + "subtle", 934 + "zeroize", 935 + ] 936 + 937 + [[package]] 938 + name = "crypto-common" 939 + version = "0.1.6" 940 + source = "registry+https://github.com/rust-lang/crates.io-index" 941 + checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 942 + dependencies = [ 943 + "generic-array", 944 + "typenum", 945 + ] 946 + 947 + [[package]] 948 + name = "darling" 949 + version = "0.20.11" 950 + source = "registry+https://github.com/rust-lang/crates.io-index" 951 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 952 + dependencies = [ 953 + "darling_core", 954 + "darling_macro", 955 + ] 956 + 957 + [[package]] 958 + name = "darling_core" 959 + version = "0.20.11" 960 + source = "registry+https://github.com/rust-lang/crates.io-index" 961 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 962 + dependencies = [ 963 + "fnv", 964 + "ident_case", 965 + "proc-macro2", 966 + "quote", 967 + "strsim", 968 + "syn 2.0.104", 969 + ] 970 + 971 + [[package]] 972 + name = "darling_macro" 973 + version = "0.20.11" 974 + source = "registry+https://github.com/rust-lang/crates.io-index" 975 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 976 + dependencies = [ 977 + "darling_core", 978 + "quote", 979 + "syn 2.0.104", 980 + ] 981 + 982 + [[package]] 983 + name = "dashmap" 984 + version = "6.1.0" 985 + source = "registry+https://github.com/rust-lang/crates.io-index" 986 + checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 987 + dependencies = [ 988 + "cfg-if", 989 + "crossbeam-utils", 990 + "hashbrown 0.14.5", 991 + "lock_api", 992 + "once_cell", 993 + "parking_lot_core", 994 + ] 995 + 996 + [[package]] 997 + name = "data-encoding" 998 + version = "2.9.0" 999 + source = "registry+https://github.com/rust-lang/crates.io-index" 1000 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 1001 + 1002 + [[package]] 1003 + name = "data-encoding-macro" 1004 + version = "0.1.18" 1005 + source = "registry+https://github.com/rust-lang/crates.io-index" 1006 + checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" 1007 + dependencies = [ 1008 + "data-encoding", 1009 + "data-encoding-macro-internal", 1010 + ] 1011 + 1012 + [[package]] 1013 + name = "data-encoding-macro-internal" 1014 + version = "0.1.16" 1015 + source = "registry+https://github.com/rust-lang/crates.io-index" 1016 + checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" 1017 + dependencies = [ 1018 + "data-encoding", 1019 + "syn 2.0.104", 1020 + ] 1021 + 1022 + [[package]] 1023 + name = "der" 1024 + version = "0.7.10" 1025 + source = "registry+https://github.com/rust-lang/crates.io-index" 1026 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 1027 + dependencies = [ 1028 + "const-oid", 1029 + "pem-rfc7468", 1030 + "zeroize", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "deranged" 1035 + version = "0.4.0" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 1038 + dependencies = [ 1039 + "powerfmt", 1040 + ] 1041 + 1042 + [[package]] 1043 + name = "derivative" 1044 + version = "2.2.0" 1045 + source = "registry+https://github.com/rust-lang/crates.io-index" 1046 + checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 1047 + dependencies = [ 1048 + "proc-macro2", 1049 + "quote", 1050 + "syn 1.0.109", 1051 + ] 1052 + 1053 + [[package]] 1054 + name = "deunicode" 1055 + version = "1.6.2" 1056 + source = "registry+https://github.com/rust-lang/crates.io-index" 1057 + checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" 1058 + 1059 + [[package]] 1060 + name = "digest" 1061 + version = "0.10.7" 1062 + source = "registry+https://github.com/rust-lang/crates.io-index" 1063 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 1064 + dependencies = [ 1065 + "block-buffer", 1066 + "const-oid", 1067 + "crypto-common", 1068 + "subtle", 1069 + ] 1070 + 1071 + [[package]] 1072 + name = "displaydoc" 1073 + version = "0.2.5" 1074 + source = "registry+https://github.com/rust-lang/crates.io-index" 1075 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 1076 + dependencies = [ 1077 + "proc-macro2", 1078 + "quote", 1079 + "syn 2.0.104", 1080 + ] 1081 + 1082 + [[package]] 1083 + name = "dotenvy" 1084 + version = "0.15.7" 1085 + source = "registry+https://github.com/rust-lang/crates.io-index" 1086 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 1087 + 1088 + [[package]] 1089 + name = "duration-str" 1090 + version = "0.11.3" 1091 + source = "registry+https://github.com/rust-lang/crates.io-index" 1092 + checksum = "f88959de2d447fd3eddcf1909d1f19fe084e27a056a6904203dc5d8b9e771c1e" 1093 + dependencies = [ 1094 + "chrono", 1095 + "rust_decimal", 1096 + "serde", 1097 + "thiserror 2.0.12", 1098 + "time", 1099 + "winnow 0.6.26", 1100 + ] 1101 + 1102 + [[package]] 1103 + name = "ecdsa" 1104 + version = "0.16.9" 1105 + source = "registry+https://github.com/rust-lang/crates.io-index" 1106 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 1107 + dependencies = [ 1108 + "der", 1109 + "digest", 1110 + "elliptic-curve", 1111 + "rfc6979", 1112 + "serdect", 1113 + "signature", 1114 + "spki", 1115 + ] 1116 + 1117 + [[package]] 1118 + name = "either" 1119 + version = "1.15.0" 1120 + source = "registry+https://github.com/rust-lang/crates.io-index" 1121 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 1122 + dependencies = [ 1123 + "serde", 1124 + ] 1125 + 1126 + [[package]] 1127 + name = "elliptic-curve" 1128 + version = "0.13.8" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 1131 + dependencies = [ 1132 + "base16ct", 1133 + "base64ct", 1134 + "crypto-bigint", 1135 + "digest", 1136 + "ff", 1137 + "generic-array", 1138 + "group", 1139 + "hkdf", 1140 + "pem-rfc7468", 1141 + "pkcs8", 1142 + "rand_core 0.6.4", 1143 + "sec1", 1144 + "serde_json", 1145 + "serdect", 1146 + "subtle", 1147 + "zeroize", 1148 + ] 1149 + 1150 + [[package]] 1151 + name = "encoding_rs" 1152 + version = "0.8.35" 1153 + source = "registry+https://github.com/rust-lang/crates.io-index" 1154 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 1155 + dependencies = [ 1156 + "cfg-if", 1157 + ] 1158 + 1159 + [[package]] 1160 + name = "entities" 1161 + version = "1.0.1" 1162 + source = "registry+https://github.com/rust-lang/crates.io-index" 1163 + checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" 1164 + 1165 + [[package]] 1166 + name = "enum-as-inner" 1167 + version = "0.6.1" 1168 + source = "registry+https://github.com/rust-lang/crates.io-index" 1169 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 1170 + dependencies = [ 1171 + "heck", 1172 + "proc-macro2", 1173 + "quote", 1174 + "syn 2.0.104", 1175 + ] 1176 + 1177 + [[package]] 1178 + name = "env_filter" 1179 + version = "0.1.3" 1180 + source = "registry+https://github.com/rust-lang/crates.io-index" 1181 + checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 1182 + dependencies = [ 1183 + "log", 1184 + "regex", 1185 + ] 1186 + 1187 + [[package]] 1188 + name = "env_logger" 1189 + version = "0.11.8" 1190 + source = "registry+https://github.com/rust-lang/crates.io-index" 1191 + checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 1192 + dependencies = [ 1193 + "anstream", 1194 + "anstyle", 1195 + "env_filter", 1196 + "jiff", 1197 + "log", 1198 + ] 1199 + 1200 + [[package]] 1201 + name = "equator" 1202 + version = "0.4.2" 1203 + source = "registry+https://github.com/rust-lang/crates.io-index" 1204 + checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" 1205 + dependencies = [ 1206 + "equator-macro", 1207 + ] 1208 + 1209 + [[package]] 1210 + name = "equator-macro" 1211 + version = "0.4.2" 1212 + source = "registry+https://github.com/rust-lang/crates.io-index" 1213 + checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" 1214 + dependencies = [ 1215 + "proc-macro2", 1216 + "quote", 1217 + "syn 2.0.104", 1218 + ] 1219 + 1220 + [[package]] 1221 + name = "equivalent" 1222 + version = "1.0.2" 1223 + source = "registry+https://github.com/rust-lang/crates.io-index" 1224 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 1225 + 1226 + [[package]] 1227 + name = "errno" 1228 + version = "0.3.13" 1229 + source = "registry+https://github.com/rust-lang/crates.io-index" 1230 + checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 1231 + dependencies = [ 1232 + "libc", 1233 + "windows-sys 0.60.2", 1234 + ] 1235 + 1236 + [[package]] 1237 + name = "etcetera" 1238 + version = "0.8.0" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 1241 + dependencies = [ 1242 + "cfg-if", 1243 + "home", 1244 + "windows-sys 0.48.0", 1245 + ] 1246 + 1247 + [[package]] 1248 + name = "event-listener" 1249 + version = "5.4.0" 1250 + source = "registry+https://github.com/rust-lang/crates.io-index" 1251 + checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" 1252 + dependencies = [ 1253 + "concurrent-queue", 1254 + "parking", 1255 + "pin-project-lite", 1256 + ] 1257 + 1258 + [[package]] 1259 + name = "exr" 1260 + version = "1.73.0" 1261 + source = "registry+https://github.com/rust-lang/crates.io-index" 1262 + checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 1263 + dependencies = [ 1264 + "bit_field", 1265 + "half", 1266 + "lebe", 1267 + "miniz_oxide", 1268 + "rayon-core", 1269 + "smallvec", 1270 + "zune-inflate", 1271 + ] 1272 + 1273 + [[package]] 1274 + name = "fancy-regex" 1275 + version = "0.11.0" 1276 + source = "registry+https://github.com/rust-lang/crates.io-index" 1277 + checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" 1278 + dependencies = [ 1279 + "bit-set", 1280 + "regex", 1281 + ] 1282 + 1283 + [[package]] 1284 + name = "fastrand" 1285 + version = "2.3.0" 1286 + source = "registry+https://github.com/rust-lang/crates.io-index" 1287 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1288 + 1289 + [[package]] 1290 + name = "fdeflate" 1291 + version = "0.3.7" 1292 + source = "registry+https://github.com/rust-lang/crates.io-index" 1293 + checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 1294 + dependencies = [ 1295 + "simd-adler32", 1296 + ] 1297 + 1298 + [[package]] 1299 + name = "ff" 1300 + version = "0.13.1" 1301 + source = "registry+https://github.com/rust-lang/crates.io-index" 1302 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1303 + dependencies = [ 1304 + "rand_core 0.6.4", 1305 + "subtle", 1306 + ] 1307 + 1308 + [[package]] 1309 + name = "flate2" 1310 + version = "1.1.2" 1311 + source = "registry+https://github.com/rust-lang/crates.io-index" 1312 + checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 1313 + dependencies = [ 1314 + "crc32fast", 1315 + "miniz_oxide", 1316 + ] 1317 + 1318 + [[package]] 1319 + name = "flume" 1320 + version = "0.11.1" 1321 + source = "registry+https://github.com/rust-lang/crates.io-index" 1322 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 1323 + dependencies = [ 1324 + "futures-core", 1325 + "futures-sink", 1326 + "spin", 1327 + ] 1328 + 1329 + [[package]] 1330 + name = "fnv" 1331 + version = "1.0.7" 1332 + source = "registry+https://github.com/rust-lang/crates.io-index" 1333 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 1334 + 1335 + [[package]] 1336 + name = "foldhash" 1337 + version = "0.1.5" 1338 + source = "registry+https://github.com/rust-lang/crates.io-index" 1339 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1340 + 1341 + [[package]] 1342 + name = "foreign-types" 1343 + version = "0.3.2" 1344 + source = "registry+https://github.com/rust-lang/crates.io-index" 1345 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 1346 + dependencies = [ 1347 + "foreign-types-shared", 1348 + ] 1349 + 1350 + [[package]] 1351 + name = "foreign-types-shared" 1352 + version = "0.1.1" 1353 + source = "registry+https://github.com/rust-lang/crates.io-index" 1354 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 1355 + 1356 + [[package]] 1357 + name = "form_urlencoded" 1358 + version = "1.2.1" 1359 + source = "registry+https://github.com/rust-lang/crates.io-index" 1360 + checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 1361 + dependencies = [ 1362 + "percent-encoding", 1363 + ] 1364 + 1365 + [[package]] 1366 + name = "fsevent-sys" 1367 + version = "4.1.0" 1368 + source = "registry+https://github.com/rust-lang/crates.io-index" 1369 + checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 1370 + dependencies = [ 1371 + "libc", 1372 + ] 1373 + 1374 + [[package]] 1375 + name = "futures" 1376 + version = "0.3.31" 1377 + source = "registry+https://github.com/rust-lang/crates.io-index" 1378 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 1379 + dependencies = [ 1380 + "futures-channel", 1381 + "futures-core", 1382 + "futures-executor", 1383 + "futures-io", 1384 + "futures-sink", 1385 + "futures-task", 1386 + "futures-util", 1387 + ] 1388 + 1389 + [[package]] 1390 + name = "futures-channel" 1391 + version = "0.3.31" 1392 + source = "registry+https://github.com/rust-lang/crates.io-index" 1393 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 1394 + dependencies = [ 1395 + "futures-core", 1396 + "futures-sink", 1397 + ] 1398 + 1399 + [[package]] 1400 + name = "futures-core" 1401 + version = "0.3.31" 1402 + source = "registry+https://github.com/rust-lang/crates.io-index" 1403 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 1404 + 1405 + [[package]] 1406 + name = "futures-executor" 1407 + version = "0.3.31" 1408 + source = "registry+https://github.com/rust-lang/crates.io-index" 1409 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 1410 + dependencies = [ 1411 + "futures-core", 1412 + "futures-task", 1413 + "futures-util", 1414 + ] 1415 + 1416 + [[package]] 1417 + name = "futures-intrusive" 1418 + version = "0.5.0" 1419 + source = "registry+https://github.com/rust-lang/crates.io-index" 1420 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 1421 + dependencies = [ 1422 + "futures-core", 1423 + "lock_api", 1424 + "parking_lot", 1425 + ] 1426 + 1427 + [[package]] 1428 + name = "futures-io" 1429 + version = "0.3.31" 1430 + source = "registry+https://github.com/rust-lang/crates.io-index" 1431 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1432 + 1433 + [[package]] 1434 + name = "futures-macro" 1435 + version = "0.3.31" 1436 + source = "registry+https://github.com/rust-lang/crates.io-index" 1437 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 1438 + dependencies = [ 1439 + "proc-macro2", 1440 + "quote", 1441 + "syn 2.0.104", 1442 + ] 1443 + 1444 + [[package]] 1445 + name = "futures-sink" 1446 + version = "0.3.31" 1447 + source = "registry+https://github.com/rust-lang/crates.io-index" 1448 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 1449 + 1450 + [[package]] 1451 + name = "futures-task" 1452 + version = "0.3.31" 1453 + source = "registry+https://github.com/rust-lang/crates.io-index" 1454 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 1455 + 1456 + [[package]] 1457 + name = "futures-util" 1458 + version = "0.3.31" 1459 + source = "registry+https://github.com/rust-lang/crates.io-index" 1460 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 1461 + dependencies = [ 1462 + "futures-channel", 1463 + "futures-core", 1464 + "futures-io", 1465 + "futures-macro", 1466 + "futures-sink", 1467 + "futures-task", 1468 + "memchr", 1469 + "pin-project-lite", 1470 + "pin-utils", 1471 + "slab", 1472 + ] 1473 + 1474 + [[package]] 1475 + name = "generator" 1476 + version = "0.8.5" 1477 + source = "registry+https://github.com/rust-lang/crates.io-index" 1478 + checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" 1479 + dependencies = [ 1480 + "cc", 1481 + "cfg-if", 1482 + "libc", 1483 + "log", 1484 + "rustversion", 1485 + "windows", 1486 + ] 1487 + 1488 + [[package]] 1489 + name = "generic-array" 1490 + version = "0.14.7" 1491 + source = "registry+https://github.com/rust-lang/crates.io-index" 1492 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 1493 + dependencies = [ 1494 + "typenum", 1495 + "version_check", 1496 + "zeroize", 1497 + ] 1498 + 1499 + [[package]] 1500 + name = "getrandom" 1501 + version = "0.2.16" 1502 + source = "registry+https://github.com/rust-lang/crates.io-index" 1503 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1504 + dependencies = [ 1505 + "cfg-if", 1506 + "js-sys", 1507 + "libc", 1508 + "wasi 0.11.1+wasi-snapshot-preview1", 1509 + "wasm-bindgen", 1510 + ] 1511 + 1512 + [[package]] 1513 + name = "getrandom" 1514 + version = "0.3.3" 1515 + source = "registry+https://github.com/rust-lang/crates.io-index" 1516 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1517 + dependencies = [ 1518 + "cfg-if", 1519 + "js-sys", 1520 + "libc", 1521 + "r-efi", 1522 + "wasi 0.14.2+wasi-0.2.4", 1523 + "wasm-bindgen", 1524 + ] 1525 + 1526 + [[package]] 1527 + name = "gif" 1528 + version = "0.13.3" 1529 + source = "registry+https://github.com/rust-lang/crates.io-index" 1530 + checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" 1531 + dependencies = [ 1532 + "color_quant", 1533 + "weezl", 1534 + ] 1535 + 1536 + [[package]] 1537 + name = "gimli" 1538 + version = "0.31.1" 1539 + source = "registry+https://github.com/rust-lang/crates.io-index" 1540 + checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1541 + 1542 + [[package]] 1543 + name = "group" 1544 + version = "0.13.0" 1545 + source = "registry+https://github.com/rust-lang/crates.io-index" 1546 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1547 + dependencies = [ 1548 + "ff", 1549 + "rand_core 0.6.4", 1550 + "subtle", 1551 + ] 1552 + 1553 + [[package]] 1554 + name = "h2" 1555 + version = "0.4.11" 1556 + source = "registry+https://github.com/rust-lang/crates.io-index" 1557 + checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" 1558 + dependencies = [ 1559 + "atomic-waker", 1560 + "bytes", 1561 + "fnv", 1562 + "futures-core", 1563 + "futures-sink", 1564 + "http", 1565 + "indexmap", 1566 + "slab", 1567 + "tokio", 1568 + "tokio-util", 1569 + "tracing", 1570 + ] 1571 + 1572 + [[package]] 1573 + name = "half" 1574 + version = "2.6.0" 1575 + source = "registry+https://github.com/rust-lang/crates.io-index" 1576 + checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 1577 + dependencies = [ 1578 + "cfg-if", 1579 + "crunchy", 1580 + ] 1581 + 1582 + [[package]] 1583 + name = "hashbrown" 1584 + version = "0.14.5" 1585 + source = "registry+https://github.com/rust-lang/crates.io-index" 1586 + checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 1587 + 1588 + [[package]] 1589 + name = "hashbrown" 1590 + version = "0.15.4" 1591 + source = "registry+https://github.com/rust-lang/crates.io-index" 1592 + checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 1593 + dependencies = [ 1594 + "allocator-api2", 1595 + "equivalent", 1596 + "foldhash", 1597 + ] 1598 + 1599 + [[package]] 1600 + name = "hashlink" 1601 + version = "0.10.0" 1602 + source = "registry+https://github.com/rust-lang/crates.io-index" 1603 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1604 + dependencies = [ 1605 + "hashbrown 0.15.4", 1606 + ] 1607 + 1608 + [[package]] 1609 + name = "heck" 1610 + version = "0.5.0" 1611 + source = "registry+https://github.com/rust-lang/crates.io-index" 1612 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1613 + 1614 + [[package]] 1615 + name = "hex" 1616 + version = "0.4.3" 1617 + source = "registry+https://github.com/rust-lang/crates.io-index" 1618 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1619 + 1620 + [[package]] 1621 + name = "hickory-proto" 1622 + version = "0.25.2" 1623 + source = "registry+https://github.com/rust-lang/crates.io-index" 1624 + checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" 1625 + dependencies = [ 1626 + "async-trait", 1627 + "cfg-if", 1628 + "data-encoding", 1629 + "enum-as-inner", 1630 + "futures-channel", 1631 + "futures-io", 1632 + "futures-util", 1633 + "idna", 1634 + "ipnet", 1635 + "once_cell", 1636 + "rand 0.9.1", 1637 + "ring", 1638 + "thiserror 2.0.12", 1639 + "tinyvec", 1640 + "tokio", 1641 + "tracing", 1642 + "url", 1643 + ] 1644 + 1645 + [[package]] 1646 + name = "hickory-resolver" 1647 + version = "0.25.2" 1648 + source = "registry+https://github.com/rust-lang/crates.io-index" 1649 + checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" 1650 + dependencies = [ 1651 + "cfg-if", 1652 + "futures-util", 1653 + "hickory-proto", 1654 + "ipconfig", 1655 + "moka", 1656 + "once_cell", 1657 + "parking_lot", 1658 + "rand 0.9.1", 1659 + "resolv-conf", 1660 + "smallvec", 1661 + "thiserror 2.0.12", 1662 + "tokio", 1663 + "tracing", 1664 + ] 1665 + 1666 + [[package]] 1667 + name = "hkdf" 1668 + version = "0.12.4" 1669 + source = "registry+https://github.com/rust-lang/crates.io-index" 1670 + checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 1671 + dependencies = [ 1672 + "hmac", 1673 + ] 1674 + 1675 + [[package]] 1676 + name = "hmac" 1677 + version = "0.12.1" 1678 + source = "registry+https://github.com/rust-lang/crates.io-index" 1679 + checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1680 + dependencies = [ 1681 + "digest", 1682 + ] 1683 + 1684 + [[package]] 1685 + name = "home" 1686 + version = "0.5.11" 1687 + source = "registry+https://github.com/rust-lang/crates.io-index" 1688 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 1689 + dependencies = [ 1690 + "windows-sys 0.59.0", 1691 + ] 1692 + 1693 + [[package]] 1694 + name = "http" 1695 + version = "1.3.1" 1696 + source = "registry+https://github.com/rust-lang/crates.io-index" 1697 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1698 + dependencies = [ 1699 + "bytes", 1700 + "fnv", 1701 + "itoa", 1702 + ] 1703 + 1704 + [[package]] 1705 + name = "http-body" 1706 + version = "1.0.1" 1707 + source = "registry+https://github.com/rust-lang/crates.io-index" 1708 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1709 + dependencies = [ 1710 + "bytes", 1711 + "http", 1712 + ] 1713 + 1714 + [[package]] 1715 + name = "http-body-util" 1716 + version = "0.1.3" 1717 + source = "registry+https://github.com/rust-lang/crates.io-index" 1718 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 1719 + dependencies = [ 1720 + "bytes", 1721 + "futures-core", 1722 + "http", 1723 + "http-body", 1724 + "pin-project-lite", 1725 + ] 1726 + 1727 + [[package]] 1728 + name = "http-range-header" 1729 + version = "0.4.2" 1730 + source = "registry+https://github.com/rust-lang/crates.io-index" 1731 + checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 1732 + 1733 + [[package]] 1734 + name = "httparse" 1735 + version = "1.10.1" 1736 + source = "registry+https://github.com/rust-lang/crates.io-index" 1737 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 1738 + 1739 + [[package]] 1740 + name = "httpdate" 1741 + version = "1.0.3" 1742 + source = "registry+https://github.com/rust-lang/crates.io-index" 1743 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1744 + 1745 + [[package]] 1746 + name = "hyper" 1747 + version = "1.6.0" 1748 + source = "registry+https://github.com/rust-lang/crates.io-index" 1749 + checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 1750 + dependencies = [ 1751 + "bytes", 1752 + "futures-channel", 1753 + "futures-util", 1754 + "h2", 1755 + "http", 1756 + "http-body", 1757 + "httparse", 1758 + "httpdate", 1759 + "itoa", 1760 + "pin-project-lite", 1761 + "smallvec", 1762 + "tokio", 1763 + "want", 1764 + ] 1765 + 1766 + [[package]] 1767 + name = "hyper-rustls" 1768 + version = "0.27.7" 1769 + source = "registry+https://github.com/rust-lang/crates.io-index" 1770 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1771 + dependencies = [ 1772 + "http", 1773 + "hyper", 1774 + "hyper-util", 1775 + "rustls", 1776 + "rustls-pki-types", 1777 + "tokio", 1778 + "tokio-rustls", 1779 + "tower-service", 1780 + "webpki-roots 1.0.1", 1781 + ] 1782 + 1783 + [[package]] 1784 + name = "hyper-tls" 1785 + version = "0.6.0" 1786 + source = "registry+https://github.com/rust-lang/crates.io-index" 1787 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 1788 + dependencies = [ 1789 + "bytes", 1790 + "http-body-util", 1791 + "hyper", 1792 + "hyper-util", 1793 + "native-tls", 1794 + "tokio", 1795 + "tokio-native-tls", 1796 + "tower-service", 1797 + ] 1798 + 1799 + [[package]] 1800 + name = "hyper-util" 1801 + version = "0.1.14" 1802 + source = "registry+https://github.com/rust-lang/crates.io-index" 1803 + checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" 1804 + dependencies = [ 1805 + "base64", 1806 + "bytes", 1807 + "futures-channel", 1808 + "futures-core", 1809 + "futures-util", 1810 + "http", 1811 + "http-body", 1812 + "hyper", 1813 + "ipnet", 1814 + "libc", 1815 + "percent-encoding", 1816 + "pin-project-lite", 1817 + "socket2", 1818 + "system-configuration", 1819 + "tokio", 1820 + "tower-service", 1821 + "tracing", 1822 + "windows-registry", 1823 + ] 1824 + 1825 + [[package]] 1826 + name = "iana-time-zone" 1827 + version = "0.1.63" 1828 + source = "registry+https://github.com/rust-lang/crates.io-index" 1829 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1830 + dependencies = [ 1831 + "android_system_properties", 1832 + "core-foundation-sys", 1833 + "iana-time-zone-haiku", 1834 + "js-sys", 1835 + "log", 1836 + "wasm-bindgen", 1837 + "windows-core", 1838 + ] 1839 + 1840 + [[package]] 1841 + name = "iana-time-zone-haiku" 1842 + version = "0.1.2" 1843 + source = "registry+https://github.com/rust-lang/crates.io-index" 1844 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1845 + dependencies = [ 1846 + "cc", 1847 + ] 1848 + 1849 + [[package]] 1850 + name = "icu_collections" 1851 + version = "2.0.0" 1852 + source = "registry+https://github.com/rust-lang/crates.io-index" 1853 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1854 + dependencies = [ 1855 + "displaydoc", 1856 + "potential_utf", 1857 + "yoke", 1858 + "zerofrom", 1859 + "zerovec", 1860 + ] 1861 + 1862 + [[package]] 1863 + name = "icu_locale_core" 1864 + version = "2.0.0" 1865 + source = "registry+https://github.com/rust-lang/crates.io-index" 1866 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1867 + dependencies = [ 1868 + "displaydoc", 1869 + "litemap", 1870 + "tinystr", 1871 + "writeable", 1872 + "zerovec", 1873 + ] 1874 + 1875 + [[package]] 1876 + name = "icu_normalizer" 1877 + version = "2.0.0" 1878 + source = "registry+https://github.com/rust-lang/crates.io-index" 1879 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1880 + dependencies = [ 1881 + "displaydoc", 1882 + "icu_collections", 1883 + "icu_normalizer_data", 1884 + "icu_properties", 1885 + "icu_provider", 1886 + "smallvec", 1887 + "zerovec", 1888 + ] 1889 + 1890 + [[package]] 1891 + name = "icu_normalizer_data" 1892 + version = "2.0.0" 1893 + source = "registry+https://github.com/rust-lang/crates.io-index" 1894 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1895 + 1896 + [[package]] 1897 + name = "icu_properties" 1898 + version = "2.0.1" 1899 + source = "registry+https://github.com/rust-lang/crates.io-index" 1900 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1901 + dependencies = [ 1902 + "displaydoc", 1903 + "icu_collections", 1904 + "icu_locale_core", 1905 + "icu_properties_data", 1906 + "icu_provider", 1907 + "potential_utf", 1908 + "zerotrie", 1909 + "zerovec", 1910 + ] 1911 + 1912 + [[package]] 1913 + name = "icu_properties_data" 1914 + version = "2.0.1" 1915 + source = "registry+https://github.com/rust-lang/crates.io-index" 1916 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1917 + 1918 + [[package]] 1919 + name = "icu_provider" 1920 + version = "2.0.0" 1921 + source = "registry+https://github.com/rust-lang/crates.io-index" 1922 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1923 + dependencies = [ 1924 + "displaydoc", 1925 + "icu_locale_core", 1926 + "stable_deref_trait", 1927 + "tinystr", 1928 + "writeable", 1929 + "yoke", 1930 + "zerofrom", 1931 + "zerotrie", 1932 + "zerovec", 1933 + ] 1934 + 1935 + [[package]] 1936 + name = "ident_case" 1937 + version = "1.0.1" 1938 + source = "registry+https://github.com/rust-lang/crates.io-index" 1939 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1940 + 1941 + [[package]] 1942 + name = "idna" 1943 + version = "1.0.3" 1944 + source = "registry+https://github.com/rust-lang/crates.io-index" 1945 + checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1946 + dependencies = [ 1947 + "idna_adapter", 1948 + "smallvec", 1949 + "utf8_iter", 1950 + ] 1951 + 1952 + [[package]] 1953 + name = "idna_adapter" 1954 + version = "1.2.1" 1955 + source = "registry+https://github.com/rust-lang/crates.io-index" 1956 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 1957 + dependencies = [ 1958 + "icu_normalizer", 1959 + "icu_properties", 1960 + ] 1961 + 1962 + [[package]] 1963 + name = "image" 1964 + version = "0.25.6" 1965 + source = "registry+https://github.com/rust-lang/crates.io-index" 1966 + checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" 1967 + dependencies = [ 1968 + "bytemuck", 1969 + "byteorder-lite", 1970 + "color_quant", 1971 + "exr", 1972 + "gif", 1973 + "image-webp", 1974 + "num-traits", 1975 + "png", 1976 + "qoi", 1977 + "ravif", 1978 + "rayon", 1979 + "rgb", 1980 + "tiff", 1981 + "zune-core", 1982 + "zune-jpeg", 1983 + ] 1984 + 1985 + [[package]] 1986 + name = "image-webp" 1987 + version = "0.2.3" 1988 + source = "registry+https://github.com/rust-lang/crates.io-index" 1989 + checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" 1990 + dependencies = [ 1991 + "byteorder-lite", 1992 + "quick-error", 1993 + ] 1994 + 1995 + [[package]] 1996 + name = "imgref" 1997 + version = "1.11.0" 1998 + source = "registry+https://github.com/rust-lang/crates.io-index" 1999 + checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" 2000 + 2001 + [[package]] 2002 + name = "indexmap" 2003 + version = "2.10.0" 2004 + source = "registry+https://github.com/rust-lang/crates.io-index" 2005 + checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 2006 + dependencies = [ 2007 + "equivalent", 2008 + "hashbrown 0.15.4", 2009 + ] 2010 + 2011 + [[package]] 2012 + name = "inotify" 2013 + version = "0.11.0" 2014 + source = "registry+https://github.com/rust-lang/crates.io-index" 2015 + checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" 2016 + dependencies = [ 2017 + "bitflags 2.9.1", 2018 + "inotify-sys", 2019 + "libc", 2020 + ] 2021 + 2022 + [[package]] 2023 + name = "inotify-sys" 2024 + version = "0.1.5" 2025 + source = "registry+https://github.com/rust-lang/crates.io-index" 2026 + checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 2027 + dependencies = [ 2028 + "libc", 2029 + ] 2030 + 2031 + [[package]] 2032 + name = "interpolate_name" 2033 + version = "0.2.4" 2034 + source = "registry+https://github.com/rust-lang/crates.io-index" 2035 + checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" 2036 + dependencies = [ 2037 + "proc-macro2", 2038 + "quote", 2039 + "syn 2.0.104", 2040 + ] 2041 + 2042 + [[package]] 2043 + name = "io-uring" 2044 + version = "0.7.8" 2045 + source = "registry+https://github.com/rust-lang/crates.io-index" 2046 + checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" 2047 + dependencies = [ 2048 + "bitflags 2.9.1", 2049 + "cfg-if", 2050 + "libc", 2051 + ] 2052 + 2053 + [[package]] 2054 + name = "ipconfig" 2055 + version = "0.3.2" 2056 + source = "registry+https://github.com/rust-lang/crates.io-index" 2057 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 2058 + dependencies = [ 2059 + "socket2", 2060 + "widestring", 2061 + "windows-sys 0.48.0", 2062 + "winreg", 2063 + ] 2064 + 2065 + [[package]] 2066 + name = "ipld-core" 2067 + version = "0.4.2" 2068 + source = "registry+https://github.com/rust-lang/crates.io-index" 2069 + checksum = "104718b1cc124d92a6d01ca9c9258a7df311405debb3408c445a36452f9bf8db" 2070 + dependencies = [ 2071 + "cid", 2072 + "serde", 2073 + "serde_bytes", 2074 + ] 2075 + 2076 + [[package]] 2077 + name = "ipnet" 2078 + version = "2.11.0" 2079 + source = "registry+https://github.com/rust-lang/crates.io-index" 2080 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 2081 + 2082 + [[package]] 2083 + name = "iri-string" 2084 + version = "0.7.8" 2085 + source = "registry+https://github.com/rust-lang/crates.io-index" 2086 + checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 2087 + dependencies = [ 2088 + "memchr", 2089 + "serde", 2090 + ] 2091 + 2092 + [[package]] 2093 + name = "is_terminal_polyfill" 2094 + version = "1.70.1" 2095 + source = "registry+https://github.com/rust-lang/crates.io-index" 2096 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 2097 + 2098 + [[package]] 2099 + name = "itertools" 2100 + version = "0.12.1" 2101 + source = "registry+https://github.com/rust-lang/crates.io-index" 2102 + checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 2103 + dependencies = [ 2104 + "either", 2105 + ] 2106 + 2107 + [[package]] 2108 + name = "itoa" 2109 + version = "1.0.15" 2110 + source = "registry+https://github.com/rust-lang/crates.io-index" 2111 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 2112 + 2113 + [[package]] 2114 + name = "jiff" 2115 + version = "0.2.15" 2116 + source = "registry+https://github.com/rust-lang/crates.io-index" 2117 + checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" 2118 + dependencies = [ 2119 + "jiff-static", 2120 + "log", 2121 + "portable-atomic", 2122 + "portable-atomic-util", 2123 + "serde", 2124 + ] 2125 + 2126 + [[package]] 2127 + name = "jiff-static" 2128 + version = "0.2.15" 2129 + source = "registry+https://github.com/rust-lang/crates.io-index" 2130 + checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" 2131 + dependencies = [ 2132 + "proc-macro2", 2133 + "quote", 2134 + "syn 2.0.104", 2135 + ] 2136 + 2137 + [[package]] 2138 + name = "jobserver" 2139 + version = "0.1.33" 2140 + source = "registry+https://github.com/rust-lang/crates.io-index" 2141 + checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 2142 + dependencies = [ 2143 + "getrandom 0.3.3", 2144 + "libc", 2145 + ] 2146 + 2147 + [[package]] 2148 + name = "jpeg-decoder" 2149 + version = "0.3.2" 2150 + source = "registry+https://github.com/rust-lang/crates.io-index" 2151 + checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" 2152 + 2153 + [[package]] 2154 + name = "js-sys" 2155 + version = "0.3.77" 2156 + source = "registry+https://github.com/rust-lang/crates.io-index" 2157 + checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 2158 + dependencies = [ 2159 + "once_cell", 2160 + "wasm-bindgen", 2161 + ] 2162 + 2163 + [[package]] 2164 + name = "k256" 2165 + version = "0.13.4" 2166 + source = "registry+https://github.com/rust-lang/crates.io-index" 2167 + checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" 2168 + dependencies = [ 2169 + "cfg-if", 2170 + "ecdsa", 2171 + "elliptic-curve", 2172 + "once_cell", 2173 + "sha2", 2174 + "signature", 2175 + ] 2176 + 2177 + [[package]] 2178 + name = "kqueue" 2179 + version = "1.1.1" 2180 + source = "registry+https://github.com/rust-lang/crates.io-index" 2181 + checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" 2182 + dependencies = [ 2183 + "kqueue-sys", 2184 + "libc", 2185 + ] 2186 + 2187 + [[package]] 2188 + name = "kqueue-sys" 2189 + version = "1.0.4" 2190 + source = "registry+https://github.com/rust-lang/crates.io-index" 2191 + checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" 2192 + dependencies = [ 2193 + "bitflags 1.3.2", 2194 + "libc", 2195 + ] 2196 + 2197 + [[package]] 2198 + name = "lazy_static" 2199 + version = "1.5.0" 2200 + source = "registry+https://github.com/rust-lang/crates.io-index" 2201 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 2202 + dependencies = [ 2203 + "spin", 2204 + ] 2205 + 2206 + [[package]] 2207 + name = "lebe" 2208 + version = "0.5.2" 2209 + source = "registry+https://github.com/rust-lang/crates.io-index" 2210 + checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 2211 + 2212 + [[package]] 2213 + name = "libc" 2214 + version = "0.2.174" 2215 + source = "registry+https://github.com/rust-lang/crates.io-index" 2216 + checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 2217 + 2218 + [[package]] 2219 + name = "libfuzzer-sys" 2220 + version = "0.4.10" 2221 + source = "registry+https://github.com/rust-lang/crates.io-index" 2222 + checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" 2223 + dependencies = [ 2224 + "arbitrary", 2225 + "cc", 2226 + ] 2227 + 2228 + [[package]] 2229 + name = "libm" 2230 + version = "0.2.15" 2231 + source = "registry+https://github.com/rust-lang/crates.io-index" 2232 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 2233 + 2234 + [[package]] 2235 + name = "libsqlite3-sys" 2236 + version = "0.30.1" 2237 + source = "registry+https://github.com/rust-lang/crates.io-index" 2238 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 2239 + dependencies = [ 2240 + "cc", 2241 + "pkg-config", 2242 + "vcpkg", 2243 + ] 2244 + 2245 + [[package]] 2246 + name = "linked-hash-map" 2247 + version = "0.5.6" 2248 + source = "registry+https://github.com/rust-lang/crates.io-index" 2249 + checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 2250 + 2251 + [[package]] 2252 + name = "linux-raw-sys" 2253 + version = "0.9.4" 2254 + source = "registry+https://github.com/rust-lang/crates.io-index" 2255 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 2256 + 2257 + [[package]] 2258 + name = "litemap" 2259 + version = "0.8.0" 2260 + source = "registry+https://github.com/rust-lang/crates.io-index" 2261 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 2262 + 2263 + [[package]] 2264 + name = "lock_api" 2265 + version = "0.4.13" 2266 + source = "registry+https://github.com/rust-lang/crates.io-index" 2267 + checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 2268 + dependencies = [ 2269 + "autocfg", 2270 + "scopeguard", 2271 + ] 2272 + 2273 + [[package]] 2274 + name = "log" 2275 + version = "0.4.27" 2276 + source = "registry+https://github.com/rust-lang/crates.io-index" 2277 + checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 2278 + 2279 + [[package]] 2280 + name = "loom" 2281 + version = "0.7.2" 2282 + source = "registry+https://github.com/rust-lang/crates.io-index" 2283 + checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" 2284 + dependencies = [ 2285 + "cfg-if", 2286 + "generator", 2287 + "scoped-tls", 2288 + "tracing", 2289 + "tracing-subscriber", 2290 + ] 2291 + 2292 + [[package]] 2293 + name = "loop9" 2294 + version = "0.1.5" 2295 + source = "registry+https://github.com/rust-lang/crates.io-index" 2296 + checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" 2297 + dependencies = [ 2298 + "imgref", 2299 + ] 2300 + 2301 + [[package]] 2302 + name = "lru" 2303 + version = "0.12.5" 2304 + source = "registry+https://github.com/rust-lang/crates.io-index" 2305 + checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 2306 + dependencies = [ 2307 + "hashbrown 0.15.4", 2308 + ] 2309 + 2310 + [[package]] 2311 + name = "lru-slab" 2312 + version = "0.1.2" 2313 + source = "registry+https://github.com/rust-lang/crates.io-index" 2314 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2315 + 2316 + [[package]] 2317 + name = "matchers" 2318 + version = "0.1.0" 2319 + source = "registry+https://github.com/rust-lang/crates.io-index" 2320 + checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 2321 + dependencies = [ 2322 + "regex-automata 0.1.10", 2323 + ] 2324 + 2325 + [[package]] 2326 + name = "matchit" 2327 + version = "0.8.4" 2328 + source = "registry+https://github.com/rust-lang/crates.io-index" 2329 + checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 2330 + 2331 + [[package]] 2332 + name = "maybe-rayon" 2333 + version = "0.1.1" 2334 + source = "registry+https://github.com/rust-lang/crates.io-index" 2335 + checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" 2336 + dependencies = [ 2337 + "cfg-if", 2338 + "rayon", 2339 + ] 2340 + 2341 + [[package]] 2342 + name = "md-5" 2343 + version = "0.10.6" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 2346 + dependencies = [ 2347 + "cfg-if", 2348 + "digest", 2349 + ] 2350 + 2351 + [[package]] 2352 + name = "md5" 2353 + version = "0.7.0" 2354 + source = "registry+https://github.com/rust-lang/crates.io-index" 2355 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 2356 + 2357 + [[package]] 2358 + name = "memchr" 2359 + version = "2.7.5" 2360 + source = "registry+https://github.com/rust-lang/crates.io-index" 2361 + checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 2362 + 2363 + [[package]] 2364 + name = "memo-map" 2365 + version = "0.3.3" 2366 + source = "registry+https://github.com/rust-lang/crates.io-index" 2367 + checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" 2368 + 2369 + [[package]] 2370 + name = "mime" 2371 + version = "0.3.17" 2372 + source = "registry+https://github.com/rust-lang/crates.io-index" 2373 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 2374 + 2375 + [[package]] 2376 + name = "mime_guess" 2377 + version = "2.0.5" 2378 + source = "registry+https://github.com/rust-lang/crates.io-index" 2379 + checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 2380 + dependencies = [ 2381 + "mime", 2382 + "unicase", 2383 + ] 2384 + 2385 + [[package]] 2386 + name = "minijinja" 2387 + version = "2.11.0" 2388 + source = "registry+https://github.com/rust-lang/crates.io-index" 2389 + checksum = "4e60ac08614cc09062820e51d5d94c2fce16b94ea4e5003bb81b99a95f84e876" 2390 + dependencies = [ 2391 + "memo-map", 2392 + "self_cell", 2393 + "serde", 2394 + ] 2395 + 2396 + [[package]] 2397 + name = "minijinja-autoreload" 2398 + version = "2.11.0" 2399 + source = "registry+https://github.com/rust-lang/crates.io-index" 2400 + checksum = "15af8645fdf8689fdd932492efbb48d4478059f4eae9f476c93058e98a149ce3" 2401 + dependencies = [ 2402 + "minijinja", 2403 + "notify", 2404 + ] 2405 + 2406 + [[package]] 2407 + name = "minijinja-embed" 2408 + version = "2.11.0" 2409 + source = "registry+https://github.com/rust-lang/crates.io-index" 2410 + checksum = "c34af97e83bb3b2e61428e7c35224c9c1e1f6a2f465aa2ace41a713e3e856468" 2411 + 2412 + [[package]] 2413 + name = "minimal-lexical" 2414 + version = "0.2.1" 2415 + source = "registry+https://github.com/rust-lang/crates.io-index" 2416 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 2417 + 2418 + [[package]] 2419 + name = "minio" 2420 + version = "0.3.0" 2421 + source = "registry+https://github.com/rust-lang/crates.io-index" 2422 + checksum = "3824101357fa899d01c729e4a245776e20a03f2f6645979e86b9d3d5d9c42741" 2423 + dependencies = [ 2424 + "async-recursion", 2425 + "async-trait", 2426 + "base64", 2427 + "byteorder", 2428 + "bytes", 2429 + "chrono", 2430 + "crc", 2431 + "dashmap", 2432 + "derivative", 2433 + "env_logger", 2434 + "futures", 2435 + "futures-util", 2436 + "hex", 2437 + "hmac", 2438 + "http", 2439 + "hyper", 2440 + "lazy_static", 2441 + "log", 2442 + "md5", 2443 + "multimap", 2444 + "percent-encoding", 2445 + "rand 0.8.5", 2446 + "regex", 2447 + "reqwest", 2448 + "serde", 2449 + "serde_json", 2450 + "sha2", 2451 + "tokio", 2452 + "tokio-stream", 2453 + "tokio-util", 2454 + "urlencoding", 2455 + "xmltree", 2456 + ] 2457 + 2458 + [[package]] 2459 + name = "miniz_oxide" 2460 + version = "0.8.9" 2461 + source = "registry+https://github.com/rust-lang/crates.io-index" 2462 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 2463 + dependencies = [ 2464 + "adler2", 2465 + "simd-adler32", 2466 + ] 2467 + 2468 + [[package]] 2469 + name = "mio" 2470 + version = "1.0.4" 2471 + source = "registry+https://github.com/rust-lang/crates.io-index" 2472 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2473 + dependencies = [ 2474 + "libc", 2475 + "log", 2476 + "wasi 0.11.1+wasi-snapshot-preview1", 2477 + "windows-sys 0.59.0", 2478 + ] 2479 + 2480 + [[package]] 2481 + name = "moka" 2482 + version = "0.12.10" 2483 + source = "registry+https://github.com/rust-lang/crates.io-index" 2484 + checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" 2485 + dependencies = [ 2486 + "crossbeam-channel", 2487 + "crossbeam-epoch", 2488 + "crossbeam-utils", 2489 + "loom", 2490 + "parking_lot", 2491 + "portable-atomic", 2492 + "rustc_version", 2493 + "smallvec", 2494 + "tagptr", 2495 + "thiserror 1.0.69", 2496 + "uuid", 2497 + ] 2498 + 2499 + [[package]] 2500 + name = "multibase" 2501 + version = "0.9.1" 2502 + source = "registry+https://github.com/rust-lang/crates.io-index" 2503 + checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" 2504 + dependencies = [ 2505 + "base-x", 2506 + "data-encoding", 2507 + "data-encoding-macro", 2508 + ] 2509 + 2510 + [[package]] 2511 + name = "multihash" 2512 + version = "0.19.3" 2513 + source = "registry+https://github.com/rust-lang/crates.io-index" 2514 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 2515 + dependencies = [ 2516 + "core2", 2517 + "serde", 2518 + "unsigned-varint", 2519 + ] 2520 + 2521 + [[package]] 2522 + name = "multimap" 2523 + version = "0.10.1" 2524 + source = "registry+https://github.com/rust-lang/crates.io-index" 2525 + checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" 2526 + dependencies = [ 2527 + "serde", 2528 + ] 2529 + 2530 + [[package]] 2531 + name = "native-tls" 2532 + version = "0.2.14" 2533 + source = "registry+https://github.com/rust-lang/crates.io-index" 2534 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 2535 + dependencies = [ 2536 + "libc", 2537 + "log", 2538 + "openssl", 2539 + "openssl-probe", 2540 + "openssl-sys", 2541 + "schannel", 2542 + "security-framework 2.11.1", 2543 + "security-framework-sys", 2544 + "tempfile", 2545 + ] 2546 + 2547 + [[package]] 2548 + name = "new_debug_unreachable" 2549 + version = "1.0.6" 2550 + source = "registry+https://github.com/rust-lang/crates.io-index" 2551 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 2552 + 2553 + [[package]] 2554 + name = "nom" 2555 + version = "7.1.3" 2556 + source = "registry+https://github.com/rust-lang/crates.io-index" 2557 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2558 + dependencies = [ 2559 + "memchr", 2560 + "minimal-lexical", 2561 + ] 2562 + 2563 + [[package]] 2564 + name = "noop_proc_macro" 2565 + version = "0.3.0" 2566 + source = "registry+https://github.com/rust-lang/crates.io-index" 2567 + checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 2568 + 2569 + [[package]] 2570 + name = "notify" 2571 + version = "8.1.0" 2572 + source = "registry+https://github.com/rust-lang/crates.io-index" 2573 + checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97" 2574 + dependencies = [ 2575 + "bitflags 2.9.1", 2576 + "fsevent-sys", 2577 + "inotify", 2578 + "kqueue", 2579 + "libc", 2580 + "log", 2581 + "mio", 2582 + "notify-types", 2583 + "walkdir", 2584 + "windows-sys 0.60.2", 2585 + ] 2586 + 2587 + [[package]] 2588 + name = "notify-types" 2589 + version = "2.0.0" 2590 + source = "registry+https://github.com/rust-lang/crates.io-index" 2591 + checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 2592 + 2593 + [[package]] 2594 + name = "nu-ansi-term" 2595 + version = "0.46.0" 2596 + source = "registry+https://github.com/rust-lang/crates.io-index" 2597 + checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 2598 + dependencies = [ 2599 + "overload", 2600 + "winapi", 2601 + ] 2602 + 2603 + [[package]] 2604 + name = "num-bigint" 2605 + version = "0.4.6" 2606 + source = "registry+https://github.com/rust-lang/crates.io-index" 2607 + checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 2608 + dependencies = [ 2609 + "num-integer", 2610 + "num-traits", 2611 + ] 2612 + 2613 + [[package]] 2614 + name = "num-bigint-dig" 2615 + version = "0.8.4" 2616 + source = "registry+https://github.com/rust-lang/crates.io-index" 2617 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 2618 + dependencies = [ 2619 + "byteorder", 2620 + "lazy_static", 2621 + "libm", 2622 + "num-integer", 2623 + "num-iter", 2624 + "num-traits", 2625 + "rand 0.8.5", 2626 + "smallvec", 2627 + "zeroize", 2628 + ] 2629 + 2630 + [[package]] 2631 + name = "num-conv" 2632 + version = "0.1.0" 2633 + source = "registry+https://github.com/rust-lang/crates.io-index" 2634 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 2635 + 2636 + [[package]] 2637 + name = "num-derive" 2638 + version = "0.4.2" 2639 + source = "registry+https://github.com/rust-lang/crates.io-index" 2640 + checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 2641 + dependencies = [ 2642 + "proc-macro2", 2643 + "quote", 2644 + "syn 2.0.104", 2645 + ] 2646 + 2647 + [[package]] 2648 + name = "num-integer" 2649 + version = "0.1.46" 2650 + source = "registry+https://github.com/rust-lang/crates.io-index" 2651 + checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 2652 + dependencies = [ 2653 + "num-traits", 2654 + ] 2655 + 2656 + [[package]] 2657 + name = "num-iter" 2658 + version = "0.1.45" 2659 + source = "registry+https://github.com/rust-lang/crates.io-index" 2660 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 2661 + dependencies = [ 2662 + "autocfg", 2663 + "num-integer", 2664 + "num-traits", 2665 + ] 2666 + 2667 + [[package]] 2668 + name = "num-rational" 2669 + version = "0.4.2" 2670 + source = "registry+https://github.com/rust-lang/crates.io-index" 2671 + checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 2672 + dependencies = [ 2673 + "num-bigint", 2674 + "num-integer", 2675 + "num-traits", 2676 + ] 2677 + 2678 + [[package]] 2679 + name = "num-traits" 2680 + version = "0.2.19" 2681 + source = "registry+https://github.com/rust-lang/crates.io-index" 2682 + checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 2683 + dependencies = [ 2684 + "autocfg", 2685 + "libm", 2686 + ] 2687 + 2688 + [[package]] 2689 + name = "object" 2690 + version = "0.36.7" 2691 + source = "registry+https://github.com/rust-lang/crates.io-index" 2692 + checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 2693 + dependencies = [ 2694 + "memchr", 2695 + ] 2696 + 2697 + [[package]] 2698 + name = "once_cell" 2699 + version = "1.21.3" 2700 + source = "registry+https://github.com/rust-lang/crates.io-index" 2701 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 2702 + dependencies = [ 2703 + "critical-section", 2704 + "portable-atomic", 2705 + ] 2706 + 2707 + [[package]] 2708 + name = "once_cell_polyfill" 2709 + version = "1.70.1" 2710 + source = "registry+https://github.com/rust-lang/crates.io-index" 2711 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 2712 + 2713 + [[package]] 2714 + name = "onig" 2715 + version = "6.5.1" 2716 + source = "registry+https://github.com/rust-lang/crates.io-index" 2717 + checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" 2718 + dependencies = [ 2719 + "bitflags 2.9.1", 2720 + "libc", 2721 + "once_cell", 2722 + "onig_sys", 2723 + ] 2724 + 2725 + [[package]] 2726 + name = "onig_sys" 2727 + version = "69.9.1" 2728 + source = "registry+https://github.com/rust-lang/crates.io-index" 2729 + checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" 2730 + dependencies = [ 2731 + "cc", 2732 + "pkg-config", 2733 + ] 2734 + 2735 + [[package]] 2736 + name = "openssl" 2737 + version = "0.10.73" 2738 + source = "registry+https://github.com/rust-lang/crates.io-index" 2739 + checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 2740 + dependencies = [ 2741 + "bitflags 2.9.1", 2742 + "cfg-if", 2743 + "foreign-types", 2744 + "libc", 2745 + "once_cell", 2746 + "openssl-macros", 2747 + "openssl-sys", 2748 + ] 2749 + 2750 + [[package]] 2751 + name = "openssl-macros" 2752 + version = "0.1.1" 2753 + source = "registry+https://github.com/rust-lang/crates.io-index" 2754 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 2755 + dependencies = [ 2756 + "proc-macro2", 2757 + "quote", 2758 + "syn 2.0.104", 2759 + ] 2760 + 2761 + [[package]] 2762 + name = "openssl-probe" 2763 + version = "0.1.6" 2764 + source = "registry+https://github.com/rust-lang/crates.io-index" 2765 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2766 + 2767 + [[package]] 2768 + name = "openssl-sys" 2769 + version = "0.9.109" 2770 + source = "registry+https://github.com/rust-lang/crates.io-index" 2771 + checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 2772 + dependencies = [ 2773 + "cc", 2774 + "libc", 2775 + "pkg-config", 2776 + "vcpkg", 2777 + ] 2778 + 2779 + [[package]] 2780 + name = "overload" 2781 + version = "0.1.1" 2782 + source = "registry+https://github.com/rust-lang/crates.io-index" 2783 + checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 2784 + 2785 + [[package]] 2786 + name = "p256" 2787 + version = "0.13.2" 2788 + source = "registry+https://github.com/rust-lang/crates.io-index" 2789 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2790 + dependencies = [ 2791 + "ecdsa", 2792 + "elliptic-curve", 2793 + "primeorder", 2794 + "serdect", 2795 + "sha2", 2796 + ] 2797 + 2798 + [[package]] 2799 + name = "p384" 2800 + version = "0.13.1" 2801 + source = "registry+https://github.com/rust-lang/crates.io-index" 2802 + checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" 2803 + dependencies = [ 2804 + "ecdsa", 2805 + "elliptic-curve", 2806 + "primeorder", 2807 + "serdect", 2808 + "sha2", 2809 + ] 2810 + 2811 + [[package]] 2812 + name = "parking" 2813 + version = "2.2.1" 2814 + source = "registry+https://github.com/rust-lang/crates.io-index" 2815 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 2816 + 2817 + [[package]] 2818 + name = "parking_lot" 2819 + version = "0.12.4" 2820 + source = "registry+https://github.com/rust-lang/crates.io-index" 2821 + checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 2822 + dependencies = [ 2823 + "lock_api", 2824 + "parking_lot_core", 2825 + ] 2826 + 2827 + [[package]] 2828 + name = "parking_lot_core" 2829 + version = "0.9.11" 2830 + source = "registry+https://github.com/rust-lang/crates.io-index" 2831 + checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 2832 + dependencies = [ 2833 + "cfg-if", 2834 + "libc", 2835 + "redox_syscall", 2836 + "smallvec", 2837 + "windows-targets 0.52.6", 2838 + ] 2839 + 2840 + [[package]] 2841 + name = "paste" 2842 + version = "1.0.15" 2843 + source = "registry+https://github.com/rust-lang/crates.io-index" 2844 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 2845 + 2846 + [[package]] 2847 + name = "pem-rfc7468" 2848 + version = "0.7.0" 2849 + source = "registry+https://github.com/rust-lang/crates.io-index" 2850 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 2851 + dependencies = [ 2852 + "base64ct", 2853 + ] 2854 + 2855 + [[package]] 2856 + name = "percent-encoding" 2857 + version = "2.3.1" 2858 + source = "registry+https://github.com/rust-lang/crates.io-index" 2859 + checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 2860 + 2861 + [[package]] 2862 + name = "pin-project-lite" 2863 + version = "0.2.16" 2864 + source = "registry+https://github.com/rust-lang/crates.io-index" 2865 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 2866 + 2867 + [[package]] 2868 + name = "pin-utils" 2869 + version = "0.1.0" 2870 + source = "registry+https://github.com/rust-lang/crates.io-index" 2871 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2872 + 2873 + [[package]] 2874 + name = "pkcs1" 2875 + version = "0.7.5" 2876 + source = "registry+https://github.com/rust-lang/crates.io-index" 2877 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 2878 + dependencies = [ 2879 + "der", 2880 + "pkcs8", 2881 + "spki", 2882 + ] 2883 + 2884 + [[package]] 2885 + name = "pkcs8" 2886 + version = "0.10.2" 2887 + source = "registry+https://github.com/rust-lang/crates.io-index" 2888 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 2889 + dependencies = [ 2890 + "der", 2891 + "spki", 2892 + ] 2893 + 2894 + [[package]] 2895 + name = "pkg-config" 2896 + version = "0.3.32" 2897 + source = "registry+https://github.com/rust-lang/crates.io-index" 2898 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2899 + 2900 + [[package]] 2901 + name = "plist" 2902 + version = "1.7.3" 2903 + source = "registry+https://github.com/rust-lang/crates.io-index" 2904 + checksum = "546b279bf0638ee811d9e47de2ca5b66575a543035d79fdf83959dd2f5c3b4c3" 2905 + dependencies = [ 2906 + "base64", 2907 + "indexmap", 2908 + "quick-xml", 2909 + "serde", 2910 + "time", 2911 + ] 2912 + 2913 + [[package]] 2914 + name = "png" 2915 + version = "0.17.16" 2916 + source = "registry+https://github.com/rust-lang/crates.io-index" 2917 + checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 2918 + dependencies = [ 2919 + "bitflags 1.3.2", 2920 + "crc32fast", 2921 + "fdeflate", 2922 + "flate2", 2923 + "miniz_oxide", 2924 + ] 2925 + 2926 + [[package]] 2927 + name = "portable-atomic" 2928 + version = "1.11.1" 2929 + source = "registry+https://github.com/rust-lang/crates.io-index" 2930 + checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2931 + 2932 + [[package]] 2933 + name = "portable-atomic-util" 2934 + version = "0.2.4" 2935 + source = "registry+https://github.com/rust-lang/crates.io-index" 2936 + checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 2937 + dependencies = [ 2938 + "portable-atomic", 2939 + ] 2940 + 2941 + [[package]] 2942 + name = "potential_utf" 2943 + version = "0.1.2" 2944 + source = "registry+https://github.com/rust-lang/crates.io-index" 2945 + checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 2946 + dependencies = [ 2947 + "zerovec", 2948 + ] 2949 + 2950 + [[package]] 2951 + name = "powerfmt" 2952 + version = "0.2.0" 2953 + source = "registry+https://github.com/rust-lang/crates.io-index" 2954 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 2955 + 2956 + [[package]] 2957 + name = "ppv-lite86" 2958 + version = "0.2.21" 2959 + source = "registry+https://github.com/rust-lang/crates.io-index" 2960 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 2961 + dependencies = [ 2962 + "zerocopy", 2963 + ] 2964 + 2965 + [[package]] 2966 + name = "prettyplease" 2967 + version = "0.2.35" 2968 + source = "registry+https://github.com/rust-lang/crates.io-index" 2969 + checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" 2970 + dependencies = [ 2971 + "proc-macro2", 2972 + "syn 2.0.104", 2973 + ] 2974 + 2975 + [[package]] 2976 + name = "primeorder" 2977 + version = "0.13.6" 2978 + source = "registry+https://github.com/rust-lang/crates.io-index" 2979 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2980 + dependencies = [ 2981 + "elliptic-curve", 2982 + "serdect", 2983 + ] 2984 + 2985 + [[package]] 2986 + name = "proc-macro2" 2987 + version = "1.0.95" 2988 + source = "registry+https://github.com/rust-lang/crates.io-index" 2989 + checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 2990 + dependencies = [ 2991 + "unicode-ident", 2992 + ] 2993 + 2994 + [[package]] 2995 + name = "profiling" 2996 + version = "1.0.17" 2997 + source = "registry+https://github.com/rust-lang/crates.io-index" 2998 + checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" 2999 + dependencies = [ 3000 + "profiling-procmacros", 3001 + ] 3002 + 3003 + [[package]] 3004 + name = "profiling-procmacros" 3005 + version = "1.0.17" 3006 + source = "registry+https://github.com/rust-lang/crates.io-index" 3007 + checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" 3008 + dependencies = [ 3009 + "quote", 3010 + "syn 2.0.104", 3011 + ] 3012 + 3013 + [[package]] 3014 + name = "qoi" 3015 + version = "0.4.1" 3016 + source = "registry+https://github.com/rust-lang/crates.io-index" 3017 + checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 3018 + dependencies = [ 3019 + "bytemuck", 3020 + ] 3021 + 3022 + [[package]] 3023 + name = "quick-error" 3024 + version = "2.0.1" 3025 + source = "registry+https://github.com/rust-lang/crates.io-index" 3026 + checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 3027 + 3028 + [[package]] 3029 + name = "quick-xml" 3030 + version = "0.37.5" 3031 + source = "registry+https://github.com/rust-lang/crates.io-index" 3032 + checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 3033 + dependencies = [ 3034 + "memchr", 3035 + ] 3036 + 3037 + [[package]] 3038 + name = "quinn" 3039 + version = "0.11.8" 3040 + source = "registry+https://github.com/rust-lang/crates.io-index" 3041 + checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 3042 + dependencies = [ 3043 + "bytes", 3044 + "cfg_aliases", 3045 + "pin-project-lite", 3046 + "quinn-proto", 3047 + "quinn-udp", 3048 + "rustc-hash", 3049 + "rustls", 3050 + "socket2", 3051 + "thiserror 2.0.12", 3052 + "tokio", 3053 + "tracing", 3054 + "web-time", 3055 + ] 3056 + 3057 + [[package]] 3058 + name = "quinn-proto" 3059 + version = "0.11.12" 3060 + source = "registry+https://github.com/rust-lang/crates.io-index" 3061 + checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 3062 + dependencies = [ 3063 + "bytes", 3064 + "getrandom 0.3.3", 3065 + "lru-slab", 3066 + "rand 0.9.1", 3067 + "ring", 3068 + "rustc-hash", 3069 + "rustls", 3070 + "rustls-pki-types", 3071 + "slab", 3072 + "thiserror 2.0.12", 3073 + "tinyvec", 3074 + "tracing", 3075 + "web-time", 3076 + ] 3077 + 3078 + [[package]] 3079 + name = "quinn-udp" 3080 + version = "0.5.13" 3081 + source = "registry+https://github.com/rust-lang/crates.io-index" 3082 + checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" 3083 + dependencies = [ 3084 + "cfg_aliases", 3085 + "libc", 3086 + "once_cell", 3087 + "socket2", 3088 + "tracing", 3089 + "windows-sys 0.59.0", 3090 + ] 3091 + 3092 + [[package]] 3093 + name = "quote" 3094 + version = "1.0.40" 3095 + source = "registry+https://github.com/rust-lang/crates.io-index" 3096 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 3097 + dependencies = [ 3098 + "proc-macro2", 3099 + ] 3100 + 3101 + [[package]] 3102 + name = "r-efi" 3103 + version = "5.3.0" 3104 + source = "registry+https://github.com/rust-lang/crates.io-index" 3105 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 3106 + 3107 + [[package]] 3108 + name = "rand" 3109 + version = "0.8.5" 3110 + source = "registry+https://github.com/rust-lang/crates.io-index" 3111 + checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 3112 + dependencies = [ 3113 + "libc", 3114 + "rand_chacha 0.3.1", 3115 + "rand_core 0.6.4", 3116 + ] 3117 + 3118 + [[package]] 3119 + name = "rand" 3120 + version = "0.9.1" 3121 + source = "registry+https://github.com/rust-lang/crates.io-index" 3122 + checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 3123 + dependencies = [ 3124 + "rand_chacha 0.9.0", 3125 + "rand_core 0.9.3", 3126 + ] 3127 + 3128 + [[package]] 3129 + name = "rand_chacha" 3130 + version = "0.3.1" 3131 + source = "registry+https://github.com/rust-lang/crates.io-index" 3132 + checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 3133 + dependencies = [ 3134 + "ppv-lite86", 3135 + "rand_core 0.6.4", 3136 + ] 3137 + 3138 + [[package]] 3139 + name = "rand_chacha" 3140 + version = "0.9.0" 3141 + source = "registry+https://github.com/rust-lang/crates.io-index" 3142 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 3143 + dependencies = [ 3144 + "ppv-lite86", 3145 + "rand_core 0.9.3", 3146 + ] 3147 + 3148 + [[package]] 3149 + name = "rand_core" 3150 + version = "0.6.4" 3151 + source = "registry+https://github.com/rust-lang/crates.io-index" 3152 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 3153 + dependencies = [ 3154 + "getrandom 0.2.16", 3155 + ] 3156 + 3157 + [[package]] 3158 + name = "rand_core" 3159 + version = "0.9.3" 3160 + source = "registry+https://github.com/rust-lang/crates.io-index" 3161 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 3162 + dependencies = [ 3163 + "getrandom 0.3.3", 3164 + ] 3165 + 3166 + [[package]] 3167 + name = "rav1e" 3168 + version = "0.7.1" 3169 + source = "registry+https://github.com/rust-lang/crates.io-index" 3170 + checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" 3171 + dependencies = [ 3172 + "arbitrary", 3173 + "arg_enum_proc_macro", 3174 + "arrayvec", 3175 + "av1-grain", 3176 + "bitstream-io", 3177 + "built", 3178 + "cfg-if", 3179 + "interpolate_name", 3180 + "itertools", 3181 + "libc", 3182 + "libfuzzer-sys", 3183 + "log", 3184 + "maybe-rayon", 3185 + "new_debug_unreachable", 3186 + "noop_proc_macro", 3187 + "num-derive", 3188 + "num-traits", 3189 + "once_cell", 3190 + "paste", 3191 + "profiling", 3192 + "rand 0.8.5", 3193 + "rand_chacha 0.3.1", 3194 + "simd_helpers", 3195 + "system-deps", 3196 + "thiserror 1.0.69", 3197 + "v_frame", 3198 + "wasm-bindgen", 3199 + ] 3200 + 3201 + [[package]] 3202 + name = "ravif" 3203 + version = "0.11.20" 3204 + source = "registry+https://github.com/rust-lang/crates.io-index" 3205 + checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" 3206 + dependencies = [ 3207 + "avif-serialize", 3208 + "imgref", 3209 + "loop9", 3210 + "quick-error", 3211 + "rav1e", 3212 + "rayon", 3213 + "rgb", 3214 + ] 3215 + 3216 + [[package]] 3217 + name = "rayon" 3218 + version = "1.10.0" 3219 + source = "registry+https://github.com/rust-lang/crates.io-index" 3220 + checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 3221 + dependencies = [ 3222 + "either", 3223 + "rayon-core", 3224 + ] 3225 + 3226 + [[package]] 3227 + name = "rayon-core" 3228 + version = "1.12.1" 3229 + source = "registry+https://github.com/rust-lang/crates.io-index" 3230 + checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 3231 + dependencies = [ 3232 + "crossbeam-deque", 3233 + "crossbeam-utils", 3234 + ] 3235 + 3236 + [[package]] 3237 + name = "redox_syscall" 3238 + version = "0.5.13" 3239 + source = "registry+https://github.com/rust-lang/crates.io-index" 3240 + checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 3241 + dependencies = [ 3242 + "bitflags 2.9.1", 3243 + ] 3244 + 3245 + [[package]] 3246 + name = "regex" 3247 + version = "1.11.1" 3248 + source = "registry+https://github.com/rust-lang/crates.io-index" 3249 + checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 3250 + dependencies = [ 3251 + "aho-corasick", 3252 + "memchr", 3253 + "regex-automata 0.4.9", 3254 + "regex-syntax 0.8.5", 3255 + ] 3256 + 3257 + [[package]] 3258 + name = "regex-automata" 3259 + version = "0.1.10" 3260 + source = "registry+https://github.com/rust-lang/crates.io-index" 3261 + checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 3262 + dependencies = [ 3263 + "regex-syntax 0.6.29", 3264 + ] 3265 + 3266 + [[package]] 3267 + name = "regex-automata" 3268 + version = "0.4.9" 3269 + source = "registry+https://github.com/rust-lang/crates.io-index" 3270 + checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 3271 + dependencies = [ 3272 + "aho-corasick", 3273 + "memchr", 3274 + "regex-syntax 0.8.5", 3275 + ] 3276 + 3277 + [[package]] 3278 + name = "regex-syntax" 3279 + version = "0.6.29" 3280 + source = "registry+https://github.com/rust-lang/crates.io-index" 3281 + checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 3282 + 3283 + [[package]] 3284 + name = "regex-syntax" 3285 + version = "0.8.5" 3286 + source = "registry+https://github.com/rust-lang/crates.io-index" 3287 + checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 3288 + 3289 + [[package]] 3290 + name = "reqwest" 3291 + version = "0.12.22" 3292 + source = "registry+https://github.com/rust-lang/crates.io-index" 3293 + checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" 3294 + dependencies = [ 3295 + "base64", 3296 + "bytes", 3297 + "encoding_rs", 3298 + "futures-core", 3299 + "futures-util", 3300 + "h2", 3301 + "http", 3302 + "http-body", 3303 + "http-body-util", 3304 + "hyper", 3305 + "hyper-rustls", 3306 + "hyper-tls", 3307 + "hyper-util", 3308 + "js-sys", 3309 + "log", 3310 + "mime", 3311 + "mime_guess", 3312 + "native-tls", 3313 + "percent-encoding", 3314 + "pin-project-lite", 3315 + "quinn", 3316 + "rustls", 3317 + "rustls-pki-types", 3318 + "serde", 3319 + "serde_json", 3320 + "serde_urlencoded", 3321 + "sync_wrapper", 3322 + "tokio", 3323 + "tokio-native-tls", 3324 + "tokio-rustls", 3325 + "tokio-util", 3326 + "tower", 3327 + "tower-http 0.6.6", 3328 + "tower-service", 3329 + "url", 3330 + "wasm-bindgen", 3331 + "wasm-bindgen-futures", 3332 + "wasm-streams", 3333 + "web-sys", 3334 + "webpki-roots 1.0.1", 3335 + ] 3336 + 3337 + [[package]] 3338 + name = "reqwest-chain" 3339 + version = "1.0.0" 3340 + source = "registry+https://github.com/rust-lang/crates.io-index" 3341 + checksum = "da5c014fb79a8227db44a0433d748107750d2550b7fca55c59a3d7ee7d2ee2b2" 3342 + dependencies = [ 3343 + "anyhow", 3344 + "async-trait", 3345 + "http", 3346 + "reqwest-middleware", 3347 + ] 3348 + 3349 + [[package]] 3350 + name = "reqwest-middleware" 3351 + version = "0.4.2" 3352 + source = "registry+https://github.com/rust-lang/crates.io-index" 3353 + checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" 3354 + dependencies = [ 3355 + "anyhow", 3356 + "async-trait", 3357 + "http", 3358 + "reqwest", 3359 + "serde", 3360 + "thiserror 1.0.69", 3361 + "tower-service", 3362 + ] 3363 + 3364 + [[package]] 3365 + name = "resolv-conf" 3366 + version = "0.7.4" 3367 + source = "registry+https://github.com/rust-lang/crates.io-index" 3368 + checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" 3369 + 3370 + [[package]] 3371 + name = "rfc6979" 3372 + version = "0.4.0" 3373 + source = "registry+https://github.com/rust-lang/crates.io-index" 3374 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 3375 + dependencies = [ 3376 + "hmac", 3377 + "subtle", 3378 + ] 3379 + 3380 + [[package]] 3381 + name = "rgb" 3382 + version = "0.8.51" 3383 + source = "registry+https://github.com/rust-lang/crates.io-index" 3384 + checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f" 3385 + 3386 + [[package]] 3387 + name = "ring" 3388 + version = "0.17.14" 3389 + source = "registry+https://github.com/rust-lang/crates.io-index" 3390 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 3391 + dependencies = [ 3392 + "cc", 3393 + "cfg-if", 3394 + "getrandom 0.2.16", 3395 + "libc", 3396 + "untrusted", 3397 + "windows-sys 0.52.0", 3398 + ] 3399 + 3400 + [[package]] 3401 + name = "rsa" 3402 + version = "0.9.8" 3403 + source = "registry+https://github.com/rust-lang/crates.io-index" 3404 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 3405 + dependencies = [ 3406 + "const-oid", 3407 + "digest", 3408 + "num-bigint-dig", 3409 + "num-integer", 3410 + "num-traits", 3411 + "pkcs1", 3412 + "pkcs8", 3413 + "rand_core 0.6.4", 3414 + "signature", 3415 + "spki", 3416 + "subtle", 3417 + "zeroize", 3418 + ] 3419 + 3420 + [[package]] 3421 + name = "rust-embed" 3422 + version = "8.7.2" 3423 + source = "registry+https://github.com/rust-lang/crates.io-index" 3424 + checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" 3425 + dependencies = [ 3426 + "rust-embed-impl", 3427 + "rust-embed-utils", 3428 + "walkdir", 3429 + ] 3430 + 3431 + [[package]] 3432 + name = "rust-embed-impl" 3433 + version = "8.7.2" 3434 + source = "registry+https://github.com/rust-lang/crates.io-index" 3435 + checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" 3436 + dependencies = [ 3437 + "proc-macro2", 3438 + "quote", 3439 + "rust-embed-utils", 3440 + "syn 2.0.104", 3441 + "walkdir", 3442 + ] 3443 + 3444 + [[package]] 3445 + name = "rust-embed-utils" 3446 + version = "8.7.2" 3447 + source = "registry+https://github.com/rust-lang/crates.io-index" 3448 + checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" 3449 + dependencies = [ 3450 + "sha2", 3451 + "walkdir", 3452 + ] 3453 + 3454 + [[package]] 3455 + name = "rust_decimal" 3456 + version = "1.37.2" 3457 + source = "registry+https://github.com/rust-lang/crates.io-index" 3458 + checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" 3459 + dependencies = [ 3460 + "arrayvec", 3461 + "num-traits", 3462 + ] 3463 + 3464 + [[package]] 3465 + name = "rustc-demangle" 3466 + version = "0.1.25" 3467 + source = "registry+https://github.com/rust-lang/crates.io-index" 3468 + checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 3469 + 3470 + [[package]] 3471 + name = "rustc-hash" 3472 + version = "2.1.1" 3473 + source = "registry+https://github.com/rust-lang/crates.io-index" 3474 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 3475 + 3476 + [[package]] 3477 + name = "rustc_version" 3478 + version = "0.4.1" 3479 + source = "registry+https://github.com/rust-lang/crates.io-index" 3480 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 3481 + dependencies = [ 3482 + "semver", 3483 + ] 3484 + 3485 + [[package]] 3486 + name = "rustix" 3487 + version = "1.0.7" 3488 + source = "registry+https://github.com/rust-lang/crates.io-index" 3489 + checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 3490 + dependencies = [ 3491 + "bitflags 2.9.1", 3492 + "errno", 3493 + "libc", 3494 + "linux-raw-sys", 3495 + "windows-sys 0.59.0", 3496 + ] 3497 + 3498 + [[package]] 3499 + name = "rustls" 3500 + version = "0.23.28" 3501 + source = "registry+https://github.com/rust-lang/crates.io-index" 3502 + checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" 3503 + dependencies = [ 3504 + "once_cell", 3505 + "ring", 3506 + "rustls-pki-types", 3507 + "rustls-webpki", 3508 + "subtle", 3509 + "zeroize", 3510 + ] 3511 + 3512 + [[package]] 3513 + name = "rustls-native-certs" 3514 + version = "0.8.1" 3515 + source = "registry+https://github.com/rust-lang/crates.io-index" 3516 + checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 3517 + dependencies = [ 3518 + "openssl-probe", 3519 + "rustls-pki-types", 3520 + "schannel", 3521 + "security-framework 3.2.0", 3522 + ] 3523 + 3524 + [[package]] 3525 + name = "rustls-pki-types" 3526 + version = "1.12.0" 3527 + source = "registry+https://github.com/rust-lang/crates.io-index" 3528 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 3529 + dependencies = [ 3530 + "web-time", 3531 + "zeroize", 3532 + ] 3533 + 3534 + [[package]] 3535 + name = "rustls-webpki" 3536 + version = "0.103.3" 3537 + source = "registry+https://github.com/rust-lang/crates.io-index" 3538 + checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 3539 + dependencies = [ 3540 + "ring", 3541 + "rustls-pki-types", 3542 + "untrusted", 3543 + ] 3544 + 3545 + [[package]] 3546 + name = "rustversion" 3547 + version = "1.0.21" 3548 + source = "registry+https://github.com/rust-lang/crates.io-index" 3549 + checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 3550 + 3551 + [[package]] 3552 + name = "ryu" 3553 + version = "1.0.20" 3554 + source = "registry+https://github.com/rust-lang/crates.io-index" 3555 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 3556 + 3557 + [[package]] 3558 + name = "same-file" 3559 + version = "1.0.6" 3560 + source = "registry+https://github.com/rust-lang/crates.io-index" 3561 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 3562 + dependencies = [ 3563 + "winapi-util", 3564 + ] 3565 + 3566 + [[package]] 3567 + name = "schannel" 3568 + version = "0.1.27" 3569 + source = "registry+https://github.com/rust-lang/crates.io-index" 3570 + checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 3571 + dependencies = [ 3572 + "windows-sys 0.59.0", 3573 + ] 3574 + 3575 + [[package]] 3576 + name = "scoped-tls" 3577 + version = "1.0.1" 3578 + source = "registry+https://github.com/rust-lang/crates.io-index" 3579 + checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 3580 + 3581 + [[package]] 3582 + name = "scopeguard" 3583 + version = "1.2.0" 3584 + source = "registry+https://github.com/rust-lang/crates.io-index" 3585 + checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 3586 + 3587 + [[package]] 3588 + name = "sec1" 3589 + version = "0.7.3" 3590 + source = "registry+https://github.com/rust-lang/crates.io-index" 3591 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 3592 + dependencies = [ 3593 + "base16ct", 3594 + "der", 3595 + "generic-array", 3596 + "pkcs8", 3597 + "serdect", 3598 + "subtle", 3599 + "zeroize", 3600 + ] 3601 + 3602 + [[package]] 3603 + name = "security-framework" 3604 + version = "2.11.1" 3605 + source = "registry+https://github.com/rust-lang/crates.io-index" 3606 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3607 + dependencies = [ 3608 + "bitflags 2.9.1", 3609 + "core-foundation 0.9.4", 3610 + "core-foundation-sys", 3611 + "libc", 3612 + "security-framework-sys", 3613 + ] 3614 + 3615 + [[package]] 3616 + name = "security-framework" 3617 + version = "3.2.0" 3618 + source = "registry+https://github.com/rust-lang/crates.io-index" 3619 + checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 3620 + dependencies = [ 3621 + "bitflags 2.9.1", 3622 + "core-foundation 0.10.1", 3623 + "core-foundation-sys", 3624 + "libc", 3625 + "security-framework-sys", 3626 + ] 3627 + 3628 + [[package]] 3629 + name = "security-framework-sys" 3630 + version = "2.14.0" 3631 + source = "registry+https://github.com/rust-lang/crates.io-index" 3632 + checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 3633 + dependencies = [ 3634 + "core-foundation-sys", 3635 + "libc", 3636 + ] 3637 + 3638 + [[package]] 3639 + name = "self_cell" 3640 + version = "1.2.0" 3641 + source = "registry+https://github.com/rust-lang/crates.io-index" 3642 + checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" 3643 + 3644 + [[package]] 3645 + name = "semver" 3646 + version = "1.0.26" 3647 + source = "registry+https://github.com/rust-lang/crates.io-index" 3648 + checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 3649 + 3650 + [[package]] 3651 + name = "serde" 3652 + version = "1.0.219" 3653 + source = "registry+https://github.com/rust-lang/crates.io-index" 3654 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 3655 + dependencies = [ 3656 + "serde_derive", 3657 + ] 3658 + 3659 + [[package]] 3660 + name = "serde_bytes" 3661 + version = "0.11.17" 3662 + source = "registry+https://github.com/rust-lang/crates.io-index" 3663 + checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" 3664 + dependencies = [ 3665 + "serde", 3666 + ] 3667 + 3668 + [[package]] 3669 + name = "serde_derive" 3670 + version = "1.0.219" 3671 + source = "registry+https://github.com/rust-lang/crates.io-index" 3672 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 3673 + dependencies = [ 3674 + "proc-macro2", 3675 + "quote", 3676 + "syn 2.0.104", 3677 + ] 3678 + 3679 + [[package]] 3680 + name = "serde_ipld_dagcbor" 3681 + version = "0.6.3" 3682 + source = "registry+https://github.com/rust-lang/crates.io-index" 3683 + checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc" 3684 + dependencies = [ 3685 + "cbor4ii", 3686 + "ipld-core", 3687 + "scopeguard", 3688 + "serde", 3689 + ] 3690 + 3691 + [[package]] 3692 + name = "serde_json" 3693 + version = "1.0.140" 3694 + source = "registry+https://github.com/rust-lang/crates.io-index" 3695 + checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 3696 + dependencies = [ 3697 + "itoa", 3698 + "memchr", 3699 + "ryu", 3700 + "serde", 3701 + ] 3702 + 3703 + [[package]] 3704 + name = "serde_path_to_error" 3705 + version = "0.1.17" 3706 + source = "registry+https://github.com/rust-lang/crates.io-index" 3707 + checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 3708 + dependencies = [ 3709 + "itoa", 3710 + "serde", 3711 + ] 3712 + 3713 + [[package]] 3714 + name = "serde_spanned" 3715 + version = "0.6.9" 3716 + source = "registry+https://github.com/rust-lang/crates.io-index" 3717 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 3718 + dependencies = [ 3719 + "serde", 3720 + ] 3721 + 3722 + [[package]] 3723 + name = "serde_urlencoded" 3724 + version = "0.7.1" 3725 + source = "registry+https://github.com/rust-lang/crates.io-index" 3726 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 3727 + dependencies = [ 3728 + "form_urlencoded", 3729 + "itoa", 3730 + "ryu", 3731 + "serde", 3732 + ] 3733 + 3734 + [[package]] 3735 + name = "serdect" 3736 + version = "0.2.0" 3737 + source = "registry+https://github.com/rust-lang/crates.io-index" 3738 + checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" 3739 + dependencies = [ 3740 + "base16ct", 3741 + "serde", 3742 + ] 3743 + 3744 + [[package]] 3745 + name = "sha1" 3746 + version = "0.10.6" 3747 + source = "registry+https://github.com/rust-lang/crates.io-index" 3748 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 3749 + dependencies = [ 3750 + "cfg-if", 3751 + "cpufeatures", 3752 + "digest", 3753 + ] 3754 + 3755 + [[package]] 3756 + name = "sha2" 3757 + version = "0.10.9" 3758 + source = "registry+https://github.com/rust-lang/crates.io-index" 3759 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 3760 + dependencies = [ 3761 + "cfg-if", 3762 + "cpufeatures", 3763 + "digest", 3764 + ] 3765 + 3766 + [[package]] 3767 + name = "sharded-slab" 3768 + version = "0.1.7" 3769 + source = "registry+https://github.com/rust-lang/crates.io-index" 3770 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 3771 + dependencies = [ 3772 + "lazy_static", 3773 + ] 3774 + 3775 + [[package]] 3776 + name = "shell-words" 3777 + version = "1.1.0" 3778 + source = "registry+https://github.com/rust-lang/crates.io-index" 3779 + checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 3780 + 3781 + [[package]] 3782 + name = "shlex" 3783 + version = "1.3.0" 3784 + source = "registry+https://github.com/rust-lang/crates.io-index" 3785 + checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 3786 + 3787 + [[package]] 3788 + name = "signal-hook-registry" 3789 + version = "1.4.5" 3790 + source = "registry+https://github.com/rust-lang/crates.io-index" 3791 + checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 3792 + dependencies = [ 3793 + "libc", 3794 + ] 3795 + 3796 + [[package]] 3797 + name = "signature" 3798 + version = "2.2.0" 3799 + source = "registry+https://github.com/rust-lang/crates.io-index" 3800 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3801 + dependencies = [ 3802 + "digest", 3803 + "rand_core 0.6.4", 3804 + ] 3805 + 3806 + [[package]] 3807 + name = "simd-adler32" 3808 + version = "0.3.7" 3809 + source = "registry+https://github.com/rust-lang/crates.io-index" 3810 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 3811 + 3812 + [[package]] 3813 + name = "simd_helpers" 3814 + version = "0.1.0" 3815 + source = "registry+https://github.com/rust-lang/crates.io-index" 3816 + checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" 3817 + dependencies = [ 3818 + "quote", 3819 + ] 3820 + 3821 + [[package]] 3822 + name = "simdutf8" 3823 + version = "0.1.5" 3824 + source = "registry+https://github.com/rust-lang/crates.io-index" 3825 + checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 3826 + 3827 + [[package]] 3828 + name = "siphasher" 3829 + version = "1.0.1" 3830 + source = "registry+https://github.com/rust-lang/crates.io-index" 3831 + checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 3832 + 3833 + [[package]] 3834 + name = "slab" 3835 + version = "0.4.10" 3836 + source = "registry+https://github.com/rust-lang/crates.io-index" 3837 + checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 3838 + 3839 + [[package]] 3840 + name = "slug" 3841 + version = "0.1.6" 3842 + source = "registry+https://github.com/rust-lang/crates.io-index" 3843 + checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" 3844 + dependencies = [ 3845 + "deunicode", 3846 + "wasm-bindgen", 3847 + ] 3848 + 3849 + [[package]] 3850 + name = "slugify" 3851 + version = "0.1.0" 3852 + source = "registry+https://github.com/rust-lang/crates.io-index" 3853 + checksum = "d6b8cf203d2088b831d7558f8e5151bfa420c57a34240b28cee29d0ae5f2ac8b" 3854 + dependencies = [ 3855 + "unidecode", 3856 + ] 3857 + 3858 + [[package]] 3859 + name = "smallvec" 3860 + version = "1.15.1" 3861 + source = "registry+https://github.com/rust-lang/crates.io-index" 3862 + checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 3863 + dependencies = [ 3864 + "serde", 3865 + ] 3866 + 3867 + [[package]] 3868 + name = "socket2" 3869 + version = "0.5.10" 3870 + source = "registry+https://github.com/rust-lang/crates.io-index" 3871 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 3872 + dependencies = [ 3873 + "libc", 3874 + "windows-sys 0.52.0", 3875 + ] 3876 + 3877 + [[package]] 3878 + name = "spin" 3879 + version = "0.9.8" 3880 + source = "registry+https://github.com/rust-lang/crates.io-index" 3881 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3882 + dependencies = [ 3883 + "lock_api", 3884 + ] 3885 + 3886 + [[package]] 3887 + name = "spki" 3888 + version = "0.7.3" 3889 + source = "registry+https://github.com/rust-lang/crates.io-index" 3890 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 3891 + dependencies = [ 3892 + "base64ct", 3893 + "der", 3894 + ] 3895 + 3896 + [[package]] 3897 + name = "sqlx" 3898 + version = "0.8.6" 3899 + source = "registry+https://github.com/rust-lang/crates.io-index" 3900 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 3901 + dependencies = [ 3902 + "sqlx-core", 3903 + "sqlx-macros", 3904 + "sqlx-mysql", 3905 + "sqlx-postgres", 3906 + "sqlx-sqlite", 3907 + ] 3908 + 3909 + [[package]] 3910 + name = "sqlx-core" 3911 + version = "0.8.6" 3912 + source = "registry+https://github.com/rust-lang/crates.io-index" 3913 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 3914 + dependencies = [ 3915 + "base64", 3916 + "bytes", 3917 + "chrono", 3918 + "crc", 3919 + "crossbeam-queue", 3920 + "either", 3921 + "event-listener", 3922 + "futures-core", 3923 + "futures-intrusive", 3924 + "futures-io", 3925 + "futures-util", 3926 + "hashbrown 0.15.4", 3927 + "hashlink", 3928 + "indexmap", 3929 + "log", 3930 + "memchr", 3931 + "once_cell", 3932 + "percent-encoding", 3933 + "rustls", 3934 + "serde", 3935 + "serde_json", 3936 + "sha2", 3937 + "smallvec", 3938 + "thiserror 2.0.12", 3939 + "tokio", 3940 + "tokio-stream", 3941 + "tracing", 3942 + "url", 3943 + "uuid", 3944 + "webpki-roots 0.26.11", 3945 + ] 3946 + 3947 + [[package]] 3948 + name = "sqlx-macros" 3949 + version = "0.8.6" 3950 + source = "registry+https://github.com/rust-lang/crates.io-index" 3951 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 3952 + dependencies = [ 3953 + "proc-macro2", 3954 + "quote", 3955 + "sqlx-core", 3956 + "sqlx-macros-core", 3957 + "syn 2.0.104", 3958 + ] 3959 + 3960 + [[package]] 3961 + name = "sqlx-macros-core" 3962 + version = "0.8.6" 3963 + source = "registry+https://github.com/rust-lang/crates.io-index" 3964 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 3965 + dependencies = [ 3966 + "dotenvy", 3967 + "either", 3968 + "heck", 3969 + "hex", 3970 + "once_cell", 3971 + "proc-macro2", 3972 + "quote", 3973 + "serde", 3974 + "serde_json", 3975 + "sha2", 3976 + "sqlx-core", 3977 + "sqlx-mysql", 3978 + "sqlx-postgres", 3979 + "sqlx-sqlite", 3980 + "syn 2.0.104", 3981 + "tokio", 3982 + "url", 3983 + ] 3984 + 3985 + [[package]] 3986 + name = "sqlx-mysql" 3987 + version = "0.8.6" 3988 + source = "registry+https://github.com/rust-lang/crates.io-index" 3989 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 3990 + dependencies = [ 3991 + "atoi", 3992 + "base64", 3993 + "bitflags 2.9.1", 3994 + "byteorder", 3995 + "bytes", 3996 + "chrono", 3997 + "crc", 3998 + "digest", 3999 + "dotenvy", 4000 + "either", 4001 + "futures-channel", 4002 + "futures-core", 4003 + "futures-io", 4004 + "futures-util", 4005 + "generic-array", 4006 + "hex", 4007 + "hkdf", 4008 + "hmac", 4009 + "itoa", 4010 + "log", 4011 + "md-5", 4012 + "memchr", 4013 + "once_cell", 4014 + "percent-encoding", 4015 + "rand 0.8.5", 4016 + "rsa", 4017 + "serde", 4018 + "sha1", 4019 + "sha2", 4020 + "smallvec", 4021 + "sqlx-core", 4022 + "stringprep", 4023 + "thiserror 2.0.12", 4024 + "tracing", 4025 + "uuid", 4026 + "whoami", 4027 + ] 4028 + 4029 + [[package]] 4030 + name = "sqlx-postgres" 4031 + version = "0.8.6" 4032 + source = "registry+https://github.com/rust-lang/crates.io-index" 4033 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 4034 + dependencies = [ 4035 + "atoi", 4036 + "base64", 4037 + "bitflags 2.9.1", 4038 + "byteorder", 4039 + "chrono", 4040 + "crc", 4041 + "dotenvy", 4042 + "etcetera", 4043 + "futures-channel", 4044 + "futures-core", 4045 + "futures-util", 4046 + "hex", 4047 + "hkdf", 4048 + "hmac", 4049 + "home", 4050 + "itoa", 4051 + "log", 4052 + "md-5", 4053 + "memchr", 4054 + "once_cell", 4055 + "rand 0.8.5", 4056 + "serde", 4057 + "serde_json", 4058 + "sha2", 4059 + "smallvec", 4060 + "sqlx-core", 4061 + "stringprep", 4062 + "thiserror 2.0.12", 4063 + "tracing", 4064 + "uuid", 4065 + "whoami", 4066 + ] 4067 + 4068 + [[package]] 4069 + name = "sqlx-sqlite" 4070 + version = "0.8.6" 4071 + source = "registry+https://github.com/rust-lang/crates.io-index" 4072 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 4073 + dependencies = [ 4074 + "atoi", 4075 + "chrono", 4076 + "flume", 4077 + "futures-channel", 4078 + "futures-core", 4079 + "futures-executor", 4080 + "futures-intrusive", 4081 + "futures-util", 4082 + "libsqlite3-sys", 4083 + "log", 4084 + "percent-encoding", 4085 + "serde", 4086 + "serde_urlencoded", 4087 + "sqlx-core", 4088 + "thiserror 2.0.12", 4089 + "tracing", 4090 + "url", 4091 + "uuid", 4092 + ] 4093 + 4094 + [[package]] 4095 + name = "stable_deref_trait" 4096 + version = "1.2.0" 4097 + source = "registry+https://github.com/rust-lang/crates.io-index" 4098 + checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 4099 + 4100 + [[package]] 4101 + name = "stringprep" 4102 + version = "0.1.5" 4103 + source = "registry+https://github.com/rust-lang/crates.io-index" 4104 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 4105 + dependencies = [ 4106 + "unicode-bidi", 4107 + "unicode-normalization", 4108 + "unicode-properties", 4109 + ] 4110 + 4111 + [[package]] 4112 + name = "strsim" 4113 + version = "0.11.1" 4114 + source = "registry+https://github.com/rust-lang/crates.io-index" 4115 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 4116 + 4117 + [[package]] 4118 + name = "subtle" 4119 + version = "2.6.1" 4120 + source = "registry+https://github.com/rust-lang/crates.io-index" 4121 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 4122 + 4123 + [[package]] 4124 + name = "syn" 4125 + version = "1.0.109" 4126 + source = "registry+https://github.com/rust-lang/crates.io-index" 4127 + checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 4128 + dependencies = [ 4129 + "proc-macro2", 4130 + "quote", 4131 + "unicode-ident", 4132 + ] 4133 + 4134 + [[package]] 4135 + name = "syn" 4136 + version = "2.0.104" 4137 + source = "registry+https://github.com/rust-lang/crates.io-index" 4138 + checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 4139 + dependencies = [ 4140 + "proc-macro2", 4141 + "quote", 4142 + "unicode-ident", 4143 + ] 4144 + 4145 + [[package]] 4146 + name = "sync_wrapper" 4147 + version = "1.0.2" 4148 + source = "registry+https://github.com/rust-lang/crates.io-index" 4149 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 4150 + dependencies = [ 4151 + "futures-core", 4152 + ] 4153 + 4154 + [[package]] 4155 + name = "synstructure" 4156 + version = "0.13.2" 4157 + source = "registry+https://github.com/rust-lang/crates.io-index" 4158 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 4159 + dependencies = [ 4160 + "proc-macro2", 4161 + "quote", 4162 + "syn 2.0.104", 4163 + ] 4164 + 4165 + [[package]] 4166 + name = "syntect" 4167 + version = "5.2.0" 4168 + source = "registry+https://github.com/rust-lang/crates.io-index" 4169 + checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" 4170 + dependencies = [ 4171 + "bincode", 4172 + "bitflags 1.3.2", 4173 + "fancy-regex", 4174 + "flate2", 4175 + "fnv", 4176 + "once_cell", 4177 + "onig", 4178 + "plist", 4179 + "regex-syntax 0.8.5", 4180 + "serde", 4181 + "serde_derive", 4182 + "serde_json", 4183 + "thiserror 1.0.69", 4184 + "walkdir", 4185 + "yaml-rust", 4186 + ] 4187 + 4188 + [[package]] 4189 + name = "system-configuration" 4190 + version = "0.6.1" 4191 + source = "registry+https://github.com/rust-lang/crates.io-index" 4192 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 4193 + dependencies = [ 4194 + "bitflags 2.9.1", 4195 + "core-foundation 0.9.4", 4196 + "system-configuration-sys", 4197 + ] 4198 + 4199 + [[package]] 4200 + name = "system-configuration-sys" 4201 + version = "0.6.0" 4202 + source = "registry+https://github.com/rust-lang/crates.io-index" 4203 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 4204 + dependencies = [ 4205 + "core-foundation-sys", 4206 + "libc", 4207 + ] 4208 + 4209 + [[package]] 4210 + name = "system-deps" 4211 + version = "6.2.2" 4212 + source = "registry+https://github.com/rust-lang/crates.io-index" 4213 + checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" 4214 + dependencies = [ 4215 + "cfg-expr", 4216 + "heck", 4217 + "pkg-config", 4218 + "toml", 4219 + "version-compare", 4220 + ] 4221 + 4222 + [[package]] 4223 + name = "tagptr" 4224 + version = "0.2.0" 4225 + source = "registry+https://github.com/rust-lang/crates.io-index" 4226 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 4227 + 4228 + [[package]] 4229 + name = "target-lexicon" 4230 + version = "0.12.16" 4231 + source = "registry+https://github.com/rust-lang/crates.io-index" 4232 + checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 4233 + 4234 + [[package]] 4235 + name = "tempfile" 4236 + version = "3.20.0" 4237 + source = "registry+https://github.com/rust-lang/crates.io-index" 4238 + checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 4239 + dependencies = [ 4240 + "fastrand", 4241 + "getrandom 0.3.3", 4242 + "once_cell", 4243 + "rustix", 4244 + "windows-sys 0.59.0", 4245 + ] 4246 + 4247 + [[package]] 4248 + name = "terminal_size" 4249 + version = "0.4.2" 4250 + source = "registry+https://github.com/rust-lang/crates.io-index" 4251 + checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 4252 + dependencies = [ 4253 + "rustix", 4254 + "windows-sys 0.59.0", 4255 + ] 4256 + 4257 + [[package]] 4258 + name = "thiserror" 4259 + version = "1.0.69" 4260 + source = "registry+https://github.com/rust-lang/crates.io-index" 4261 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 4262 + dependencies = [ 4263 + "thiserror-impl 1.0.69", 4264 + ] 4265 + 4266 + [[package]] 4267 + name = "thiserror" 4268 + version = "2.0.12" 4269 + source = "registry+https://github.com/rust-lang/crates.io-index" 4270 + checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 4271 + dependencies = [ 4272 + "thiserror-impl 2.0.12", 4273 + ] 4274 + 4275 + [[package]] 4276 + name = "thiserror-impl" 4277 + version = "1.0.69" 4278 + source = "registry+https://github.com/rust-lang/crates.io-index" 4279 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 4280 + dependencies = [ 4281 + "proc-macro2", 4282 + "quote", 4283 + "syn 2.0.104", 4284 + ] 4285 + 4286 + [[package]] 4287 + name = "thiserror-impl" 4288 + version = "2.0.12" 4289 + source = "registry+https://github.com/rust-lang/crates.io-index" 4290 + checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 4291 + dependencies = [ 4292 + "proc-macro2", 4293 + "quote", 4294 + "syn 2.0.104", 4295 + ] 4296 + 4297 + [[package]] 4298 + name = "thread_local" 4299 + version = "1.1.9" 4300 + source = "registry+https://github.com/rust-lang/crates.io-index" 4301 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 4302 + dependencies = [ 4303 + "cfg-if", 4304 + ] 4305 + 4306 + [[package]] 4307 + name = "tiff" 4308 + version = "0.9.1" 4309 + source = "registry+https://github.com/rust-lang/crates.io-index" 4310 + checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 4311 + dependencies = [ 4312 + "flate2", 4313 + "jpeg-decoder", 4314 + "weezl", 4315 + ] 4316 + 4317 + [[package]] 4318 + name = "time" 4319 + version = "0.3.41" 4320 + source = "registry+https://github.com/rust-lang/crates.io-index" 4321 + checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 4322 + dependencies = [ 4323 + "deranged", 4324 + "itoa", 4325 + "num-conv", 4326 + "powerfmt", 4327 + "serde", 4328 + "time-core", 4329 + "time-macros", 4330 + ] 4331 + 4332 + [[package]] 4333 + name = "time-core" 4334 + version = "0.1.4" 4335 + source = "registry+https://github.com/rust-lang/crates.io-index" 4336 + checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 4337 + 4338 + [[package]] 4339 + name = "time-macros" 4340 + version = "0.2.22" 4341 + source = "registry+https://github.com/rust-lang/crates.io-index" 4342 + checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 4343 + dependencies = [ 4344 + "num-conv", 4345 + "time-core", 4346 + ] 4347 + 4348 + [[package]] 4349 + name = "tinystr" 4350 + version = "0.8.1" 4351 + source = "registry+https://github.com/rust-lang/crates.io-index" 4352 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 4353 + dependencies = [ 4354 + "displaydoc", 4355 + "zerovec", 4356 + ] 4357 + 4358 + [[package]] 4359 + name = "tinyvec" 4360 + version = "1.9.0" 4361 + source = "registry+https://github.com/rust-lang/crates.io-index" 4362 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 4363 + dependencies = [ 4364 + "tinyvec_macros", 4365 + ] 4366 + 4367 + [[package]] 4368 + name = "tinyvec_macros" 4369 + version = "0.1.1" 4370 + source = "registry+https://github.com/rust-lang/crates.io-index" 4371 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 4372 + 4373 + [[package]] 4374 + name = "tokio" 4375 + version = "1.46.1" 4376 + source = "registry+https://github.com/rust-lang/crates.io-index" 4377 + checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" 4378 + dependencies = [ 4379 + "backtrace", 4380 + "bytes", 4381 + "io-uring", 4382 + "libc", 4383 + "mio", 4384 + "parking_lot", 4385 + "pin-project-lite", 4386 + "signal-hook-registry", 4387 + "slab", 4388 + "socket2", 4389 + "tokio-macros", 4390 + "windows-sys 0.52.0", 4391 + ] 4392 + 4393 + [[package]] 4394 + name = "tokio-macros" 4395 + version = "2.5.0" 4396 + source = "registry+https://github.com/rust-lang/crates.io-index" 4397 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 4398 + dependencies = [ 4399 + "proc-macro2", 4400 + "quote", 4401 + "syn 2.0.104", 4402 + ] 4403 + 4404 + [[package]] 4405 + name = "tokio-native-tls" 4406 + version = "0.3.1" 4407 + source = "registry+https://github.com/rust-lang/crates.io-index" 4408 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 4409 + dependencies = [ 4410 + "native-tls", 4411 + "tokio", 4412 + ] 4413 + 4414 + [[package]] 4415 + name = "tokio-rustls" 4416 + version = "0.26.2" 4417 + source = "registry+https://github.com/rust-lang/crates.io-index" 4418 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 4419 + dependencies = [ 4420 + "rustls", 4421 + "tokio", 4422 + ] 4423 + 4424 + [[package]] 4425 + name = "tokio-stream" 4426 + version = "0.1.17" 4427 + source = "registry+https://github.com/rust-lang/crates.io-index" 4428 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 4429 + dependencies = [ 4430 + "futures-core", 4431 + "pin-project-lite", 4432 + "tokio", 4433 + ] 4434 + 4435 + [[package]] 4436 + name = "tokio-util" 4437 + version = "0.7.15" 4438 + source = "registry+https://github.com/rust-lang/crates.io-index" 4439 + checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 4440 + dependencies = [ 4441 + "bytes", 4442 + "futures-core", 4443 + "futures-sink", 4444 + "futures-util", 4445 + "hashbrown 0.15.4", 4446 + "pin-project-lite", 4447 + "tokio", 4448 + ] 4449 + 4450 + [[package]] 4451 + name = "tokio-websockets" 4452 + version = "0.11.4" 4453 + source = "registry+https://github.com/rust-lang/crates.io-index" 4454 + checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" 4455 + dependencies = [ 4456 + "base64", 4457 + "bytes", 4458 + "futures-core", 4459 + "futures-sink", 4460 + "http", 4461 + "httparse", 4462 + "rand 0.9.1", 4463 + "ring", 4464 + "rustls-native-certs", 4465 + "rustls-pki-types", 4466 + "simdutf8", 4467 + "tokio", 4468 + "tokio-rustls", 4469 + "tokio-util", 4470 + ] 4471 + 4472 + [[package]] 4473 + name = "toml" 4474 + version = "0.8.23" 4475 + source = "registry+https://github.com/rust-lang/crates.io-index" 4476 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 4477 + dependencies = [ 4478 + "serde", 4479 + "serde_spanned", 4480 + "toml_datetime", 4481 + "toml_edit", 4482 + ] 4483 + 4484 + [[package]] 4485 + name = "toml_datetime" 4486 + version = "0.6.11" 4487 + source = "registry+https://github.com/rust-lang/crates.io-index" 4488 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 4489 + dependencies = [ 4490 + "serde", 4491 + ] 4492 + 4493 + [[package]] 4494 + name = "toml_edit" 4495 + version = "0.22.27" 4496 + source = "registry+https://github.com/rust-lang/crates.io-index" 4497 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 4498 + dependencies = [ 4499 + "indexmap", 4500 + "serde", 4501 + "serde_spanned", 4502 + "toml_datetime", 4503 + "winnow 0.7.11", 4504 + ] 4505 + 4506 + [[package]] 4507 + name = "tower" 4508 + version = "0.5.2" 4509 + source = "registry+https://github.com/rust-lang/crates.io-index" 4510 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 4511 + dependencies = [ 4512 + "futures-core", 4513 + "futures-util", 4514 + "pin-project-lite", 4515 + "sync_wrapper", 4516 + "tokio", 4517 + "tower-layer", 4518 + "tower-service", 4519 + "tracing", 4520 + ] 4521 + 4522 + [[package]] 4523 + name = "tower-http" 4524 + version = "0.5.2" 4525 + source = "registry+https://github.com/rust-lang/crates.io-index" 4526 + checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" 4527 + dependencies = [ 4528 + "bitflags 2.9.1", 4529 + "bytes", 4530 + "futures-util", 4531 + "http", 4532 + "http-body", 4533 + "http-body-util", 4534 + "http-range-header", 4535 + "httpdate", 4536 + "mime", 4537 + "mime_guess", 4538 + "percent-encoding", 4539 + "pin-project-lite", 4540 + "tokio", 4541 + "tokio-util", 4542 + "tower-layer", 4543 + "tower-service", 4544 + "tracing", 4545 + ] 4546 + 4547 + [[package]] 4548 + name = "tower-http" 4549 + version = "0.6.6" 4550 + source = "registry+https://github.com/rust-lang/crates.io-index" 4551 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 4552 + dependencies = [ 4553 + "bitflags 2.9.1", 4554 + "bytes", 4555 + "futures-util", 4556 + "http", 4557 + "http-body", 4558 + "iri-string", 4559 + "pin-project-lite", 4560 + "tower", 4561 + "tower-layer", 4562 + "tower-service", 4563 + ] 4564 + 4565 + [[package]] 4566 + name = "tower-layer" 4567 + version = "0.3.3" 4568 + source = "registry+https://github.com/rust-lang/crates.io-index" 4569 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 4570 + 4571 + [[package]] 4572 + name = "tower-service" 4573 + version = "0.3.3" 4574 + source = "registry+https://github.com/rust-lang/crates.io-index" 4575 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 4576 + 4577 + [[package]] 4578 + name = "tracing" 4579 + version = "0.1.41" 4580 + source = "registry+https://github.com/rust-lang/crates.io-index" 4581 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 4582 + dependencies = [ 4583 + "log", 4584 + "pin-project-lite", 4585 + "tracing-attributes", 4586 + "tracing-core", 4587 + ] 4588 + 4589 + [[package]] 4590 + name = "tracing-attributes" 4591 + version = "0.1.30" 4592 + source = "registry+https://github.com/rust-lang/crates.io-index" 4593 + checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 4594 + dependencies = [ 4595 + "proc-macro2", 4596 + "quote", 4597 + "syn 2.0.104", 4598 + ] 4599 + 4600 + [[package]] 4601 + name = "tracing-core" 4602 + version = "0.1.34" 4603 + source = "registry+https://github.com/rust-lang/crates.io-index" 4604 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 4605 + dependencies = [ 4606 + "once_cell", 4607 + "valuable", 4608 + ] 4609 + 4610 + [[package]] 4611 + name = "tracing-log" 4612 + version = "0.2.0" 4613 + source = "registry+https://github.com/rust-lang/crates.io-index" 4614 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 4615 + dependencies = [ 4616 + "log", 4617 + "once_cell", 4618 + "tracing-core", 4619 + ] 4620 + 4621 + [[package]] 4622 + name = "tracing-subscriber" 4623 + version = "0.3.19" 4624 + source = "registry+https://github.com/rust-lang/crates.io-index" 4625 + checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 4626 + dependencies = [ 4627 + "matchers", 4628 + "nu-ansi-term", 4629 + "once_cell", 4630 + "regex", 4631 + "sharded-slab", 4632 + "smallvec", 4633 + "thread_local", 4634 + "tracing", 4635 + "tracing-core", 4636 + "tracing-log", 4637 + ] 4638 + 4639 + [[package]] 4640 + name = "try-lock" 4641 + version = "0.2.5" 4642 + source = "registry+https://github.com/rust-lang/crates.io-index" 4643 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 4644 + 4645 + [[package]] 4646 + name = "typed-arena" 4647 + version = "2.0.2" 4648 + source = "registry+https://github.com/rust-lang/crates.io-index" 4649 + checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 4650 + 4651 + [[package]] 4652 + name = "typenum" 4653 + version = "1.18.0" 4654 + source = "registry+https://github.com/rust-lang/crates.io-index" 4655 + checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 4656 + 4657 + [[package]] 4658 + name = "ulid" 4659 + version = "1.2.1" 4660 + source = "registry+https://github.com/rust-lang/crates.io-index" 4661 + checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" 4662 + dependencies = [ 4663 + "rand 0.9.1", 4664 + "web-time", 4665 + ] 4666 + 4667 + [[package]] 4668 + name = "unicase" 4669 + version = "2.8.1" 4670 + source = "registry+https://github.com/rust-lang/crates.io-index" 4671 + checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 4672 + 4673 + [[package]] 4674 + name = "unicode-bidi" 4675 + version = "0.3.18" 4676 + source = "registry+https://github.com/rust-lang/crates.io-index" 4677 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 4678 + 4679 + [[package]] 4680 + name = "unicode-ident" 4681 + version = "1.0.18" 4682 + source = "registry+https://github.com/rust-lang/crates.io-index" 4683 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 4684 + 4685 + [[package]] 4686 + name = "unicode-normalization" 4687 + version = "0.1.24" 4688 + source = "registry+https://github.com/rust-lang/crates.io-index" 4689 + checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 4690 + dependencies = [ 4691 + "tinyvec", 4692 + ] 4693 + 4694 + [[package]] 4695 + name = "unicode-properties" 4696 + version = "0.1.3" 4697 + source = "registry+https://github.com/rust-lang/crates.io-index" 4698 + checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 4699 + 4700 + [[package]] 4701 + name = "unicode_categories" 4702 + version = "0.1.1" 4703 + source = "registry+https://github.com/rust-lang/crates.io-index" 4704 + checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 4705 + 4706 + [[package]] 4707 + name = "unidecode" 4708 + version = "0.3.0" 4709 + source = "registry+https://github.com/rust-lang/crates.io-index" 4710 + checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" 4711 + 4712 + [[package]] 4713 + name = "unsigned-varint" 4714 + version = "0.8.0" 4715 + source = "registry+https://github.com/rust-lang/crates.io-index" 4716 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 4717 + 4718 + [[package]] 4719 + name = "untrusted" 4720 + version = "0.9.0" 4721 + source = "registry+https://github.com/rust-lang/crates.io-index" 4722 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 4723 + 4724 + [[package]] 4725 + name = "url" 4726 + version = "2.5.4" 4727 + source = "registry+https://github.com/rust-lang/crates.io-index" 4728 + checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 4729 + dependencies = [ 4730 + "form_urlencoded", 4731 + "idna", 4732 + "percent-encoding", 4733 + ] 4734 + 4735 + [[package]] 4736 + name = "urlencoding" 4737 + version = "2.1.3" 4738 + source = "registry+https://github.com/rust-lang/crates.io-index" 4739 + checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 4740 + 4741 + [[package]] 4742 + name = "utf8_iter" 4743 + version = "1.0.4" 4744 + source = "registry+https://github.com/rust-lang/crates.io-index" 4745 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 4746 + 4747 + [[package]] 4748 + name = "utf8parse" 4749 + version = "0.2.2" 4750 + source = "registry+https://github.com/rust-lang/crates.io-index" 4751 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 4752 + 4753 + [[package]] 4754 + name = "uuid" 4755 + version = "1.17.0" 4756 + source = "registry+https://github.com/rust-lang/crates.io-index" 4757 + checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 4758 + dependencies = [ 4759 + "getrandom 0.3.3", 4760 + "js-sys", 4761 + "wasm-bindgen", 4762 + ] 4763 + 4764 + [[package]] 4765 + name = "v_frame" 4766 + version = "0.3.9" 4767 + source = "registry+https://github.com/rust-lang/crates.io-index" 4768 + checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" 4769 + dependencies = [ 4770 + "aligned-vec", 4771 + "num-traits", 4772 + "wasm-bindgen", 4773 + ] 4774 + 4775 + [[package]] 4776 + name = "valuable" 4777 + version = "0.1.1" 4778 + source = "registry+https://github.com/rust-lang/crates.io-index" 4779 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 4780 + 4781 + [[package]] 4782 + name = "vcpkg" 4783 + version = "0.2.15" 4784 + source = "registry+https://github.com/rust-lang/crates.io-index" 4785 + checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 4786 + 4787 + [[package]] 4788 + name = "version-compare" 4789 + version = "0.2.0" 4790 + source = "registry+https://github.com/rust-lang/crates.io-index" 4791 + checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 4792 + 4793 + [[package]] 4794 + name = "version_check" 4795 + version = "0.9.5" 4796 + source = "registry+https://github.com/rust-lang/crates.io-index" 4797 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 4798 + 4799 + [[package]] 4800 + name = "walkdir" 4801 + version = "2.5.0" 4802 + source = "registry+https://github.com/rust-lang/crates.io-index" 4803 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 4804 + dependencies = [ 4805 + "same-file", 4806 + "winapi-util", 4807 + ] 4808 + 4809 + [[package]] 4810 + name = "want" 4811 + version = "0.3.1" 4812 + source = "registry+https://github.com/rust-lang/crates.io-index" 4813 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 4814 + dependencies = [ 4815 + "try-lock", 4816 + ] 4817 + 4818 + [[package]] 4819 + name = "wasi" 4820 + version = "0.11.1+wasi-snapshot-preview1" 4821 + source = "registry+https://github.com/rust-lang/crates.io-index" 4822 + checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 4823 + 4824 + [[package]] 4825 + name = "wasi" 4826 + version = "0.14.2+wasi-0.2.4" 4827 + source = "registry+https://github.com/rust-lang/crates.io-index" 4828 + checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 4829 + dependencies = [ 4830 + "wit-bindgen-rt", 4831 + ] 4832 + 4833 + [[package]] 4834 + name = "wasite" 4835 + version = "0.1.0" 4836 + source = "registry+https://github.com/rust-lang/crates.io-index" 4837 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 4838 + 4839 + [[package]] 4840 + name = "wasm-bindgen" 4841 + version = "0.2.100" 4842 + source = "registry+https://github.com/rust-lang/crates.io-index" 4843 + checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 4844 + dependencies = [ 4845 + "cfg-if", 4846 + "once_cell", 4847 + "rustversion", 4848 + "wasm-bindgen-macro", 4849 + ] 4850 + 4851 + [[package]] 4852 + name = "wasm-bindgen-backend" 4853 + version = "0.2.100" 4854 + source = "registry+https://github.com/rust-lang/crates.io-index" 4855 + checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 4856 + dependencies = [ 4857 + "bumpalo", 4858 + "log", 4859 + "proc-macro2", 4860 + "quote", 4861 + "syn 2.0.104", 4862 + "wasm-bindgen-shared", 4863 + ] 4864 + 4865 + [[package]] 4866 + name = "wasm-bindgen-futures" 4867 + version = "0.4.50" 4868 + source = "registry+https://github.com/rust-lang/crates.io-index" 4869 + checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 4870 + dependencies = [ 4871 + "cfg-if", 4872 + "js-sys", 4873 + "once_cell", 4874 + "wasm-bindgen", 4875 + "web-sys", 4876 + ] 4877 + 4878 + [[package]] 4879 + name = "wasm-bindgen-macro" 4880 + version = "0.2.100" 4881 + source = "registry+https://github.com/rust-lang/crates.io-index" 4882 + checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 4883 + dependencies = [ 4884 + "quote", 4885 + "wasm-bindgen-macro-support", 4886 + ] 4887 + 4888 + [[package]] 4889 + name = "wasm-bindgen-macro-support" 4890 + version = "0.2.100" 4891 + source = "registry+https://github.com/rust-lang/crates.io-index" 4892 + checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 4893 + dependencies = [ 4894 + "proc-macro2", 4895 + "quote", 4896 + "syn 2.0.104", 4897 + "wasm-bindgen-backend", 4898 + "wasm-bindgen-shared", 4899 + ] 4900 + 4901 + [[package]] 4902 + name = "wasm-bindgen-shared" 4903 + version = "0.2.100" 4904 + source = "registry+https://github.com/rust-lang/crates.io-index" 4905 + checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 4906 + dependencies = [ 4907 + "unicode-ident", 4908 + ] 4909 + 4910 + [[package]] 4911 + name = "wasm-streams" 4912 + version = "0.4.2" 4913 + source = "registry+https://github.com/rust-lang/crates.io-index" 4914 + checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 4915 + dependencies = [ 4916 + "futures-util", 4917 + "js-sys", 4918 + "wasm-bindgen", 4919 + "wasm-bindgen-futures", 4920 + "web-sys", 4921 + ] 4922 + 4923 + [[package]] 4924 + name = "web-sys" 4925 + version = "0.3.77" 4926 + source = "registry+https://github.com/rust-lang/crates.io-index" 4927 + checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 4928 + dependencies = [ 4929 + "js-sys", 4930 + "wasm-bindgen", 4931 + ] 4932 + 4933 + [[package]] 4934 + name = "web-time" 4935 + version = "1.1.0" 4936 + source = "registry+https://github.com/rust-lang/crates.io-index" 4937 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 4938 + dependencies = [ 4939 + "js-sys", 4940 + "wasm-bindgen", 4941 + ] 4942 + 4943 + [[package]] 4944 + name = "webpki-roots" 4945 + version = "0.26.11" 4946 + source = "registry+https://github.com/rust-lang/crates.io-index" 4947 + checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 4948 + dependencies = [ 4949 + "webpki-roots 1.0.1", 4950 + ] 4951 + 4952 + [[package]] 4953 + name = "webpki-roots" 4954 + version = "1.0.1" 4955 + source = "registry+https://github.com/rust-lang/crates.io-index" 4956 + checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" 4957 + dependencies = [ 4958 + "rustls-pki-types", 4959 + ] 4960 + 4961 + [[package]] 4962 + name = "weezl" 4963 + version = "0.1.10" 4964 + source = "registry+https://github.com/rust-lang/crates.io-index" 4965 + checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 4966 + 4967 + [[package]] 4968 + name = "whoami" 4969 + version = "1.6.0" 4970 + source = "registry+https://github.com/rust-lang/crates.io-index" 4971 + checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" 4972 + dependencies = [ 4973 + "redox_syscall", 4974 + "wasite", 4975 + ] 4976 + 4977 + [[package]] 4978 + name = "widestring" 4979 + version = "1.2.0" 4980 + source = "registry+https://github.com/rust-lang/crates.io-index" 4981 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 4982 + 4983 + [[package]] 4984 + name = "winapi" 4985 + version = "0.3.9" 4986 + source = "registry+https://github.com/rust-lang/crates.io-index" 4987 + checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 4988 + dependencies = [ 4989 + "winapi-i686-pc-windows-gnu", 4990 + "winapi-x86_64-pc-windows-gnu", 4991 + ] 4992 + 4993 + [[package]] 4994 + name = "winapi-i686-pc-windows-gnu" 4995 + version = "0.4.0" 4996 + source = "registry+https://github.com/rust-lang/crates.io-index" 4997 + checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 4998 + 4999 + [[package]] 5000 + name = "winapi-util" 5001 + version = "0.1.9" 5002 + source = "registry+https://github.com/rust-lang/crates.io-index" 5003 + checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 5004 + dependencies = [ 5005 + "windows-sys 0.59.0", 5006 + ] 5007 + 5008 + [[package]] 5009 + name = "winapi-x86_64-pc-windows-gnu" 5010 + version = "0.4.0" 5011 + source = "registry+https://github.com/rust-lang/crates.io-index" 5012 + checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 5013 + 5014 + [[package]] 5015 + name = "windows" 5016 + version = "0.61.3" 5017 + source = "registry+https://github.com/rust-lang/crates.io-index" 5018 + checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" 5019 + dependencies = [ 5020 + "windows-collections", 5021 + "windows-core", 5022 + "windows-future", 5023 + "windows-link", 5024 + "windows-numerics", 5025 + ] 5026 + 5027 + [[package]] 5028 + name = "windows-collections" 5029 + version = "0.2.0" 5030 + source = "registry+https://github.com/rust-lang/crates.io-index" 5031 + checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 5032 + dependencies = [ 5033 + "windows-core", 5034 + ] 5035 + 5036 + [[package]] 5037 + name = "windows-core" 5038 + version = "0.61.2" 5039 + source = "registry+https://github.com/rust-lang/crates.io-index" 5040 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 5041 + dependencies = [ 5042 + "windows-implement", 5043 + "windows-interface", 5044 + "windows-link", 5045 + "windows-result", 5046 + "windows-strings", 5047 + ] 5048 + 5049 + [[package]] 5050 + name = "windows-future" 5051 + version = "0.2.1" 5052 + source = "registry+https://github.com/rust-lang/crates.io-index" 5053 + checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" 5054 + dependencies = [ 5055 + "windows-core", 5056 + "windows-link", 5057 + "windows-threading", 5058 + ] 5059 + 5060 + [[package]] 5061 + name = "windows-implement" 5062 + version = "0.60.0" 5063 + source = "registry+https://github.com/rust-lang/crates.io-index" 5064 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 5065 + dependencies = [ 5066 + "proc-macro2", 5067 + "quote", 5068 + "syn 2.0.104", 5069 + ] 5070 + 5071 + [[package]] 5072 + name = "windows-interface" 5073 + version = "0.59.1" 5074 + source = "registry+https://github.com/rust-lang/crates.io-index" 5075 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 5076 + dependencies = [ 5077 + "proc-macro2", 5078 + "quote", 5079 + "syn 2.0.104", 5080 + ] 5081 + 5082 + [[package]] 5083 + name = "windows-link" 5084 + version = "0.1.3" 5085 + source = "registry+https://github.com/rust-lang/crates.io-index" 5086 + checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 5087 + 5088 + [[package]] 5089 + name = "windows-numerics" 5090 + version = "0.2.0" 5091 + source = "registry+https://github.com/rust-lang/crates.io-index" 5092 + checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 5093 + dependencies = [ 5094 + "windows-core", 5095 + "windows-link", 5096 + ] 5097 + 5098 + [[package]] 5099 + name = "windows-registry" 5100 + version = "0.5.3" 5101 + source = "registry+https://github.com/rust-lang/crates.io-index" 5102 + checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 5103 + dependencies = [ 5104 + "windows-link", 5105 + "windows-result", 5106 + "windows-strings", 5107 + ] 5108 + 5109 + [[package]] 5110 + name = "windows-result" 5111 + version = "0.3.4" 5112 + source = "registry+https://github.com/rust-lang/crates.io-index" 5113 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 5114 + dependencies = [ 5115 + "windows-link", 5116 + ] 5117 + 5118 + [[package]] 5119 + name = "windows-strings" 5120 + version = "0.4.2" 5121 + source = "registry+https://github.com/rust-lang/crates.io-index" 5122 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 5123 + dependencies = [ 5124 + "windows-link", 5125 + ] 5126 + 5127 + [[package]] 5128 + name = "windows-sys" 5129 + version = "0.48.0" 5130 + source = "registry+https://github.com/rust-lang/crates.io-index" 5131 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 5132 + dependencies = [ 5133 + "windows-targets 0.48.5", 5134 + ] 5135 + 5136 + [[package]] 5137 + name = "windows-sys" 5138 + version = "0.52.0" 5139 + source = "registry+https://github.com/rust-lang/crates.io-index" 5140 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 5141 + dependencies = [ 5142 + "windows-targets 0.52.6", 5143 + ] 5144 + 5145 + [[package]] 5146 + name = "windows-sys" 5147 + version = "0.59.0" 5148 + source = "registry+https://github.com/rust-lang/crates.io-index" 5149 + checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 5150 + dependencies = [ 5151 + "windows-targets 0.52.6", 5152 + ] 5153 + 5154 + [[package]] 5155 + name = "windows-sys" 5156 + version = "0.60.2" 5157 + source = "registry+https://github.com/rust-lang/crates.io-index" 5158 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 5159 + dependencies = [ 5160 + "windows-targets 0.53.2", 5161 + ] 5162 + 5163 + [[package]] 5164 + name = "windows-targets" 5165 + version = "0.48.5" 5166 + source = "registry+https://github.com/rust-lang/crates.io-index" 5167 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 5168 + dependencies = [ 5169 + "windows_aarch64_gnullvm 0.48.5", 5170 + "windows_aarch64_msvc 0.48.5", 5171 + "windows_i686_gnu 0.48.5", 5172 + "windows_i686_msvc 0.48.5", 5173 + "windows_x86_64_gnu 0.48.5", 5174 + "windows_x86_64_gnullvm 0.48.5", 5175 + "windows_x86_64_msvc 0.48.5", 5176 + ] 5177 + 5178 + [[package]] 5179 + name = "windows-targets" 5180 + version = "0.52.6" 5181 + source = "registry+https://github.com/rust-lang/crates.io-index" 5182 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 5183 + dependencies = [ 5184 + "windows_aarch64_gnullvm 0.52.6", 5185 + "windows_aarch64_msvc 0.52.6", 5186 + "windows_i686_gnu 0.52.6", 5187 + "windows_i686_gnullvm 0.52.6", 5188 + "windows_i686_msvc 0.52.6", 5189 + "windows_x86_64_gnu 0.52.6", 5190 + "windows_x86_64_gnullvm 0.52.6", 5191 + "windows_x86_64_msvc 0.52.6", 5192 + ] 5193 + 5194 + [[package]] 5195 + name = "windows-targets" 5196 + version = "0.53.2" 5197 + source = "registry+https://github.com/rust-lang/crates.io-index" 5198 + checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 5199 + dependencies = [ 5200 + "windows_aarch64_gnullvm 0.53.0", 5201 + "windows_aarch64_msvc 0.53.0", 5202 + "windows_i686_gnu 0.53.0", 5203 + "windows_i686_gnullvm 0.53.0", 5204 + "windows_i686_msvc 0.53.0", 5205 + "windows_x86_64_gnu 0.53.0", 5206 + "windows_x86_64_gnullvm 0.53.0", 5207 + "windows_x86_64_msvc 0.53.0", 5208 + ] 5209 + 5210 + [[package]] 5211 + name = "windows-threading" 5212 + version = "0.1.0" 5213 + source = "registry+https://github.com/rust-lang/crates.io-index" 5214 + checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" 5215 + dependencies = [ 5216 + "windows-link", 5217 + ] 5218 + 5219 + [[package]] 5220 + name = "windows_aarch64_gnullvm" 5221 + version = "0.48.5" 5222 + source = "registry+https://github.com/rust-lang/crates.io-index" 5223 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 5224 + 5225 + [[package]] 5226 + name = "windows_aarch64_gnullvm" 5227 + version = "0.52.6" 5228 + source = "registry+https://github.com/rust-lang/crates.io-index" 5229 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 5230 + 5231 + [[package]] 5232 + name = "windows_aarch64_gnullvm" 5233 + version = "0.53.0" 5234 + source = "registry+https://github.com/rust-lang/crates.io-index" 5235 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 5236 + 5237 + [[package]] 5238 + name = "windows_aarch64_msvc" 5239 + version = "0.48.5" 5240 + source = "registry+https://github.com/rust-lang/crates.io-index" 5241 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 5242 + 5243 + [[package]] 5244 + name = "windows_aarch64_msvc" 5245 + version = "0.52.6" 5246 + source = "registry+https://github.com/rust-lang/crates.io-index" 5247 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 5248 + 5249 + [[package]] 5250 + name = "windows_aarch64_msvc" 5251 + version = "0.53.0" 5252 + source = "registry+https://github.com/rust-lang/crates.io-index" 5253 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 5254 + 5255 + [[package]] 5256 + name = "windows_i686_gnu" 5257 + version = "0.48.5" 5258 + source = "registry+https://github.com/rust-lang/crates.io-index" 5259 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 5260 + 5261 + [[package]] 5262 + name = "windows_i686_gnu" 5263 + version = "0.52.6" 5264 + source = "registry+https://github.com/rust-lang/crates.io-index" 5265 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 5266 + 5267 + [[package]] 5268 + name = "windows_i686_gnu" 5269 + version = "0.53.0" 5270 + source = "registry+https://github.com/rust-lang/crates.io-index" 5271 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 5272 + 5273 + [[package]] 5274 + name = "windows_i686_gnullvm" 5275 + version = "0.52.6" 5276 + source = "registry+https://github.com/rust-lang/crates.io-index" 5277 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 5278 + 5279 + [[package]] 5280 + name = "windows_i686_gnullvm" 5281 + version = "0.53.0" 5282 + source = "registry+https://github.com/rust-lang/crates.io-index" 5283 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 5284 + 5285 + [[package]] 5286 + name = "windows_i686_msvc" 5287 + version = "0.48.5" 5288 + source = "registry+https://github.com/rust-lang/crates.io-index" 5289 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 5290 + 5291 + [[package]] 5292 + name = "windows_i686_msvc" 5293 + version = "0.52.6" 5294 + source = "registry+https://github.com/rust-lang/crates.io-index" 5295 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 5296 + 5297 + [[package]] 5298 + name = "windows_i686_msvc" 5299 + version = "0.53.0" 5300 + source = "registry+https://github.com/rust-lang/crates.io-index" 5301 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 5302 + 5303 + [[package]] 5304 + name = "windows_x86_64_gnu" 5305 + version = "0.48.5" 5306 + source = "registry+https://github.com/rust-lang/crates.io-index" 5307 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 5308 + 5309 + [[package]] 5310 + name = "windows_x86_64_gnu" 5311 + version = "0.52.6" 5312 + source = "registry+https://github.com/rust-lang/crates.io-index" 5313 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 5314 + 5315 + [[package]] 5316 + name = "windows_x86_64_gnu" 5317 + version = "0.53.0" 5318 + source = "registry+https://github.com/rust-lang/crates.io-index" 5319 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 5320 + 5321 + [[package]] 5322 + name = "windows_x86_64_gnullvm" 5323 + version = "0.48.5" 5324 + source = "registry+https://github.com/rust-lang/crates.io-index" 5325 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 5326 + 5327 + [[package]] 5328 + name = "windows_x86_64_gnullvm" 5329 + version = "0.52.6" 5330 + source = "registry+https://github.com/rust-lang/crates.io-index" 5331 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 5332 + 5333 + [[package]] 5334 + name = "windows_x86_64_gnullvm" 5335 + version = "0.53.0" 5336 + source = "registry+https://github.com/rust-lang/crates.io-index" 5337 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 5338 + 5339 + [[package]] 5340 + name = "windows_x86_64_msvc" 5341 + version = "0.48.5" 5342 + source = "registry+https://github.com/rust-lang/crates.io-index" 5343 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 5344 + 5345 + [[package]] 5346 + name = "windows_x86_64_msvc" 5347 + version = "0.52.6" 5348 + source = "registry+https://github.com/rust-lang/crates.io-index" 5349 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 5350 + 5351 + [[package]] 5352 + name = "windows_x86_64_msvc" 5353 + version = "0.53.0" 5354 + source = "registry+https://github.com/rust-lang/crates.io-index" 5355 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 5356 + 5357 + [[package]] 5358 + name = "winnow" 5359 + version = "0.6.26" 5360 + source = "registry+https://github.com/rust-lang/crates.io-index" 5361 + checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" 5362 + dependencies = [ 5363 + "memchr", 5364 + ] 5365 + 5366 + [[package]] 5367 + name = "winnow" 5368 + version = "0.7.11" 5369 + source = "registry+https://github.com/rust-lang/crates.io-index" 5370 + checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" 5371 + dependencies = [ 5372 + "memchr", 5373 + ] 5374 + 5375 + [[package]] 5376 + name = "winreg" 5377 + version = "0.50.0" 5378 + source = "registry+https://github.com/rust-lang/crates.io-index" 5379 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 5380 + dependencies = [ 5381 + "cfg-if", 5382 + "windows-sys 0.48.0", 5383 + ] 5384 + 5385 + [[package]] 5386 + name = "wit-bindgen-rt" 5387 + version = "0.39.0" 5388 + source = "registry+https://github.com/rust-lang/crates.io-index" 5389 + checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 5390 + dependencies = [ 5391 + "bitflags 2.9.1", 5392 + ] 5393 + 5394 + [[package]] 5395 + name = "writeable" 5396 + version = "0.6.1" 5397 + source = "registry+https://github.com/rust-lang/crates.io-index" 5398 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 5399 + 5400 + [[package]] 5401 + name = "xdg" 5402 + version = "2.5.2" 5403 + source = "registry+https://github.com/rust-lang/crates.io-index" 5404 + checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" 5405 + 5406 + [[package]] 5407 + name = "xml-rs" 5408 + version = "0.8.26" 5409 + source = "registry+https://github.com/rust-lang/crates.io-index" 5410 + checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" 5411 + 5412 + [[package]] 5413 + name = "xmltree" 5414 + version = "0.11.0" 5415 + source = "registry+https://github.com/rust-lang/crates.io-index" 5416 + checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6" 5417 + dependencies = [ 5418 + "xml-rs", 5419 + ] 5420 + 5421 + [[package]] 5422 + name = "yaml-rust" 5423 + version = "0.4.5" 5424 + source = "registry+https://github.com/rust-lang/crates.io-index" 5425 + checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 5426 + dependencies = [ 5427 + "linked-hash-map", 5428 + ] 5429 + 5430 + [[package]] 5431 + name = "yoke" 5432 + version = "0.8.0" 5433 + source = "registry+https://github.com/rust-lang/crates.io-index" 5434 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 5435 + dependencies = [ 5436 + "serde", 5437 + "stable_deref_trait", 5438 + "yoke-derive", 5439 + "zerofrom", 5440 + ] 5441 + 5442 + [[package]] 5443 + name = "yoke-derive" 5444 + version = "0.8.0" 5445 + source = "registry+https://github.com/rust-lang/crates.io-index" 5446 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 5447 + dependencies = [ 5448 + "proc-macro2", 5449 + "quote", 5450 + "syn 2.0.104", 5451 + "synstructure", 5452 + ] 5453 + 5454 + [[package]] 5455 + name = "zerocopy" 5456 + version = "0.8.26" 5457 + source = "registry+https://github.com/rust-lang/crates.io-index" 5458 + checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 5459 + dependencies = [ 5460 + "zerocopy-derive", 5461 + ] 5462 + 5463 + [[package]] 5464 + name = "zerocopy-derive" 5465 + version = "0.8.26" 5466 + source = "registry+https://github.com/rust-lang/crates.io-index" 5467 + checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 5468 + dependencies = [ 5469 + "proc-macro2", 5470 + "quote", 5471 + "syn 2.0.104", 5472 + ] 5473 + 5474 + [[package]] 5475 + name = "zerofrom" 5476 + version = "0.1.6" 5477 + source = "registry+https://github.com/rust-lang/crates.io-index" 5478 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 5479 + dependencies = [ 5480 + "zerofrom-derive", 5481 + ] 5482 + 5483 + [[package]] 5484 + name = "zerofrom-derive" 5485 + version = "0.1.6" 5486 + source = "registry+https://github.com/rust-lang/crates.io-index" 5487 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 5488 + dependencies = [ 5489 + "proc-macro2", 5490 + "quote", 5491 + "syn 2.0.104", 5492 + "synstructure", 5493 + ] 5494 + 5495 + [[package]] 5496 + name = "zeroize" 5497 + version = "1.8.1" 5498 + source = "registry+https://github.com/rust-lang/crates.io-index" 5499 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 5500 + 5501 + [[package]] 5502 + name = "zerotrie" 5503 + version = "0.2.2" 5504 + source = "registry+https://github.com/rust-lang/crates.io-index" 5505 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 5506 + dependencies = [ 5507 + "displaydoc", 5508 + "yoke", 5509 + "zerofrom", 5510 + ] 5511 + 5512 + [[package]] 5513 + name = "zerovec" 5514 + version = "0.11.2" 5515 + source = "registry+https://github.com/rust-lang/crates.io-index" 5516 + checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 5517 + dependencies = [ 5518 + "yoke", 5519 + "zerofrom", 5520 + "zerovec-derive", 5521 + ] 5522 + 5523 + [[package]] 5524 + name = "zerovec-derive" 5525 + version = "0.11.1" 5526 + source = "registry+https://github.com/rust-lang/crates.io-index" 5527 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 5528 + dependencies = [ 5529 + "proc-macro2", 5530 + "quote", 5531 + "syn 2.0.104", 5532 + ] 5533 + 5534 + [[package]] 5535 + name = "zstd" 5536 + version = "0.13.3" 5537 + source = "registry+https://github.com/rust-lang/crates.io-index" 5538 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 5539 + dependencies = [ 5540 + "zstd-safe", 5541 + ] 5542 + 5543 + [[package]] 5544 + name = "zstd-safe" 5545 + version = "7.2.4" 5546 + source = "registry+https://github.com/rust-lang/crates.io-index" 5547 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 5548 + dependencies = [ 5549 + "zstd-sys", 5550 + ] 5551 + 5552 + [[package]] 5553 + name = "zstd-sys" 5554 + version = "2.0.15+zstd.1.5.7" 5555 + source = "registry+https://github.com/rust-lang/crates.io-index" 5556 + checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 5557 + dependencies = [ 5558 + "cc", 5559 + "pkg-config", 5560 + ] 5561 + 5562 + [[package]] 5563 + name = "zune-core" 5564 + version = "0.4.12" 5565 + source = "registry+https://github.com/rust-lang/crates.io-index" 5566 + checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 5567 + 5568 + [[package]] 5569 + name = "zune-inflate" 5570 + version = "0.2.54" 5571 + source = "registry+https://github.com/rust-lang/crates.io-index" 5572 + checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 5573 + dependencies = [ 5574 + "simd-adler32", 5575 + ] 5576 + 5577 + [[package]] 5578 + name = "zune-jpeg" 5579 + version = "0.4.19" 5580 + source = "registry+https://github.com/rust-lang/crates.io-index" 5581 + checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" 5582 + dependencies = [ 5583 + "zune-core", 5584 + ]
+75
Cargo.toml
···
··· 1 + [package] 2 + name = "blahg" 3 + version = "0.1.0" 4 + edition = "2024" 5 + 6 + [features] 7 + default = ["reload", "sqlite", "postgres", "s3"] 8 + embed = ["dep:minijinja-embed", "dep:rust-embed"] 9 + reload = ["dep:minijinja-autoreload", "minijinja/loader", "axum-template/minijinja-autoreload"] 10 + sqlite = ["sqlx/sqlite"] 11 + postgres = ["sqlx/postgres"] 12 + s3 = ["dep:minio"] 13 + 14 + [build-dependencies] 15 + minijinja-embed = {version = "2.7"} 16 + 17 + [dependencies] 18 + # ATProtocol dependencies 19 + atproto-client = { version = "0.9.2" } 20 + atproto-identity = { version = "0.9.2" } 21 + atproto-record = { version = "0.9.2" } 22 + atproto-jetstream = { version = "0.9.2" } 23 + 24 + # Web framework 25 + axum = "0.8" 26 + axum-template = { version = "3.0", features = ["minijinja"] } 27 + tower-http = { version = "0.5", features = ["fs"] } 28 + 29 + # Template engine 30 + minijinja = { version = "2.7", features = ["builtins", ] } 31 + minijinja-autoreload = { version = "2.7", optional = true } 32 + minijinja-embed = { version = "2.7", optional = true } 33 + rust-embed = { version = "8.5", optional = true } 34 + 35 + # Database 36 + sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "chrono", "json", "uuid"] } 37 + 38 + # Image processing 39 + image = "0.25" 40 + 41 + # Core dependencies 42 + anyhow = "1.0" 43 + async-trait = "0.1.88" 44 + base64 = "0.22.1" 45 + chrono = {version = "0.4.41", default-features = false, features = ["std", "now", "serde"]} 46 + duration-str = "0.11" 47 + ecdsa = { version = "0.16.9", features = ["std"] } 48 + elliptic-curve = { version = "0.13.8", features = ["jwk", "serde"] } 49 + hickory-resolver = { version = "0.25" } 50 + k256 = "0.13.4" 51 + lru = "0.12" 52 + multibase = "0.9.1" 53 + p256 = "0.13.2" 54 + reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 55 + serde = { version = "1.0", features = ["derive"] } 56 + serde_ipld_dagcbor = "0.6.3" 57 + serde_json = "1.0" 58 + sha2 = "0.10.9" 59 + thiserror = "2.0" 60 + tokio = { version = "1.41", features = ["macros", "rt", "rt-multi-thread", "fs", "signal"] } 61 + tokio-util = { version = "0.7", features = ["rt"] } 62 + tracing = { version = "0.1", features = ["async-await"] } 63 + tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } 64 + 65 + # Object storage 66 + minio = { version = "0.3", optional = true } 67 + bytes = "1.10.1" 68 + 69 + # Markdown rendering 70 + comrak = { version = "0.39", features = ["syntect"] } 71 + slugify = "0.1.0" 72 + bloomfilter = "1.0.15" 73 + 74 + [dev-dependencies] 75 + tempfile = "3.0"
+60
Dockerfile
···
··· 1 + # Build stage 2 + FROM rust:1.87-slim AS builder 3 + 4 + # Install required system dependencies for building 5 + RUN apt-get update && apt-get install -y \ 6 + pkg-config \ 7 + libssl-dev \ 8 + && rm -rf /var/lib/apt/lists/* 9 + 10 + # Set working directory 11 + WORKDIR /app 12 + 13 + # Copy manifests first for better layer caching 14 + COPY Cargo.toml Cargo.lock build.rs ./ 15 + 16 + ARG FEATURES=embed,postgres,s3 17 + ARG TEMPLATES=./templates 18 + ARG STATIC=./static 19 + 20 + # Copy actual source code and assets 21 + COPY src ./src 22 + COPY ${TEMPLATES} ./templates 23 + COPY ${STATIC} ./static 24 + 25 + ENV HTTP_TEMPLATE_PATH=/app/templates/ 26 + 27 + # Build the actual application with embed feature only 28 + RUN cargo build --release --no-default-features --features ${FEATURES} 29 + 30 + # Runtime stage using distroless 31 + FROM gcr.io/distroless/cc-debian12 32 + 33 + # Add OCI labels 34 + LABEL org.opencontainers.image.title="blahg" 35 + LABEL org.opencontainers.image.description="An ATProtocol blogging AppView." 36 + LABEL org.opencontainers.image.version="0.1.0" 37 + LABEL org.opencontainers.image.authors="Nick Gerakines <nick.gerakines@gmail.com>" 38 + LABEL org.opencontainers.image.url="https://blog.smokesignal.events/" 39 + LABEL org.opencontainers.image.source="https://tangled.sh/@smokesignal.events/blahg" 40 + LABEL org.opencontainers.image.licenses="MIT" 41 + LABEL org.opencontainers.image.created="2025-01-06T00:00:00Z" 42 + 43 + # Set working directory 44 + WORKDIR /app 45 + 46 + # Copy the binary from builder stage 47 + COPY --from=builder /app/target/release/blahg /app/blahg 48 + 49 + # Copy static directory 50 + COPY --from=builder /app/static ./static 51 + 52 + # Set environment variables 53 + ENV HTTP_STATIC_PATH=/app/static 54 + ENV HTTP_PORT=8080 55 + 56 + # Expose port 57 + EXPOSE 8080 58 + 59 + # Run the application 60 + ENTRYPOINT ["/app/blahg"]
+9
LICENSE
···
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Nick Gerakines 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 + 7 + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+205
README.md
···
··· 1 + # Blahg 2 + 3 + A Rust-based ATProtocol AppView for rendering personal blog content from the ATProtocol network. 4 + 5 + ## Overview 6 + 7 + Blahg is a high-performance blog engine that consumes ATProtocol records to create a traditional blog-like reading experience. It processes both custom blog post records (`tools.smokesignal.blahg.content.post`) and standard ATProtocol social media posts (`app.bsky.feed.post`), providing a unified interface for content discovery and presentation. 8 + 9 + ## Features 10 + 11 + ### Core Functionality 12 + - **Real-time Event Processing**: Consumes ATProtocol Jetstream events for live content updates 13 + - **Multi-Format Content Support**: Handles markdown, HTML, and plain text content 14 + - **Custom Lexicon Support**: Implements `tools.smokesignal.blahg.content.post` schema for rich blog posts 15 + - **Post Import Tool**: Command-line utility for importing individual posts via AT-URI 16 + - **Identity Resolution**: Automatic DID and handle resolution with caching 17 + - **Post Interactions**: Tracks likes, reposts, and other engagement metrics 18 + 19 + ### Storage & Infrastructure 20 + - **Multi-Database Support**: Both PostgreSQL and SQLite backends supported 21 + - **Flexible Content Storage**: Filesystem and S3-compatible object storage 22 + - **Efficient Caching**: LRU caching for posts, identities, and content 23 + - **Template Engine**: Minijinja-based templating with hot-reload support during development 24 + - **Markdown Rendering**: Comrak-powered markdown with syntax highlighting 25 + 26 + ### Web Interface 27 + - **Responsive Design**: Clean, accessible blog interface with Pico.css 28 + - **SEO Optimized**: Proper meta tags, OpenGraph support, and canonical URLs 29 + - **Post Interactions**: Display engagement metrics and interaction history 30 + - **Static Asset Serving**: Efficient static file serving with proper caching 31 + 32 + ## Architecture 33 + 34 + Blahg consists of two main components: 35 + 36 + 1. **blahg**: The main server application that runs the web interface and processes real-time events 37 + 2. **blahg-import**: A command-line tool for importing individual posts from AT-URIs 38 + 39 + ### Data Flow 40 + 1. ATProtocol events are consumed via Jetstream 41 + 2. Relevant records are filtered and processed 42 + 3. Post content and attachments are stored 43 + 4. Web interface serves rendered content with caching 44 + 45 + ## Dependencies 46 + 47 + This project uses the following atproto crates: 48 + - `atproto-identity` - Identity resolution and verification 49 + - `atproto-record` - Record handling and parsing 50 + - `atproto-client` - ATProtocol client functionality 51 + - `atproto-jetstream` - Real-time event streaming 52 + 53 + ## Installation 54 + 55 + ### Prerequisites 56 + - Rust 1.87 or higher 57 + - Cargo 58 + - PostgreSQL or SQLite (depending on your database choice) 59 + 60 + ### Building from Source 61 + 62 + ```bash 63 + git clone https://github.com/yourusername/blahg.git 64 + cd blahg 65 + cargo build --release 66 + ``` 67 + 68 + ### Docker 69 + 70 + ```bash 71 + docker build -t blahg . 72 + docker run -p 8080:8080 blahg 73 + ``` 74 + 75 + ## Usage 76 + 77 + ### Running the Server 78 + 79 + ```bash 80 + # Run with default configuration 81 + ./target/release/blahg 82 + 83 + # Run with custom configuration via environment variables 84 + BLAHG_AUTHOR="did:plc:example" \ 85 + BLAHG_DATABASE_URL="postgresql://user:pass@localhost/blahg" \ 86 + BLAHG_ATTACHMENT_STORAGE="s3://your-bucket/attachments/" \ 87 + ./target/release/blahg 88 + ``` 89 + 90 + ### Importing Posts 91 + 92 + ```bash 93 + # Import a single post 94 + ./target/release/blahg-import at://did:plc:example/tools.smokesignal.blahg.content.post/abc123 95 + 96 + # Import multiple posts 97 + ./target/release/blahg-import \ 98 + at://did:plc:example/tools.smokesignal.blahg.content.post/abc123 \ 99 + at://did:plc:example/tools.smokesignal.blahg.content.post/def456 100 + ``` 101 + 102 + ## Configuration 103 + 104 + Configuration is managed via environment variables: 105 + 106 + ### Core Settings 107 + - `BLAHG_AUTHOR`: Your ATProtocol DID (required) 108 + - `BLAHG_EXTERNAL_BASE`: Base URL for your blog (default: `http://localhost:8080`) 109 + - `BLAHG_HTTP_PORT`: HTTP server port (default: `8080`) 110 + 111 + ### Database 112 + - `BLAHG_DATABASE_URL`: Database connection string 113 + - SQLite: `sqlite://blahg.db` 114 + - PostgreSQL: `postgresql://user:pass@localhost/blahg` 115 + 116 + ### Storage 117 + - `BLAHG_ATTACHMENT_STORAGE`: Content storage location 118 + - Filesystem: `./attachments` 119 + - S3: `s3://bucket/prefix/` 120 + 121 + ### ATProtocol 122 + - `BLAHG_ENABLE_JETSTREAM`: Enable real-time event processing (default: `true`) 123 + - `BLAHG_JETSTREAM_CURSOR_PATH`: Path to store Jetstream cursor (optional) 124 + - `BLAHG_PLC_HOSTNAME`: PLC directory hostname (default: `plc.directory`) 125 + 126 + ### HTTP Client 127 + - `BLAHG_USER_AGENT`: HTTP client user agent 128 + - `BLAHG_HTTP_CLIENT_TIMEOUT`: HTTP client timeout (default: `10s`) 129 + - `BLAHG_CERTIFICATE_BUNDLES`: Additional CA certificates (comma-separated paths) 130 + 131 + ## Development 132 + 133 + ### Feature Flags 134 + 135 + Blahg supports several feature flags for different deployment scenarios: 136 + 137 + - `sqlite`: Enable SQLite database support (default: enabled) 138 + - `postgres`: Enable PostgreSQL database support (default: enabled) 139 + - `s3`: Enable S3-compatible object storage (default: enabled) 140 + - `embed`: Embed templates in binary for production (default: disabled) 141 + - `reload`: Enable template hot-reloading for development (default: enabled) 142 + 143 + ### Development Environment 144 + 145 + ```bash 146 + # Run in development mode with hot-reloading 147 + cargo run 148 + 149 + # Run with specific features 150 + cargo run --no-default-features --features "sqlite,reload" 151 + ``` 152 + 153 + ### Running Tests 154 + ```bash 155 + cargo test 156 + ``` 157 + 158 + ### Linting and Type Checking 159 + ```bash 160 + cargo clippy 161 + cargo check 162 + ``` 163 + 164 + ### Building for Production 165 + 166 + ```bash 167 + # Build with embedded templates 168 + cargo build --release --no-default-features --features "embed,postgres,s3" 169 + ``` 170 + 171 + ## API Endpoints 172 + 173 + Blahg provides a simple web interface with the following endpoints: 174 + 175 + - `GET /`: Homepage with list of all posts 176 + - `GET /posts/{slug}`: Individual post page 177 + - `GET /posts/{slug}/{collection}`: Post interaction references (likes, reposts, etc.) 178 + - `GET /static/*`: Static asset serving (CSS, JS, images) 179 + 180 + ## ATProtocol Lexicon 181 + 182 + Blahg implements the `tools.smokesignal.blahg.content.post` lexicon for rich blog posts: 183 + 184 + ```json 185 + { 186 + "title": "string", // Post title (max 200 graphemes) 187 + "content": "blob", // Post content (markdown/HTML/text, max 1MB) 188 + "publishedAt": "datetime", // Publication timestamp 189 + "langs": ["string"], // Language codes (max 3) 190 + "attachments": [ // Optional image attachments 191 + { 192 + "content": "blob", // Image blob (max 3MB) 193 + "alt": "string" // Alt text for accessibility 194 + } 195 + ] 196 + } 197 + ``` 198 + 199 + ## Contributing 200 + 201 + Contributions are welcome! Please feel free to submit a Pull Request. 202 + 203 + ## License 204 + 205 + Blahg is open source software released under the [MIT License](LICENSE).
+22
build.rs
···
··· 1 + fn main() { 2 + #[cfg(all(feature = "embed", feature = "reload"))] 3 + compile_error!("feature \"embed\" and feature \"reload\" cannot be enabled at the same time"); 4 + 5 + #[cfg(not(any(feature = "sqlite", feature = "postgres")))] 6 + compile_error!("one of feature \"sqlite\" or feature \"postgres\" must be enabled"); 7 + 8 + #[cfg(feature = "embed")] 9 + { 10 + use std::env; 11 + use std::path::PathBuf; 12 + let template_path = if let Ok(value) = env::var("HTTP_TEMPLATE_PATH") { 13 + value.to_string() 14 + } else { 15 + PathBuf::from(env!("CARGO_MANIFEST_DIR")) 16 + .join("templates") 17 + .display() 18 + .to_string() 19 + }; 20 + minijinja_embed::embed_templates!(&template_path); 21 + } 22 + }
+262
src/bin/blahg-import.rs
···
··· 1 + use atproto_client::com::atproto::repo::get_blob; 2 + use atproto_client::com::atproto::repo::get_record; 3 + use atproto_identity::resolve::IdentityResolver; 4 + use atproto_identity::resolve::InnerIdentityResolver; 5 + use atproto_identity::resolve::create_resolver; 6 + use atproto_record::aturi::ATURI; 7 + use blahg::identity::CachingIdentityResolver; 8 + use chrono::Utc; 9 + use slugify::slugify; 10 + #[cfg(feature = "sqlite")] 11 + use sqlx::SqlitePool; 12 + #[cfg(feature = "postgres")] 13 + use sqlx::postgres::PgPool; 14 + use std::str::FromStr; 15 + use std::{env, sync::Arc}; 16 + use tracing::{error, info}; 17 + use tracing_subscriber::prelude::*; 18 + 19 + use blahg::errors::Result; 20 + use blahg::lexicon::PostRecord; 21 + use blahg::storage::Post; 22 + #[cfg(feature = "s3")] 23 + use blahg::storage::content::S3FileStorage; 24 + #[cfg(feature = "s3")] 25 + use blahg::storage::content::parse_s3_url; 26 + #[cfg(feature = "postgres")] 27 + use blahg::storage::postgres::PostgresStorage; 28 + #[cfg(feature = "sqlite")] 29 + use blahg::storage::sqlite::SqliteStorage; 30 + use blahg::{ 31 + config::Config, 32 + storage::content::FilesystemContentStorage, 33 + storage::{ContentStorage, Storage}, 34 + }; 35 + 36 + async fn create_content_storage(storage_config: &str) -> Result<Arc<dyn ContentStorage>> { 37 + if storage_config.starts_with("s3://") { 38 + #[cfg(feature = "s3")] 39 + { 40 + tracing::warn!("S3 content storage used"); 41 + 42 + let (endpoint, access_key, secret_key, bucket, prefix) = parse_s3_url(storage_config)?; 43 + let s3_storage = S3FileStorage::new(endpoint, access_key, secret_key, bucket, prefix)?; 44 + Ok(Arc::new(s3_storage)) 45 + } 46 + #[cfg(not(feature = "s3"))] 47 + { 48 + Err(blahg::errors::BlahgError::ConfigFeatureNotEnabled { 49 + feature: "S3 storage requested but s3 feature is not enabled".to_string(), 50 + }) 51 + } 52 + } else { 53 + tracing::warn!("Filesystem content storage used"); 54 + let storage = FilesystemContentStorage::new(storage_config).await?; 55 + Ok(Arc::new(storage)) 56 + } 57 + } 58 + 59 + async fn import_record( 60 + http_client: &reqwest::Client, 61 + identity_resolver: &CachingIdentityResolver<dyn Storage>, 62 + at_uri: &str, 63 + storage: Arc<dyn Storage>, 64 + content_storage: Arc<dyn ContentStorage>, 65 + ) -> Result<()> { 66 + info!("Importing record from AT-URI: {}", at_uri); 67 + 68 + // Parse the AT-URI 69 + let aturi = ATURI::from_str(at_uri)?; 70 + info!( 71 + "Parsed AT-URI - repo: {}, collection: {}, rkey: {}", 72 + aturi.authority, aturi.collection, aturi.record_key 73 + ); 74 + 75 + // Resolve identity to find PDS 76 + let identity = identity_resolver.resolve(&aturi.authority).await?; 77 + 78 + let pds_endpoint = identity.pds_endpoints().first().cloned().ok_or_else(|| { 79 + blahg::errors::BlahgError::ProcessIdentityResolutionFailed { 80 + did: aturi.authority.clone(), 81 + details: "No PDS endpoint found for identity".to_string(), 82 + } 83 + })?; 84 + 85 + info!("Resolved PDS endpoint: {}", pds_endpoint); 86 + 87 + // Get the record 88 + let record_response = get_record( 89 + http_client, 90 + None, 91 + pds_endpoint, 92 + &aturi.authority, 93 + &aturi.collection, 94 + &aturi.record_key, 95 + None, // cid 96 + ) 97 + .await?; 98 + 99 + info!("Retrieved record successfully"); 100 + 101 + let (raw_post_record, uri, cid) = match record_response { 102 + atproto_client::com::atproto::repo::GetRecordResponse::Record { 103 + uri, cid, value, .. 104 + } => (value, uri, cid), 105 + atproto_client::com::atproto::repo::GetRecordResponse::Error(simple_error) => { 106 + error!("nope: {}", simple_error.error_message()); 107 + return Ok(()); 108 + } 109 + }; 110 + 111 + let post_record = serde_json::from_value::<PostRecord>(raw_post_record.clone())?; 112 + 113 + let content_cid = &post_record.content.r#ref.link; 114 + 115 + let slug = format!("{}-{}", aturi.record_key, slugify!(&post_record.title)); 116 + 117 + let now = Utc::now(); 118 + 119 + let post = Post { 120 + aturi: uri, 121 + cid, 122 + title: post_record.title.clone(), 123 + slug, 124 + content: content_cid.clone(), 125 + record_key: aturi.record_key.clone(), 126 + created_at: now, 127 + updated_at: now, 128 + record: raw_post_record, 129 + }; 130 + storage.upsert_post(&post).await?; 131 + 132 + let content_blob = get_blob(http_client, pds_endpoint, &aturi.authority, content_cid).await?; 133 + 134 + content_storage 135 + .write_content(content_cid, &content_blob) 136 + .await?; 137 + 138 + for attachment in post_record.attachments { 139 + let attachment_cid = attachment.content.r#ref.link; 140 + let attachment_blob = get_blob( 141 + http_client, 142 + pds_endpoint, 143 + &aturi.authority, 144 + &attachment_cid, 145 + ) 146 + .await?; 147 + 148 + content_storage 149 + .write_content(&attachment_cid, &attachment_blob) 150 + .await?; 151 + } 152 + 153 + Ok(()) 154 + } 155 + 156 + #[tokio::main] 157 + async fn main() -> Result<()> { 158 + // Initialize logging 159 + tracing_subscriber::registry() 160 + .with(tracing_subscriber::EnvFilter::new( 161 + std::env::var("RUST_LOG").unwrap_or_else(|_| "blahg=info,info".into()), 162 + )) 163 + .with(tracing_subscriber::fmt::layer().pretty()) 164 + .init(); 165 + 166 + // Get command line arguments (skip program name) 167 + let args: Vec<String> = env::args().skip(1).collect(); 168 + 169 + if args.is_empty() { 170 + eprintln!("Usage: blagh-import <AT-URI> [<AT-URI> ...]"); 171 + eprintln!( 172 + "Example: blagh-import at://did:plc:example/tools.smokesignal.blahg.content.post/123" 173 + ); 174 + std::process::exit(1); 175 + } 176 + 177 + // Load configuration 178 + let config = Arc::new(Config::from_env()?); 179 + info!("Loaded configuration for import"); 180 + 181 + let mut client_builder = reqwest::Client::builder(); 182 + for ca_certificate in &config.certificate_bundles { 183 + tracing::info!("Loading CA certificate: {:?}", ca_certificate); 184 + let cert = std::fs::read(ca_certificate)?; 185 + let cert = reqwest::Certificate::from_pem(&cert)?; 186 + client_builder = client_builder.add_root_certificate(cert); 187 + } 188 + 189 + client_builder = client_builder 190 + .user_agent(config.user_agent.clone()) 191 + .timeout(config.http_client_timeout); 192 + let http_client: reqwest::Client = client_builder.build()?; 193 + 194 + let dns_resolver = create_resolver(&config.dns_nameservers); 195 + 196 + let base_identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver { 197 + dns_resolver, 198 + http_client: http_client.clone(), 199 + plc_hostname: config.plc_hostname.clone(), 200 + })); 201 + 202 + // Setup database based on the database URL and available features 203 + let storage: Arc<dyn Storage> = { 204 + #[cfg(all(feature = "sqlite", not(feature = "postgres")))] 205 + { 206 + let pool = SqlitePool::connect(&config.database_url).await?; 207 + Arc::new(SqliteStorage::new(pool)) 208 + } 209 + 210 + #[cfg(all(feature = "postgres", not(feature = "sqlite")))] 211 + { 212 + let pool = PgPool::connect(&config.database_url).await?; 213 + Arc::new(PostgresStorage::new(pool)) 214 + } 215 + 216 + #[cfg(all(feature = "sqlite", feature = "postgres"))] 217 + { 218 + if config.database_url.starts_with("postgres://") 219 + || config.database_url.starts_with("postgresql://") 220 + { 221 + let pool = PgPool::connect(&config.database_url).await?; 222 + Arc::new(PostgresStorage::new(pool)) 223 + } else { 224 + let pool = SqlitePool::connect(&config.database_url).await?; 225 + Arc::new(SqliteStorage::new(pool)) 226 + } 227 + } 228 + }; 229 + 230 + // Run migrations 231 + storage.migrate().await?; 232 + info!("Database migrations completed"); 233 + 234 + // Create caching identity resolver 235 + let identity_resolver = CachingIdentityResolver::new(base_identity_resolver, storage.clone()); 236 + 237 + // Setup content storage 238 + let content_storage = create_content_storage(&config.attachment_storage).await?; 239 + 240 + // Process each AT-URI 241 + for at_uri in args { 242 + match import_record( 243 + &http_client, 244 + &identity_resolver, 245 + &at_uri, 246 + storage.clone(), 247 + content_storage.clone(), 248 + ) 249 + .await 250 + { 251 + Ok(()) => { 252 + info!("Successfully imported record from: {}", at_uri); 253 + } 254 + Err(e) => { 255 + error!("Failed to import record from {}: {}", at_uri, e); 256 + } 257 + } 258 + } 259 + 260 + info!("Import process completed"); 261 + Ok(()) 262 + }
+392
src/bin/blahg.rs
···
··· 1 + use atproto_identity::resolve::IdentityResolver; 2 + use atproto_identity::resolve::InnerIdentityResolver; 3 + use atproto_identity::resolve::create_resolver; 4 + use atproto_jetstream::{CancellationToken, Consumer as JetstreamConsumer, ConsumerTaskConfig}; 5 + use blahg::consumer::Consumer; 6 + use blahg::errors::Result; 7 + use blahg::http::AppEngine; 8 + use blahg::identity::CachingIdentityResolver; 9 + use blahg::process::EventProcessor; 10 + #[cfg(feature = "s3")] 11 + use blahg::storage::content::S3FileStorage; 12 + #[cfg(feature = "s3")] 13 + use blahg::storage::content::parse_s3_url; 14 + #[cfg(feature = "postgres")] 15 + use blahg::storage::postgres::PostgresStorage; 16 + #[cfg(feature = "sqlite")] 17 + use blahg::storage::sqlite::SqliteStorage; 18 + use blahg::{ 19 + config::Config, 20 + http::{AppState, create_router}, 21 + render::{ComrakRenderManager, RenderManager}, 22 + storage::content::FilesystemContentStorage, 23 + storage::{CachedContentStorage, CachedPostStorage, ContentStorage, Storage}, 24 + }; 25 + #[cfg(feature = "sqlite")] 26 + use sqlx::SqlitePool; 27 + #[cfg(feature = "postgres")] 28 + use sqlx::postgres::PgPool; 29 + use std::{env, sync::Arc}; 30 + use tokio::net::TcpListener; 31 + use tokio::signal; 32 + use tokio_util::task::TaskTracker; 33 + use tracing_subscriber::prelude::*; 34 + 35 + #[cfg(feature = "embed")] 36 + use blahg::templates::build_env; 37 + 38 + #[cfg(feature = "reload")] 39 + use blahg::templates::build_env; 40 + 41 + #[tokio::main] 42 + async fn main() -> Result<()> { 43 + // Initialize logging 44 + tracing_subscriber::registry() 45 + .with(tracing_subscriber::EnvFilter::new( 46 + std::env::var("RUST_LOG").unwrap_or_else(|_| "blahg=info,info".into()), 47 + )) 48 + .with(tracing_subscriber::fmt::layer().pretty()) 49 + .init(); 50 + 51 + // Handle version flag 52 + env::args().for_each(|arg| { 53 + if arg == "--version" { 54 + println!("blahg {}", env!("CARGO_PKG_VERSION")); 55 + std::process::exit(0); 56 + } 57 + }); 58 + 59 + // Load configuration 60 + let config = Arc::new(Config::from_env()?); 61 + tracing::info!("Starting Blahg with config"); 62 + 63 + let dns_resolver = create_resolver(&config.dns_nameservers); 64 + let http_client = reqwest::Client::builder() 65 + .user_agent(&config.user_agent) 66 + .timeout(config.http_client_timeout) 67 + .build()?; 68 + 69 + let base_identity_resolver = IdentityResolver(Arc::new(InnerIdentityResolver { 70 + dns_resolver, 71 + http_client: http_client.clone(), 72 + plc_hostname: config.plc_hostname.clone(), 73 + })); 74 + 75 + // Setup database based on the database URL and available features 76 + let storage: Arc<dyn Storage> = { 77 + #[cfg(all(feature = "sqlite", not(feature = "postgres")))] 78 + { 79 + let pool = SqlitePool::connect(&config.database_url).await?; 80 + let base_storage = Arc::new(SqliteStorage::new(pool)); 81 + // Wrap with caching - note: this only caches PostStorage operations 82 + Arc::new(CachedPostStorage::new(base_storage)) 83 + } 84 + 85 + #[cfg(all(feature = "postgres", not(feature = "sqlite")))] 86 + { 87 + let pool = PgPool::connect(&config.database_url).await?; 88 + let base_storage = Arc::new(PostgresStorage::new(pool)); 89 + // Wrap with caching - note: this only caches PostStorage operations 90 + Arc::new(CachedPostStorage::new(base_storage)) 91 + } 92 + 93 + #[cfg(all(feature = "sqlite", feature = "postgres"))] 94 + { 95 + // When both features are enabled, determine based on the database URL 96 + if config.database_url.starts_with("postgres://") 97 + || config.database_url.starts_with("postgresql://") 98 + { 99 + let pool = PgPool::connect(&config.database_url).await?; 100 + let base_storage = Arc::new(PostgresStorage::new(pool)); 101 + // Wrap with caching - note: this only caches PostStorage operations 102 + Arc::new(CachedPostStorage::new(base_storage)) 103 + } else { 104 + let pool = SqlitePool::connect(&config.database_url).await?; 105 + let base_storage = Arc::new(SqliteStorage::new(pool)); 106 + // Wrap with caching - note: this only caches PostStorage operations 107 + Arc::new(CachedPostStorage::new(base_storage)) 108 + } 109 + } 110 + }; 111 + 112 + // Run migrations 113 + storage.migrate().await?; 114 + 115 + // Create caching identity resolver 116 + let identity_resolver = CachingIdentityResolver::new(base_identity_resolver, storage.clone()); 117 + 118 + { 119 + identity_resolver 120 + .resolve(&config.author) 121 + .await 122 + .expect("this better work"); 123 + } 124 + 125 + // Setup content storage for markdown files 126 + let content_storage: Arc<dyn ContentStorage> = if config.attachment_storage.starts_with("s3://") 127 + { 128 + #[cfg(feature = "s3")] 129 + { 130 + let (endpoint, access_key, secret_key, bucket, prefix) = 131 + parse_s3_url(&config.attachment_storage)?; 132 + let s3_storage = Arc::new(S3FileStorage::new( 133 + endpoint, access_key, secret_key, bucket, prefix, 134 + )?); 135 + Arc::new(CachedContentStorage::new(s3_storage)) 136 + } 137 + #[cfg(not(feature = "s3"))] 138 + { 139 + return Err(blahg::errors::BlahgError::ConfigFeatureNotEnabled { 140 + feature: "S3 storage requested but s3 feature is not enabled".to_string(), 141 + }); 142 + } 143 + } else { 144 + let filesystem_storage = 145 + Arc::new(FilesystemContentStorage::new(&config.attachment_storage).await?); 146 + Arc::new(CachedContentStorage::new(filesystem_storage)) 147 + }; 148 + 149 + // Setup markdown render manager 150 + let render_manager: Arc<dyn RenderManager> = Arc::new(ComrakRenderManager::new(&config.external_base)); 151 + 152 + // Setup template engine 153 + let template_env = { 154 + #[cfg(feature = "embed")] 155 + { 156 + AppEngine::from(build_env( 157 + config.external_base.clone(), 158 + env!("CARGO_PKG_VERSION").to_string(), 159 + )) 160 + } 161 + 162 + #[cfg(feature = "reload")] 163 + { 164 + AppEngine::from(build_env(config.external_base.clone())) 165 + } 166 + 167 + #[cfg(not(any(feature = "reload", feature = "embed")))] 168 + { 169 + use minijinja::Environment; 170 + let mut env = Environment::new(); 171 + // Add basic templates for the minimal case 172 + env.add_template( 173 + "index.html", 174 + "<!DOCTYPE html><html><head><title>{{ title }}</title></head><body><h1>{{ title }}</h1><ul>{% for post in posts %}<li><a href=\"/posts/{{ post.slug }}\">{{ post.title }}</a></li>{% endfor %}</ul></body></html>", 175 + ) 176 + .unwrap(); 177 + env.add_template( 178 + "post.html", 179 + "<!DOCTYPE html><html><head><title>{{ post.title }}</title></head><body><h1>{{ post.title }}</h1><div>{{ post_content }}</div></body></html>", 180 + ) 181 + .unwrap(); 182 + AppEngine::from(env) 183 + } 184 + }; 185 + 186 + // Create application state for HTTP server 187 + let app_state = AppState::new( 188 + storage.clone(), 189 + content_storage.clone(), 190 + render_manager, 191 + config.clone(), 192 + template_env, 193 + ); 194 + 195 + // Create HTTP router 196 + let app = create_router(app_state); 197 + 198 + // Setup task tracking for graceful shutdown 199 + let tracker = TaskTracker::new(); 200 + let token = CancellationToken::new(); 201 + 202 + // Setup signal handling 203 + { 204 + let tracker = tracker.clone(); 205 + let inner_token = token.clone(); 206 + 207 + let ctrl_c = async { 208 + signal::ctrl_c() 209 + .await 210 + .expect("failed to install Ctrl+C handler"); 211 + }; 212 + 213 + let terminate = async { 214 + signal::unix::signal(signal::unix::SignalKind::terminate()) 215 + .expect("failed to install signal handler") 216 + .recv() 217 + .await; 218 + }; 219 + 220 + tokio::spawn(async move { 221 + tokio::select! { 222 + () = inner_token.cancelled() => { }, 223 + _ = terminate => {}, 224 + _ = ctrl_c => {}, 225 + } 226 + 227 + tracker.close(); 228 + inner_token.cancel(); 229 + }); 230 + } 231 + 232 + // Only start jetstream consumer and processor if enabled 233 + if config.enable_jetstream { 234 + let consumer = Consumer {}; 235 + let (blahg_handler, event_receiver) = consumer.create_blahg_handler(); 236 + 237 + let processor = EventProcessor::new( 238 + storage.clone(), 239 + content_storage, 240 + config.clone(), 241 + identity_resolver, 242 + http_client, 243 + ); 244 + 245 + let inner_token = token.clone(); 246 + tracker.spawn(async move { 247 + tokio::select! { 248 + result = processor.start_processing(event_receiver) => { 249 + if let Err(err) = result { 250 + tracing::error!("Event processor failed: {}", err); 251 + } 252 + } 253 + () = inner_token.cancelled() => { 254 + tracing::info!("Badge processor cancelled"); 255 + } 256 + } 257 + }); 258 + 259 + // Start Jetstream consumer with reconnect logic 260 + let inner_token = token.clone(); 261 + let inner_config = config.clone(); 262 + tracker.spawn(async move { 263 + let mut disconnect_times = Vec::new(); 264 + let disconnect_window = std::time::Duration::from_secs(60); // 1 minute window 265 + let max_disconnects_per_minute = 1; 266 + let reconnect_delay = std::time::Duration::from_secs(5); 267 + 268 + loop { 269 + // Create new consumer for each connection attempt 270 + let jetstream_config = ConsumerTaskConfig { 271 + user_agent: inner_config.user_agent.clone(), 272 + compression: false, 273 + zstd_dictionary_location: String::new(), 274 + jetstream_hostname: "jetstream2.us-east.bsky.network".to_string(), 275 + collections: vec![ 276 + "tools.smokesignal.blahg.content.*".to_string(), 277 + "app.bsky.feed.*".to_string(), 278 + "app.bsky.feed.like".to_string(), 279 + "community.lexicon.interaction.*".to_string(), 280 + ], 281 + dids: vec![], 282 + max_message_size_bytes: Some(10 * 1024 * 1024), // 10MB 283 + cursor: None, 284 + require_hello: true, 285 + }; 286 + 287 + let jetstream_consumer = JetstreamConsumer::new(jetstream_config); 288 + 289 + // Register badge handler 290 + if let Err(err) = jetstream_consumer.register_handler(blahg_handler.clone()).await { 291 + tracing::error!("Failed to register processing handler: {}", err); 292 + inner_token.cancel(); 293 + break; 294 + } 295 + 296 + // Register cursor writer if configured 297 + if let Some(cursor_path) = inner_config.jetstream_cursor_path.clone() { 298 + let cursor_writer = consumer.create_cursor_writer_handler(cursor_path); 299 + if let Err(err) = jetstream_consumer.register_handler(cursor_writer).await { 300 + tracing::error!("Failed to register cursor writer: {}", err); 301 + inner_token.cancel(); 302 + break; 303 + } 304 + } 305 + 306 + tokio::select! { 307 + result = jetstream_consumer.run_background(inner_token.clone()) => { 308 + if let Err(err) = result { 309 + let now = std::time::Instant::now(); 310 + disconnect_times.push(now); 311 + 312 + // Remove disconnect times older than the window 313 + disconnect_times.retain(|&t| now.duration_since(t) <= disconnect_window); 314 + 315 + if disconnect_times.len() > max_disconnects_per_minute { 316 + let rate_error = blahg::errors::BlahgError::ConsumerDisconnectRateExceeded { 317 + disconnect_count: disconnect_times.len(), 318 + duration_mins: 1, 319 + }; 320 + tracing::error!(?rate_error, "Jetstream disconnect rate exceeded, exiting"); 321 + inner_token.cancel(); 322 + break; 323 + } 324 + 325 + let disconnect_error = blahg::errors::BlahgError::ConsumerDisconnected { 326 + details: err.to_string(), 327 + }; 328 + tracing::error!(?disconnect_error, "Jetstream disconnected, reconnecting in {:?}", reconnect_delay); 329 + 330 + // Wait before reconnecting 331 + tokio::select! { 332 + () = tokio::time::sleep(reconnect_delay) => {}, 333 + () = inner_token.cancelled() => { 334 + tracing::info!("Jetstream consumer cancelled during reconnect delay"); 335 + break; 336 + } 337 + } 338 + 339 + // Continue the loop to reconnect 340 + continue; 341 + } 342 + } 343 + () = inner_token.cancelled() => { 344 + tracing::info!("Jetstream consumer cancelled"); 345 + break; 346 + } 347 + } 348 + 349 + // If we reach here, the consumer exited without error (unlikely) 350 + tracing::info!("Jetstream consumer exited normally"); 351 + break; 352 + } 353 + }); 354 + } else { 355 + tracing::info!("Jetstream consumer and processor disabled by configuration"); 356 + } 357 + 358 + // Start HTTP server 359 + { 360 + let inner_config = config.clone(); 361 + let http_port = inner_config.http.port; 362 + let inner_token = token.clone(); 363 + tracker.spawn(async move { 364 + let bind_address = format!("0.0.0.0:{http_port}"); 365 + tracing::info!("bind_address {bind_address}"); 366 + let listener = TcpListener::bind(&bind_address).await.unwrap(); 367 + 368 + let shutdown_token = inner_token.clone(); 369 + let result = axum::serve(listener, app) 370 + .with_graceful_shutdown(async move { 371 + tokio::select! { 372 + () = shutdown_token.cancelled() => { } 373 + } 374 + tracing::info!("axum graceful shutdown complete"); 375 + }) 376 + .await; 377 + if let Err(err) = result { 378 + tracing::error!("axum task failed: {}", err); 379 + } 380 + 381 + inner_token.cancel(); 382 + }); 383 + } 384 + 385 + tracing::info!("Blahg started successfully"); 386 + 387 + // Wait for all tasks to complete 388 + tracker.wait().await; 389 + 390 + tracing::info!("Blahg shutting down"); 391 + Ok(()) 392 + }
+143
src/config.rs
···
··· 1 + use serde::{Deserialize, Serialize}; 2 + use std::time::Duration; 3 + 4 + use crate::errors::{BlahgError, Result}; 5 + 6 + /// Application configuration loaded from environment variables. 7 + #[derive(Debug, Clone, Serialize, Deserialize)] 8 + pub struct Config { 9 + pub http: HttpConfig, 10 + pub external_base: String, 11 + pub certificate_bundles: Vec<String>, 12 + pub user_agent: String, 13 + pub plc_hostname: String, 14 + pub dns_nameservers: Vec<std::net::IpAddr>, 15 + pub http_client_timeout: Duration, 16 + pub author: String, 17 + pub attachment_storage: String, 18 + pub database_url: String, 19 + pub jetstream_cursor_path: Option<String>, 20 + pub enable_jetstream: bool, 21 + } 22 + 23 + #[derive(Debug, Clone, Serialize, Deserialize)] 24 + pub struct HttpConfig { 25 + pub port: u16, 26 + pub static_path: String, 27 + pub templates_path: String, 28 + } 29 + 30 + impl Default for Config { 31 + fn default() -> Self { 32 + Self { 33 + http: HttpConfig::default(), 34 + external_base: "http://localhost:8080".to_string(), 35 + certificate_bundles: Vec::new(), 36 + user_agent: format!( 37 + "Blahg/{} (+https://tangled.sh/@smokesignal.events/blahg)", 38 + env!("CARGO_PKG_VERSION") 39 + ), 40 + plc_hostname: "plc.directory".to_string(), 41 + dns_nameservers: Vec::new(), 42 + http_client_timeout: Duration::from_secs(10), 43 + author: String::new(), 44 + attachment_storage: "./attachments".to_string(), 45 + database_url: "sqlite://blahg.db".to_string(), 46 + jetstream_cursor_path: None, 47 + enable_jetstream: true, 48 + } 49 + } 50 + } 51 + 52 + impl Default for HttpConfig { 53 + fn default() -> Self { 54 + Self { 55 + port: 8080, 56 + static_path: format!("{}/static", env!("CARGO_MANIFEST_DIR")), 57 + templates_path: format!("{}/templates", env!("CARGO_MANIFEST_DIR")), 58 + } 59 + } 60 + } 61 + 62 + impl Config { 63 + /// Create configuration from environment variables. 64 + pub fn from_env() -> Result<Self> { 65 + let mut config = Self::default(); 66 + 67 + if let Ok(port) = std::env::var("HTTP_PORT") { 68 + config.http.port = port 69 + .parse() 70 + .map_err(|_| BlahgError::ConfigHttpPortInvalid { port: port.clone() })?; 71 + } 72 + 73 + if let Ok(static_path) = std::env::var("HTTP_STATIC_PATH") { 74 + config.http.static_path = static_path; 75 + } 76 + 77 + if let Ok(templates_path) = std::env::var("HTTP_TEMPLATES_PATH") { 78 + config.http.templates_path = templates_path; 79 + } 80 + 81 + if let Ok(external_base) = std::env::var("EXTERNAL_BASE") { 82 + config.external_base = external_base; 83 + } 84 + 85 + if let Ok(cert_bundles) = std::env::var("CERTIFICATE_BUNDLES") { 86 + config.certificate_bundles = cert_bundles 87 + .split(';') 88 + .map(|s| s.trim().to_string()) 89 + .filter(|s| !s.is_empty()) 90 + .collect(); 91 + } 92 + 93 + if let Ok(user_agent) = std::env::var("USER_AGENT") { 94 + config.user_agent = user_agent; 95 + } 96 + 97 + if let Ok(plc_hostname) = std::env::var("PLC_HOSTNAME") { 98 + config.plc_hostname = plc_hostname; 99 + } 100 + 101 + if let Ok(dns_nameservers) = std::env::var("DNS_NAMESERVERS") { 102 + config.dns_nameservers = dns_nameservers 103 + .split(',') 104 + .map(|s| s.trim()) 105 + .filter(|s| !s.is_empty()) 106 + .map(|s| { 107 + s.parse::<std::net::IpAddr>() 108 + .map_err(|e| BlahgError::NameserverParsingFailed(s.to_string(), e)) 109 + }) 110 + .collect::<Result<Vec<std::net::IpAddr>>>()?; 111 + } 112 + 113 + if let Ok(timeout) = std::env::var("HTTP_CLIENT_TIMEOUT") { 114 + config.http_client_timeout = duration_str::parse(&timeout).map_err(|e| { 115 + BlahgError::ConfigHttpTimeoutInvalid { 116 + details: e.to_string(), 117 + } 118 + })?; 119 + } 120 + 121 + if let Ok(author) = std::env::var("AUTHOR") { 122 + config.author = author; 123 + } 124 + 125 + if let Ok(attachment_storage) = std::env::var("ATTACHMENT_STORAGE") { 126 + config.attachment_storage = attachment_storage; 127 + } 128 + 129 + if let Ok(database_url) = std::env::var("DATABASE_URL") { 130 + config.database_url = database_url; 131 + } 132 + 133 + if let Ok(jetstream_cursor_path) = std::env::var("JETSTREAM_CURSOR_PATH") { 134 + config.jetstream_cursor_path = Some(jetstream_cursor_path); 135 + } 136 + 137 + if let Ok(enable_jetstream) = std::env::var("ENABLE_JETSTREAM") { 138 + config.enable_jetstream = enable_jetstream.parse().unwrap_or(true); 139 + } 140 + 141 + Ok(config) 142 + } 143 + }
+176
src/consumer.rs
···
··· 1 + use std::sync::Arc; 2 + use std::sync::atomic::{AtomicU64, Ordering}; 3 + use std::time::{Duration, Instant}; 4 + 5 + use crate::errors::BlahgError; 6 + use anyhow::Result; 7 + use async_trait::async_trait; 8 + use atproto_jetstream::{EventHandler, JetstreamEvent}; 9 + use tokio::sync::Mutex; 10 + use tokio::sync::mpsc; 11 + 12 + pub type BlahgEventReceiver = mpsc::UnboundedReceiver<BlahgEvent>; 13 + 14 + #[derive(Debug, Clone)] 15 + pub enum BlahgEvent { 16 + Commit { 17 + did: String, 18 + collection: String, 19 + rkey: String, 20 + cid: String, 21 + record: serde_json::Value, 22 + }, 23 + Delete { 24 + did: String, 25 + collection: String, 26 + rkey: String, 27 + }, 28 + } 29 + 30 + pub struct BlahgEventHandler { 31 + id: String, 32 + event_sender: mpsc::UnboundedSender<BlahgEvent>, 33 + } 34 + 35 + impl BlahgEventHandler { 36 + fn new(id: String, event_sender: mpsc::UnboundedSender<BlahgEvent>) -> Self { 37 + Self { id, event_sender } 38 + } 39 + } 40 + 41 + #[async_trait] 42 + impl EventHandler for BlahgEventHandler { 43 + async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { 44 + let award_event = match event { 45 + JetstreamEvent::Commit { did, commit, .. } => { 46 + // Filter for supported collection types 47 + match commit.collection.as_str() { 48 + "tools.smokesignal.blahg.content.post" 49 + | "app.bsky.feed.post" 50 + | "app.bsky.feed.like" 51 + | "community.lexicon.interaction.like" => BlahgEvent::Commit { 52 + did, 53 + collection: commit.collection, 54 + rkey: commit.rkey, 55 + cid: commit.cid, 56 + record: commit.record, 57 + }, 58 + _ => return Ok(()), 59 + } 60 + } 61 + JetstreamEvent::Delete { did, commit, .. } => { 62 + // Filter for supported collection types 63 + match commit.collection.as_str() { 64 + "tools.smokesignal.blahg.content.post" 65 + | "app.bsky.feed.post" 66 + | "app.bsky.feed.like" 67 + | "community.lexicon.interaction.like" => BlahgEvent::Delete { 68 + did, 69 + collection: commit.collection, 70 + rkey: commit.rkey, 71 + }, 72 + _ => return Ok(()), 73 + } 74 + } 75 + JetstreamEvent::Identity { .. } | JetstreamEvent::Account { .. } => { 76 + return Ok(()); 77 + } 78 + }; 79 + 80 + if let Err(err) = self.event_sender.send(award_event) { 81 + let blahg_error = BlahgError::ConsumerQueueSendFailed { 82 + details: err.to_string(), 83 + }; 84 + tracing::error!(?blahg_error); 85 + } 86 + 87 + Ok(()) 88 + } 89 + 90 + fn handler_id(&self) -> String { 91 + self.id.clone() 92 + } 93 + } 94 + 95 + /// Cursor writer handler that periodically writes the latest time_us to a file 96 + pub struct CursorWriterHandler { 97 + id: String, 98 + cursor_path: String, 99 + last_time_us: Arc<AtomicU64>, 100 + last_write: Arc<Mutex<Instant>>, 101 + write_interval: Duration, 102 + } 103 + 104 + impl CursorWriterHandler { 105 + fn new(id: String, cursor_path: String) -> Self { 106 + Self { 107 + id, 108 + cursor_path, 109 + last_time_us: Arc::new(AtomicU64::new(0)), 110 + last_write: Arc::new(Mutex::new(Instant::now())), 111 + write_interval: Duration::from_secs(30), 112 + } 113 + } 114 + 115 + async fn maybe_write_cursor(&self) -> Result<()> { 116 + let current_time_us = self.last_time_us.load(Ordering::Relaxed); 117 + if current_time_us == 0 { 118 + return Ok(()); 119 + } 120 + 121 + let mut last_write = self.last_write.lock().await; 122 + if last_write.elapsed() >= self.write_interval { 123 + tokio::fs::write(&self.cursor_path, current_time_us.to_string()).await?; 124 + *last_write = Instant::now(); 125 + tracing::debug!("Wrote cursor to {}: {}", self.cursor_path, current_time_us); 126 + } 127 + Ok(()) 128 + } 129 + } 130 + 131 + #[async_trait] 132 + impl EventHandler for CursorWriterHandler { 133 + async fn handle_event(&self, event: JetstreamEvent) -> Result<()> { 134 + // Extract time_us from any event type 135 + let time_us = match &event { 136 + JetstreamEvent::Commit { time_us, .. } => *time_us, 137 + JetstreamEvent::Delete { time_us, .. } => *time_us, 138 + JetstreamEvent::Identity { time_us, .. } => *time_us, 139 + JetstreamEvent::Account { time_us, .. } => *time_us, 140 + }; 141 + 142 + // Update the latest time_us 143 + self.last_time_us.store(time_us, Ordering::Relaxed); 144 + 145 + // Try to write the cursor periodically 146 + if let Err(err) = self.maybe_write_cursor().await { 147 + tracing::warn!("Failed to write cursor: {}", err); 148 + } 149 + 150 + Ok(()) 151 + } 152 + 153 + fn handler_id(&self) -> String { 154 + self.id.clone() 155 + } 156 + } 157 + 158 + pub struct Consumer {} 159 + 160 + impl Consumer { 161 + pub fn create_blahg_handler(&self) -> (Arc<BlahgEventHandler>, BlahgEventReceiver) { 162 + let (sender, receiver) = mpsc::unbounded_channel(); 163 + let handler = Arc::new(BlahgEventHandler::new( 164 + "blagh-processor".to_string(), 165 + sender, 166 + )); 167 + (handler, receiver) 168 + } 169 + 170 + pub fn create_cursor_writer_handler(&self, cursor_path: String) -> Arc<CursorWriterHandler> { 171 + Arc::new(CursorWriterHandler::new( 172 + "cursor-writer".to_string(), 173 + cursor_path, 174 + )) 175 + } 176 + }
+305
src/errors.rs
···
··· 1 + use axum::response::{IntoResponse, Response}; 2 + use reqwest::StatusCode; 3 + use thiserror::Error; 4 + 5 + /// Main error type for the Blahg application 6 + /// 7 + /// All errors follow the format: error-blahg-<domain>-<number> <message>: <details> 8 + /// Domains are organized alphabetically: config, consumer, http, process, storage 9 + #[derive(Error, Debug)] 10 + pub enum BlahgError { 11 + // Config domain - Configuration-related errors 12 + #[error("error-blahg-config-1 Failed to parse HTTP client timeout: {details}")] 13 + /// Failed to parse HTTP client timeout from environment variable. 14 + ConfigHttpTimeoutInvalid { 15 + /// Details about the parsing error. 16 + details: String, 17 + }, 18 + 19 + #[error("error-blahg-config-2 Failed to parse HTTP port: {port}")] 20 + /// Failed to parse HTTP port from environment variable. 21 + ConfigHttpPortInvalid { 22 + /// The invalid port value. 23 + port: String, 24 + }, 25 + 26 + #[error("error-blahg-config-3 Invalid S3 URL format: {details}")] 27 + /// Failed to parse S3 URL format from environment variable. 28 + ConfigS3UrlInvalid { 29 + /// Details about the S3 URL parsing error. 30 + details: String, 31 + }, 32 + 33 + #[error("error-blahg-config-4 Required feature not enabled: {feature}")] 34 + /// Required feature is not enabled in the build. 35 + ConfigFeatureNotEnabled { 36 + /// The feature that is required but not enabled. 37 + feature: String, 38 + }, 39 + 40 + // Consumer domain - Jetstream consumer errors 41 + /// Error when a DNS nameserver IP cannot be parsed. 42 + /// 43 + /// This error occurs when the DNS_NAMESERVERS environment variable contains 44 + /// an IP address that cannot be parsed as a valid IpAddr. 45 + #[error("error-blahg-config-5 Unable to parse nameserver IP '{0}': {1}")] 46 + NameserverParsingFailed(String, std::net::AddrParseError), 47 + 48 + // Consumer domain - Jetstream consumer errors 49 + #[error("error-blahg-consumer-1 Failed to send badge event to queue: {details}")] 50 + /// Failed to send badge event to the processing queue. 51 + ConsumerQueueSendFailed { 52 + /// Details about the send failure. 53 + details: String, 54 + }, 55 + 56 + #[error("error-blahg-consumer-2 Jetstream disconnected: {details}")] 57 + /// Jetstream consumer disconnected. 58 + ConsumerDisconnected { 59 + /// Details about the disconnection. 60 + details: String, 61 + }, 62 + 63 + #[error( 64 + "error-blahg-consumer-3 Jetstream disconnect rate exceeded: {disconnect_count} disconnects in {duration_mins} minutes" 65 + )] 66 + /// Jetstream consumer disconnect rate exceeded the allowable limit. 67 + ConsumerDisconnectRateExceeded { 68 + /// Number of disconnects in the time period. 69 + disconnect_count: usize, 70 + /// Duration in minutes for the disconnect rate calculation. 71 + duration_mins: u64, 72 + }, 73 + 74 + // HTTP domain - HTTP server and template errors 75 + #[error("error-blahg-http-1 Template rendering failed: {template}")] 76 + /// Template rendering failed in HTTP response. 77 + HttpTemplateRenderFailed { 78 + /// The template that failed to render. 79 + template: String, 80 + }, 81 + 82 + #[error("error-blahg-http-2 Internal server error")] 83 + /// Generic internal server error for HTTP responses. 84 + HttpInternalServerError, 85 + 86 + // Process domain - Badge processing errors 87 + #[error("error-blahg-process-1 Invalid AT-URI format: {uri}")] 88 + /// Invalid AT-URI format encountered during processing. 89 + ProcessInvalidAturi { 90 + /// The invalid URI string. 91 + uri: String, 92 + }, 93 + 94 + #[error("error-blahg-process-2 Failed to resolve identity for {did}: {details}")] 95 + /// Identity resolution failed with detailed error information. 96 + ProcessIdentityResolutionFailed { 97 + /// The DID that could not be resolved. 98 + did: String, 99 + /// Detailed error information. 100 + details: String, 101 + }, 102 + 103 + #[error("error-blahg-process-3 Failed to fetch badge record: {uri}")] 104 + /// Failed to fetch badge record from AT Protocol. 105 + ProcessBadgeFetchFailed { 106 + /// The badge URI that could not be fetched. 107 + uri: String, 108 + }, 109 + 110 + #[error("error-blahg-process-4 Failed to fetch badge {uri}: {details}")] 111 + /// Badge record fetch failed with detailed error information. 112 + ProcessBadgeRecordFetchFailed { 113 + /// The badge URI that could not be fetched. 114 + uri: String, 115 + /// Detailed error information. 116 + details: String, 117 + }, 118 + 119 + #[error("error-blahg-process-5 Failed to download badge image: {image_ref}")] 120 + /// Failed to download badge image. 121 + ProcessImageDownloadFailed { 122 + /// Reference to the image that failed to download. 123 + image_ref: String, 124 + }, 125 + 126 + #[error("error-blahg-process-6 Failed to process badge event: {event_type}")] 127 + /// Failed to process a badge event from Jetstream. 128 + ProcessEventHandlingFailed { 129 + /// Type of event that failed to process. 130 + event_type: String, 131 + }, 132 + 133 + #[error("error-blahg-process-7 Image file too large: {size} bytes exceeds 3MB limit")] 134 + /// Badge image file exceeds maximum allowed size. 135 + ProcessImageTooLarge { 136 + /// The actual size of the image in bytes. 137 + size: usize, 138 + }, 139 + 140 + #[error("error-blahg-process-8 Failed to decode image: {details}")] 141 + /// Failed to decode badge image data. 142 + ProcessImageDecodeFailed { 143 + /// Details about the decode error. 144 + details: String, 145 + }, 146 + 147 + #[error("error-blahg-process-9 Unsupported image format: {format}")] 148 + /// Badge image is in an unsupported format. 149 + ProcessUnsupportedImageFormat { 150 + /// The unsupported image format. 151 + format: String, 152 + }, 153 + 154 + #[error( 155 + "error-blahg-process-10 Image dimensions too small: {width}x{height}, minimum is 512x512" 156 + )] 157 + /// Badge image dimensions are below minimum requirements. 158 + ProcessImageTooSmall { 159 + /// The actual width of the image. 160 + width: u32, 161 + /// The actual height of the image. 162 + height: u32, 163 + }, 164 + 165 + #[error("error-blahg-process-11 Image width too small after resize: {width}, minimum is 512")] 166 + /// Badge image width is below minimum after resize. 167 + ProcessImageWidthTooSmall { 168 + /// The actual width after resize. 169 + width: u32, 170 + }, 171 + 172 + #[error("error-blahg-process-12 No signatures field found in record")] 173 + /// Badge record is missing required signatures field. 174 + ProcessNoSignaturesField, 175 + 176 + #[error("error-blahg-process-13 Missing issuer field in signature")] 177 + /// Signature is missing required issuer field. 178 + ProcessMissingIssuerField, 179 + 180 + #[error("error-blahg-process-14 Missing signature field in signature")] 181 + /// Signature object is missing required signature field. 182 + ProcessMissingSignatureField, 183 + 184 + #[error("error-blahg-process-15 Record serialization failed: {details}")] 185 + /// Failed to serialize record for signature verification. 186 + ProcessRecordSerializationFailed { 187 + /// Details about the serialization error. 188 + details: String, 189 + }, 190 + 191 + #[error("error-blahg-process-16 Signature decoding failed: {details}")] 192 + /// Failed to decode signature bytes. 193 + ProcessSignatureDecodingFailed { 194 + /// Details about the decoding error. 195 + details: String, 196 + }, 197 + 198 + #[error( 199 + "error-blahg-process-17 Cryptographic validation failed for issuer {issuer}: {details}" 200 + )] 201 + /// Cryptographic signature validation failed. 202 + ProcessCryptographicValidationFailed { 203 + /// The issuer DID whose signature validation failed. 204 + issuer: String, 205 + /// Detailed error information. 206 + details: String, 207 + }, 208 + 209 + // Storage domain - Database and storage errors 210 + #[error("error-blahg-storage-1 Database operation failed: {operation}")] 211 + /// Database operation failed. 212 + StorageDatabaseFailed { 213 + /// Description of the failed operation. 214 + operation: String, 215 + }, 216 + 217 + #[error("error-blahg-storage-2 File storage operation failed: {operation}")] 218 + /// File storage operation failed. 219 + StorageFileOperationFailed { 220 + /// Description of the failed file operation. 221 + operation: String, 222 + }, 223 + } 224 + 225 + /// Result type alias for convenience 226 + pub type Result<T> = std::result::Result<T, BlahgError>; 227 + 228 + impl From<sqlx::Error> for BlahgError { 229 + fn from(err: sqlx::Error) -> Self { 230 + BlahgError::StorageDatabaseFailed { 231 + operation: err.to_string(), 232 + } 233 + } 234 + } 235 + 236 + impl From<serde_json::Error> for BlahgError { 237 + fn from(err: serde_json::Error) -> Self { 238 + BlahgError::ProcessEventHandlingFailed { 239 + event_type: err.to_string(), 240 + } 241 + } 242 + } 243 + 244 + impl From<reqwest::Error> for BlahgError { 245 + fn from(err: reqwest::Error) -> Self { 246 + BlahgError::ProcessBadgeFetchFailed { 247 + uri: err.to_string(), 248 + } 249 + } 250 + } 251 + 252 + impl From<image::ImageError> for BlahgError { 253 + fn from(err: image::ImageError) -> Self { 254 + BlahgError::ProcessImageDownloadFailed { 255 + image_ref: err.to_string(), 256 + } 257 + } 258 + } 259 + 260 + impl From<minijinja::Error> for BlahgError { 261 + fn from(err: minijinja::Error) -> Self { 262 + BlahgError::HttpTemplateRenderFailed { 263 + template: err.to_string(), 264 + } 265 + } 266 + } 267 + 268 + impl From<std::io::Error> for BlahgError { 269 + fn from(err: std::io::Error) -> Self { 270 + BlahgError::ProcessImageDownloadFailed { 271 + image_ref: err.to_string(), 272 + } 273 + } 274 + } 275 + 276 + impl From<std::num::ParseIntError> for BlahgError { 277 + fn from(err: std::num::ParseIntError) -> Self { 278 + BlahgError::ConfigHttpPortInvalid { 279 + port: err.to_string(), 280 + } 281 + } 282 + } 283 + 284 + impl From<anyhow::Error> for BlahgError { 285 + fn from(err: anyhow::Error) -> Self { 286 + BlahgError::ProcessEventHandlingFailed { 287 + event_type: err.to_string(), 288 + } 289 + } 290 + } 291 + 292 + impl From<atproto_record::errors::AturiError> for BlahgError { 293 + fn from(err: atproto_record::errors::AturiError) -> Self { 294 + BlahgError::ProcessInvalidAturi { 295 + uri: err.to_string(), 296 + } 297 + } 298 + } 299 + 300 + impl IntoResponse for BlahgError { 301 + fn into_response(self) -> Response { 302 + tracing::error!(error = ?self, "internal server error"); 303 + (StatusCode::INTERNAL_SERVER_ERROR).into_response() 304 + } 305 + }
+339
src/http.rs
···
··· 1 + //! HTTP server implementation with Axum web framework. 2 + //! 3 + //! Provides REST API endpoints and template-rendered pages for displaying 4 + //! blog posts, with content storage and markdown rendering. 5 + 6 + use std::sync::Arc; 7 + 8 + use axum::{ 9 + Router, 10 + body::Body, 11 + extract::{Path, State}, 12 + http::{StatusCode, header}, 13 + response::{IntoResponse, Response}, 14 + routing::get, 15 + }; 16 + use axum_template::RenderHtml; 17 + use axum_template::engine::Engine; 18 + use minijinja::context; 19 + use serde::{Deserialize, Serialize}; 20 + use tower_http::services::ServeDir; 21 + 22 + use crate::{ 23 + config::Config, 24 + errors::{BlahgError, Result}, 25 + render::RenderManager, 26 + storage::{ContentStorage, Post, Storage}, 27 + }; 28 + 29 + #[cfg(feature = "reload")] 30 + use minijinja_autoreload::AutoReloader; 31 + 32 + #[cfg(feature = "reload")] 33 + /// Template engine with auto-reloading support for development. 34 + pub type AppEngine = Engine<AutoReloader>; 35 + 36 + #[cfg(feature = "embed")] 37 + use minijinja::Environment; 38 + 39 + #[cfg(feature = "embed")] 40 + pub type AppEngine = Engine<Environment<'static>>; 41 + 42 + #[cfg(not(any(feature = "reload", feature = "embed")))] 43 + pub type AppEngine = Engine<minijinja::Environment<'static>>; 44 + 45 + /// Application state shared across HTTP handlers. 46 + #[derive(Clone)] 47 + pub struct AppState { 48 + /// Database storage for posts and identities. 49 + pub(crate) storage: Arc<dyn Storage>, 50 + /// Content storage for markdown files. 51 + pub(crate) content_storage: Arc<dyn ContentStorage>, 52 + /// Markdown renderer. 53 + pub(crate) render_manager: Arc<dyn RenderManager>, 54 + /// Application configuration. 55 + pub(crate) config: Arc<Config>, 56 + /// Template engine for rendering HTML responses. 57 + pub(crate) template_env: AppEngine, 58 + } 59 + 60 + impl AppState { 61 + /// Create a new AppState with the given dependencies. 62 + pub fn new( 63 + storage: Arc<dyn Storage>, 64 + content_storage: Arc<dyn ContentStorage>, 65 + render_manager: Arc<dyn RenderManager>, 66 + config: Arc<Config>, 67 + template_env: AppEngine, 68 + ) -> Self { 69 + Self { 70 + storage, 71 + content_storage, 72 + render_manager, 73 + config, 74 + template_env, 75 + } 76 + } 77 + } 78 + 79 + /// Display structure for post listing. 80 + #[derive(Debug, Serialize, Deserialize)] 81 + struct PostDisplay { 82 + slug: String, 83 + title: String, 84 + created_at: String, 85 + } 86 + 87 + impl TryFrom<Post> for PostDisplay { 88 + type Error = BlahgError; 89 + 90 + fn try_from(post: Post) -> Result<Self> { 91 + Ok(PostDisplay { 92 + slug: format!("{}-{}", post.record_key, post.slug), 93 + title: post.title, 94 + created_at: post.created_at.format("%Y-%m-%d %H:%M UTC").to_string(), 95 + }) 96 + } 97 + } 98 + 99 + /// Create the main application router with all HTTP routes. 100 + pub fn create_router(state: AppState) -> Router { 101 + Router::new() 102 + .route("/", get(handle_index)) 103 + .route("/posts/{full_slug}", get(handle_post)) 104 + .route( 105 + "/posts/{full_slug}/{collection}", 106 + get(handle_post_references), 107 + ) 108 + .route("/content/{cid}", get(handle_content)) 109 + .nest_service("/static", ServeDir::new(&state.config.http.static_path)) 110 + .with_state(state) 111 + } 112 + 113 + /// GET / - Handle the index page (home page). 114 + /// Gets all posts from storage and lists them ordered by most recent to oldest. 115 + async fn handle_index(State(state): State<AppState>) -> Result<impl IntoResponse> { 116 + match get_all_posts(&state).await { 117 + Ok(posts) => Ok(RenderHtml( 118 + "index.html", 119 + state.template_env.clone(), 120 + context! { 121 + title => "Recent Posts", 122 + posts => posts, 123 + }, 124 + )), 125 + Err(_) => Err(BlahgError::HttpInternalServerError), 126 + } 127 + } 128 + 129 + /// GET /posts/{record_key}-{slug} - Handle post pages. 130 + /// Gets the post from storage using the record key to construct an AT-URI, gets the content from file storage, 131 + /// and renders the markdown content from file storage into html to be passed to the template. 132 + async fn handle_post( 133 + Path(full_slug): Path<String>, 134 + State(state): State<AppState>, 135 + ) -> Result<impl IntoResponse> { 136 + let record_key = match full_slug.split_once('-') { 137 + Some((value, _)) => value, 138 + None => return Ok((StatusCode::NOT_FOUND).into_response()), 139 + }; 140 + 141 + // Construct AT-URI using the author config and record key 142 + let aturi = format!( 143 + "at://{}/tools.smokesignal.blahg.content.post/{}", 144 + state.config.author, record_key 145 + ); 146 + 147 + // Get the post from storage using the AT-URI 148 + let post = match state.storage.get_post(&aturi).await? { 149 + Some(post) => post, 150 + None => return Ok((StatusCode::NOT_FOUND).into_response()), 151 + }; 152 + 153 + let post_display = match PostDisplay::try_from(post.clone()) { 154 + Ok(value) => value, 155 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 156 + }; 157 + 158 + // Get the content from file storage using the content CID 159 + let content_data = match state.content_storage.read_content(&post.content).await { 160 + Ok(data) => data, 161 + Err(_) => return Ok((StatusCode::NOT_FOUND).into_response()), 162 + }; 163 + 164 + // Convert content data to string 165 + let markdown_content = match String::from_utf8(content_data) { 166 + Ok(content) => content, 167 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 168 + }; 169 + 170 + // Render the markdown content into HTML 171 + let html_content = match state 172 + .render_manager 173 + .render_markdown(&markdown_content) 174 + .await 175 + { 176 + Ok(html) => html, 177 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 178 + }; 179 + 180 + let collection_counts = match state.storage.get_post_reference_count(&aturi).await { 181 + Ok(value) => value, 182 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 183 + }; 184 + 185 + let total_activity: i64 = collection_counts.values().sum(); 186 + 187 + // Render the template with the post data and rendered content 188 + Ok(RenderHtml( 189 + "post.html", 190 + state.template_env.clone(), 191 + context! { 192 + post => post_display, 193 + post_content => html_content, 194 + total_activity, 195 + collection_counts, 196 + }, 197 + ) 198 + .into_response()) 199 + } 200 + 201 + /// GET /posts/{record_key}-{slug}/{collection} - Handle post references by collection. 202 + /// Gets the post from storage using the record key to construct an AT-URI, verifies the post content exists, 203 + /// and returns the list of post references for the specified collection. 204 + async fn handle_post_references( 205 + Path((full_slug, collection)): Path<(String, String)>, 206 + State(state): State<AppState>, 207 + ) -> Result<impl IntoResponse> { 208 + let record_key = match full_slug.split_once('-') { 209 + Some((value, _)) => value, 210 + None => return Ok((StatusCode::NOT_FOUND).into_response()), 211 + }; 212 + 213 + // Construct AT-URI using the author config and record key 214 + let aturi = format!( 215 + "at://{}/tools.smokesignal.blahg.content.post/{}", 216 + state.config.author, record_key 217 + ); 218 + 219 + // Get the post from storage using the AT-URI 220 + let post = match state.storage.get_post(&aturi).await? { 221 + Some(post) => post, 222 + None => return Ok((StatusCode::NOT_FOUND).into_response()), 223 + }; 224 + 225 + let post_display: PostDisplay = match PostDisplay::try_from(post.clone()) { 226 + Ok(value) => value, 227 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 228 + }; 229 + 230 + // Verify the post content exists in content storage 231 + let content_exists = match state.content_storage.content_exists(&post.content).await { 232 + Ok(exists) => exists, 233 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 234 + }; 235 + 236 + if !content_exists { 237 + return Ok((StatusCode::NOT_FOUND).into_response()); 238 + } 239 + 240 + // Get post references for this post and collection 241 + let post_references = match state 242 + .storage 243 + .get_post_references_for_post_for_collection(&aturi, &collection) 244 + .await 245 + { 246 + Ok(references) => references, 247 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 248 + }; 249 + 250 + // Render the template with the post data and references 251 + Ok(RenderHtml( 252 + "post_references.html", 253 + state.template_env.clone(), 254 + context! { 255 + post => post_display, 256 + collection => collection, 257 + post_references => post_references, 258 + }, 259 + ) 260 + .into_response()) 261 + } 262 + 263 + /// GET /content/{cid} - Handle content requests. 264 + /// Gets content from content storage and returns it as a response. 265 + async fn handle_content( 266 + Path(cid): Path<String>, 267 + State(state): State<AppState>, 268 + ) -> Result<impl IntoResponse> { 269 + // Check if content exists 270 + let exists = match state.content_storage.content_exists(&cid).await { 271 + Ok(exists) => exists, 272 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 273 + }; 274 + 275 + if !exists { 276 + return Ok((StatusCode::NOT_FOUND).into_response()); 277 + } 278 + 279 + // Read the content data 280 + let content_data = match state.content_storage.read_content(&cid).await { 281 + Ok(data) => data, 282 + Err(_) => return Ok((StatusCode::INTERNAL_SERVER_ERROR).into_response()), 283 + }; 284 + 285 + // Return the content with appropriate headers 286 + Ok(Response::builder() 287 + .status(StatusCode::OK) 288 + .header(header::CONTENT_TYPE, "application/octet-stream") 289 + .header(header::CACHE_CONTROL, "public, max-age=86400") // Cache for 1 day 290 + .body(Body::from(content_data)) 291 + .unwrap() 292 + .into_response()) 293 + } 294 + 295 + /// Get all posts from storage and format them for display. 296 + async fn get_all_posts(state: &AppState) -> Result<Vec<PostDisplay>> { 297 + let mut posts = state.storage.get_posts().await?; 298 + 299 + // Sort posts by created_at in descending order (most recent first) 300 + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 301 + 302 + posts 303 + .into_iter() 304 + .map(PostDisplay::try_from) 305 + .collect::<Result<Vec<_>>>() 306 + } 307 + 308 + #[cfg(test)] 309 + mod tests { 310 + use super::*; 311 + use crate::storage::Post; 312 + use chrono::Utc; 313 + use serde_json::Value; 314 + 315 + #[tokio::test] 316 + async fn test_post_display_creation() { 317 + let post = Post { 318 + aturi: "at://did:plc:test/com.example.blog/post1".to_string(), 319 + cid: "bafy123".to_string(), 320 + title: "Test Post".to_string(), 321 + slug: "test-post".to_string(), 322 + content: "bafy456".to_string(), 323 + record_key: "post1".to_string(), 324 + created_at: Utc::now(), 325 + updated_at: Utc::now(), 326 + record: Value::Null, 327 + }; 328 + 329 + let display = PostDisplay { 330 + slug: post.slug, 331 + title: post.title, 332 + created_at: post.created_at.format("%Y-%m-%d %H:%M UTC").to_string(), 333 + }; 334 + 335 + assert_eq!(display.slug, "test-post"); 336 + assert_eq!(display.title, "Test Post"); 337 + assert!(display.created_at.contains("UTC")); 338 + } 339 + }
+147
src/identity.rs
···
··· 1 + use atproto_identity::model::Document; 2 + use atproto_identity::resolve::IdentityResolver; 3 + use atproto_identity::storage::DidDocumentStorage; 4 + use chrono::Utc; 5 + use std::sync::Arc; 6 + 7 + use crate::errors::Result; 8 + use crate::storage::{Identity, IdentityStorage}; 9 + 10 + /// A caching identity resolver that uses an underlying `IdentityStorage` implementation 11 + /// to cache resolved identity documents before falling back to the underlying `IdentityResolver`. 12 + pub struct CachingIdentityResolver<T: IdentityStorage + ?Sized> { 13 + /// The underlying identity resolver to use when cache misses occur 14 + resolver: IdentityResolver, 15 + /// The storage implementation to use for caching 16 + storage: Arc<T>, 17 + } 18 + 19 + impl<T: IdentityStorage + ?Sized> CachingIdentityResolver<T> { 20 + /// Create a new caching identity resolver with the given resolver and storage. 21 + pub fn new(resolver: IdentityResolver, storage: Arc<T>) -> Self { 22 + Self { resolver, storage } 23 + } 24 + 25 + /// Resolve a DID to a Document, using the cache when possible. 26 + pub async fn resolve(&self, did: &str) -> Result<Document> { 27 + // First, try to get the identity from storage 28 + if let Some(identity) = self.storage.get_identity_by_did(did).await? { 29 + // Parse the stored record back to a Document 30 + let document: Document = serde_json::from_value(identity.record)?; 31 + return Ok(document); 32 + } 33 + 34 + // If not in storage, resolve using the underlying resolver 35 + let document = self.resolver.resolve(did).await?; 36 + 37 + // Store the resolved identity for future lookups 38 + self.store_resolved_identity(&document).await?; 39 + 40 + Ok(document) 41 + } 42 + 43 + /// Store a resolved identity document in the storage. 44 + async fn store_resolved_identity(&self, doc: &Document) -> Result<()> { 45 + let handle = doc 46 + .also_known_as 47 + .first() 48 + .and_then(|aka| aka.strip_prefix("at://")) 49 + .unwrap_or("unknown.handle") 50 + .to_string(); 51 + 52 + // Create a JSON representation of the document 53 + let record = serde_json::json!(doc); 54 + 55 + let identity = Identity { 56 + did: doc.id.clone(), 57 + handle, 58 + record, 59 + created_at: Utc::now(), 60 + updated_at: Utc::now(), 61 + }; 62 + 63 + self.storage.upsert_identity(&identity).await?; 64 + Ok(()) 65 + } 66 + } 67 + 68 + /// A variant that works with DidDocumentStorage instead of IdentityStorage 69 + pub struct CachingDidDocumentResolver<T: DidDocumentStorage + ?Sized> { 70 + /// The underlying identity resolver to use when cache misses occur 71 + resolver: IdentityResolver, 72 + /// The storage implementation to use for caching 73 + storage: Arc<T>, 74 + } 75 + 76 + impl<T: DidDocumentStorage + ?Sized> CachingDidDocumentResolver<T> { 77 + /// Create a new caching identity resolver with the given resolver and storage. 78 + pub fn new(resolver: IdentityResolver, storage: Arc<T>) -> Self { 79 + Self { resolver, storage } 80 + } 81 + 82 + /// Resolve a DID to a Document, using the cache when possible. 83 + pub async fn resolve(&self, did: &str) -> Result<Document> { 84 + // First, try to get the document from storage 85 + if let Some(document) = self.storage.get_document_by_did(did).await? { 86 + return Ok(document); 87 + } 88 + 89 + // If not in storage, resolve using the underlying resolver 90 + let document = self.resolver.resolve(did).await?; 91 + 92 + // Store the resolved document for future lookups 93 + self.storage.store_document(document.clone()).await?; 94 + 95 + Ok(document) 96 + } 97 + 98 + /// Resolve a handle to a Document, using the cache when possible. 99 + pub async fn resolve_handle(&self, handle: &str) -> Result<Document> { 100 + // For handle resolution, we need to resolve first since DidDocumentStorage 101 + // doesn't have a get_by_handle method 102 + let document = self.resolver.resolve(handle).await?; 103 + 104 + // Store the resolved document for future lookups 105 + self.storage.store_document(document.clone()).await?; 106 + 107 + Ok(document) 108 + } 109 + } 110 + 111 + #[cfg(test)] 112 + mod tests { 113 + use super::*; 114 + use crate::storage::sqlite::SqliteStorage; 115 + use atproto_identity::resolve::{InnerIdentityResolver, create_resolver}; 116 + use sqlx::SqlitePool; 117 + use std::sync::Arc; 118 + 119 + #[tokio::test] 120 + async fn test_caching_identity_resolver() -> Result<()> { 121 + // Create an in-memory SQLite database for testing 122 + let pool = SqlitePool::connect("sqlite::memory:").await?; 123 + let storage = Arc::new(SqliteStorage::new(pool)); 124 + 125 + // Run migrations using the Storage trait 126 + use crate::storage::Storage; 127 + storage.migrate().await?; 128 + 129 + // Create a mock resolver (this would normally resolve from the network) 130 + let dns_resolver = create_resolver(&[]); 131 + let http_client = reqwest::Client::new(); 132 + let inner_resolver = InnerIdentityResolver { 133 + dns_resolver, 134 + http_client, 135 + plc_hostname: "plc.directory".to_string(), 136 + }; 137 + let resolver = IdentityResolver(Arc::new(inner_resolver)); 138 + 139 + // Create the caching resolver 140 + let caching_resolver = CachingIdentityResolver::new(resolver, storage.clone()); 141 + 142 + // Test would go here - this is just a skeleton since we'd need real DIDs 143 + // and network access to properly test the resolution 144 + 145 + Ok(()) 146 + } 147 + }
+50
src/lexicon.rs
···
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, Clone, Deserialize)] 5 + pub struct BlobRecord { 6 + #[serde(rename = "$type")] 7 + pub r#type: String, 8 + 9 + #[serde(rename = "mimeType")] 10 + pub mime_type: String, 11 + pub size: i64, 12 + 13 + #[serde(rename = "ref")] 14 + pub r#ref: BlobRef, 15 + } 16 + 17 + #[derive(Debug, Clone, Deserialize)] 18 + pub struct BlobRef { 19 + #[serde(rename = "$link")] 20 + pub link: String, 21 + } 22 + 23 + #[derive(Debug, Clone, Deserialize)] 24 + pub struct PostAttachment { 25 + #[serde(rename = "$type")] 26 + pub r#type: String, 27 + 28 + pub content: BlobRecord, 29 + } 30 + 31 + #[derive(Debug, Clone, Deserialize)] 32 + pub struct PostRecord { 33 + #[serde(rename = "$type")] 34 + pub r#type: String, 35 + 36 + pub title: String, 37 + pub content: BlobRecord, 38 + 39 + #[serde(rename = "publishedAt")] 40 + pub published_at: DateTime<Utc>, 41 + 42 + #[serde(default = "empty_attachments")] 43 + pub attachments: Vec<PostAttachment>, 44 + 45 + pub langs: Vec<String>, 46 + } 47 + 48 + fn empty_attachments() -> Vec<PostAttachment> { 49 + Vec::new() 50 + }
+12
src/lib.rs
···
··· 1 + #![warn(missing_docs)] 2 + 3 + pub mod config; 4 + pub mod consumer; 5 + pub mod errors; 6 + pub mod http; 7 + pub mod identity; 8 + pub mod lexicon; 9 + pub mod process; 10 + pub mod render; 11 + pub mod storage; 12 + pub mod templates;
+455
src/process.rs
···
··· 1 + //! Event processing logic for Blahg ATProtocol events. 2 + //! 3 + //! Processes different types of ATProtocol events: 4 + //! - Blog post records (tools.smokesignal.blahg.content.post) 5 + //! - Feed posts that reference blog content (app.bsky.feed.post) 6 + //! - Likes on blog posts (app.bsky.feed.like, community.lexicon.interaction.like) 7 + 8 + use std::collections::{HashMap, HashSet}; 9 + use std::str::FromStr; 10 + use std::sync::Arc; 11 + use tokio::sync::RwLock; 12 + 13 + use crate::config::Config; 14 + use crate::consumer::{BlahgEvent, BlahgEventReceiver}; 15 + use crate::errors::{BlahgError, Result}; 16 + use crate::identity::CachingIdentityResolver; 17 + use crate::lexicon::PostRecord; 18 + use crate::storage::{ContentStorage, Post, PostReference, Storage}; 19 + use atproto_client::com::atproto::repo::get_blob; 20 + use atproto_record::aturi::ATURI; 21 + use chrono::Utc; 22 + use serde::{Deserialize, Serialize}; 23 + use serde_json::Value; 24 + use slugify::slugify; 25 + use tracing::{error, info}; 26 + 27 + /// Strong reference to another record (used in posts and likes) 28 + #[derive(Debug, Deserialize, Serialize, Default)] 29 + struct StrongRef { 30 + #[serde(rename = "$type")] 31 + pub type_: Option<String>, 32 + pub uri: String, 33 + pub cid: String, 34 + } 35 + 36 + /// app.bsky.feed.post record structure 37 + #[derive(Debug, Deserialize)] 38 + struct FeedPostRecord { 39 + #[serde(rename = "$type")] 40 + pub type_: Option<String>, 41 + 42 + #[serde(default)] 43 + facets: Vec<Facet>, 44 + 45 + #[serde(default)] 46 + embed: Option<Embed>, 47 + 48 + #[serde(default)] 49 + reply: Option<Reply>, 50 + } 51 + 52 + /// Facet in a feed post (for link detection) 53 + #[derive(Debug, Deserialize)] 54 + struct Facet { 55 + features: Vec<Feature>, 56 + } 57 + 58 + /// Feature in a facet 59 + #[derive(Debug, Deserialize)] 60 + struct Feature { 61 + #[serde(rename = "$type")] 62 + type_: String, 63 + uri: Option<String>, 64 + } 65 + 66 + /// Embed in a feed post 67 + #[derive(Debug, Deserialize)] 68 + struct Embed { 69 + #[serde(rename = "$type")] 70 + type_: String, 71 + external: Option<External>, 72 + record: Option<StrongRef>, 73 + } 74 + 75 + /// External embed 76 + #[derive(Debug, Deserialize)] 77 + struct External { 78 + uri: String, 79 + } 80 + 81 + /// Reply structure 82 + #[derive(Debug, Deserialize)] 83 + struct Reply { 84 + root: StrongRef, 85 + parent: StrongRef, 86 + } 87 + 88 + /// Like record structure (both app.bsky.feed.like and community.lexicon.interaction.like) 89 + #[derive(Debug, Deserialize)] 90 + struct LikeRecord { 91 + subject: StrongRef, 92 + } 93 + 94 + /// Background processor for Blahg events 95 + pub struct EventProcessor { 96 + storage: Arc<dyn Storage>, 97 + content_storage: Arc<dyn ContentStorage>, 98 + config: Arc<Config>, 99 + identity_resolver: CachingIdentityResolver<dyn Storage>, 100 + http_client: reqwest::Client, 101 + post_prefix_cache: RwLock<Option<HashMap<String, String>>>, 102 + } 103 + 104 + impl EventProcessor { 105 + /// Create a new event processor with the required dependencies. 106 + pub fn new( 107 + storage: Arc<dyn Storage>, 108 + content_storage: Arc<dyn ContentStorage>, 109 + config: Arc<Config>, 110 + identity_resolver: CachingIdentityResolver<dyn Storage>, 111 + http_client: reqwest::Client, 112 + ) -> Self { 113 + Self { 114 + storage, 115 + content_storage, 116 + config, 117 + identity_resolver, 118 + http_client, 119 + post_prefix_cache: RwLock::new(None), 120 + } 121 + } 122 + 123 + /// Start processing events from the queue 124 + pub async fn start_processing(&self, mut event_receiver: BlahgEventReceiver) -> Result<()> { 125 + info!("Event processor started"); 126 + 127 + while let Some(event) = event_receiver.recv().await { 128 + match &event { 129 + BlahgEvent::Commit { 130 + did, 131 + collection, 132 + rkey, 133 + cid, 134 + record, 135 + } => { 136 + if let Err(e) = self.handle_commit(did, collection, rkey, cid, record).await { 137 + error!("Failed to process commit event: {}", e); 138 + } 139 + } 140 + BlahgEvent::Delete { 141 + did, 142 + collection, 143 + rkey, 144 + } => { 145 + if let Err(e) = self.handle_delete(did, collection, rkey).await { 146 + error!("Failed to process delete event: {}", e); 147 + } 148 + } 149 + } 150 + } 151 + 152 + info!("Event processor finished"); 153 + Ok(()) 154 + } 155 + 156 + async fn handle_commit( 157 + &self, 158 + did: &str, 159 + collection: &str, 160 + rkey: &str, 161 + cid: &str, 162 + record: &Value, 163 + ) -> Result<()> { 164 + // info!("Processing commit: {} {} for {}", collection, rkey, did); 165 + 166 + match collection { 167 + "tools.smokesignal.blahg.content.post" => { 168 + self.handle_blog_post_commit(did, rkey, cid, record).await 169 + } 170 + "app.bsky.feed.post" => self.handle_feed_post_commit(did, rkey, cid, record).await, 171 + "app.bsky.feed.like" | "community.lexicon.interaction.like" => { 172 + self.handle_like_commit(did, collection, rkey, cid, record) 173 + .await 174 + } 175 + _ => { 176 + // Unknown collection type, ignore 177 + Ok(()) 178 + } 179 + } 180 + } 181 + 182 + async fn handle_delete(&self, did: &str, collection: &str, rkey: &str) -> Result<()> { 183 + let aturi = format!("at://{}/{}/{}", did, collection, rkey); 184 + 185 + match collection { 186 + "tools.smokesignal.blahg.content.post" => { 187 + if (self.storage.delete_post(&aturi).await?).is_some() { 188 + info!("Successfully deleted post: {}", aturi); 189 + } 190 + } 191 + "app.bsky.feed.post" | "app.bsky.feed.like" | "community.lexicon.interaction.like" => { 192 + self.storage.delete_post_reference(&aturi).await?; 193 + } 194 + _ => { 195 + // Unknown collection type, ignore 196 + } 197 + } 198 + 199 + Ok(()) 200 + } 201 + 202 + async fn handle_blog_post_commit( 203 + &self, 204 + did: &str, 205 + rkey: &str, 206 + cid: &str, 207 + record: &Value, 208 + ) -> Result<()> { 209 + // Check if the author matches the configured author 210 + if self.config.author != did { 211 + return Ok(()); 212 + } 213 + 214 + let aturi = format!("at://{}/tools.smokesignal.blahg.content.post/{}", did, rkey); 215 + 216 + // Parse the post record 217 + let post_record: PostRecord = serde_json::from_value(record.clone())?; 218 + 219 + // Resolve the DID to get PDS endpoint 220 + let document = self.identity_resolver.resolve(did).await?; 221 + let pds_endpoints = document.pds_endpoints(); 222 + let pds_endpoint = 223 + pds_endpoints 224 + .first() 225 + .ok_or_else(|| BlahgError::ProcessIdentityResolutionFailed { 226 + did: did.to_string(), 227 + details: "No PDS endpoint found in DID document".to_string(), 228 + })?; 229 + 230 + // Download and store the content blob 231 + let content_cid = post_record.content.r#ref.link.clone(); 232 + if !self.content_storage.content_exists(&content_cid).await? { 233 + let content_bytes = 234 + get_blob(&self.http_client, pds_endpoint, did, &content_cid).await?; 235 + self.content_storage 236 + .write_content(&content_cid, &content_bytes) 237 + .await?; 238 + } 239 + 240 + // Download and store attachment blobs 241 + for attachment in &post_record.attachments { 242 + let attachment_cid = attachment.content.r#ref.link.clone(); 243 + if !self.content_storage.content_exists(&attachment_cid).await? { 244 + let attachment_bytes = 245 + get_blob(&self.http_client, pds_endpoint, did, &attachment_cid).await?; 246 + self.content_storage 247 + .write_content(&attachment_cid, &attachment_bytes) 248 + .await?; 249 + } 250 + } 251 + 252 + // Generate slug from title 253 + // let slug = self.generate_slug(&post_record.title); 254 + let slug = format!("{}-{}", rkey, slugify!(&post_record.title)); 255 + 256 + // Create post record 257 + let post = Post { 258 + aturi: aturi.to_string(), 259 + cid: cid.to_string(), 260 + title: post_record.title.clone(), 261 + slug, 262 + content: content_cid, 263 + record_key: rkey.to_string(), 264 + created_at: post_record.published_at, 265 + updated_at: Utc::now(), 266 + record: record.clone(), 267 + }; 268 + 269 + // Store post 270 + self.storage.upsert_post(&post).await?; 271 + 272 + // Invalidate the post prefix cache 273 + { 274 + let mut guard = self.post_prefix_cache.write().await; 275 + *guard = None; 276 + } 277 + 278 + info!("Successfully processed blog post: {}", aturi); 279 + Ok(()) 280 + } 281 + 282 + async fn handle_feed_post_commit( 283 + &self, 284 + did: &str, 285 + rkey: &str, 286 + cid: &str, 287 + record: &Value, 288 + ) -> Result<()> { 289 + let aturi = format!("at://{}/app.bsky.feed.post/{}", did, rkey); 290 + 291 + // Parse the feed post record 292 + let feed_post: FeedPostRecord = match serde_json::from_value(record.clone()) { 293 + Ok(post) => post, 294 + Err(_) => return Ok(()), // Invalid record format, skip 295 + }; 296 + 297 + // Get the post prefix lookup for URL matching 298 + let post_prefix_lookup = self.post_prefix_lookup().await?; 299 + let mut referenced_post_aturis = HashSet::new(); 300 + 301 + // Check facets for post URL prefixes 302 + for facet in &feed_post.facets { 303 + for feature in &facet.features { 304 + if feature.type_ == "app.bsky.richtext.facet#link" { 305 + if let Some(uri) = &feature.uri { 306 + // Check if this URI starts with any of the post prefix keys 307 + for (prefix, post_aturi) in &post_prefix_lookup { 308 + if uri.starts_with(prefix) { 309 + referenced_post_aturis.insert(post_aturi.clone()); 310 + break; 311 + } 312 + } 313 + } 314 + } 315 + } 316 + } 317 + 318 + // Check embed for post URL prefixes and AT-URIs 319 + if let Some(embed) = &feed_post.embed { 320 + if embed.type_ == "app.bsky.embed.external" { 321 + if let Some(external) = &embed.external { 322 + // Check if this external URI starts with any of the post prefix keys 323 + for (prefix, post_aturi) in &post_prefix_lookup { 324 + if external.uri.starts_with(prefix) { 325 + referenced_post_aturis.insert(post_aturi.clone()); 326 + break; 327 + } 328 + } 329 + } 330 + } 331 + // Check for embedded records from known author 332 + if embed.type_ == "app.bsky.embed.record" { 333 + if let Some(record_ref) = &embed.record { 334 + // Check if the record URI is in our post lookup values 335 + if post_prefix_lookup.values().any(|v| v == &record_ref.uri) { 336 + referenced_post_aturis.insert(record_ref.uri.clone()); 337 + } 338 + } 339 + } 340 + } 341 + 342 + // Check reply for known author posts 343 + if let Some(reply) = &feed_post.reply { 344 + if post_prefix_lookup.values().any(|v| v == &reply.root.uri) { 345 + referenced_post_aturis.insert(reply.root.uri.clone()); 346 + } 347 + if post_prefix_lookup.values().any(|v| v == &reply.parent.uri) { 348 + referenced_post_aturis.insert(reply.parent.uri.clone()); 349 + } 350 + } 351 + 352 + // Store references for each referenced post 353 + for post_aturi in referenced_post_aturis { 354 + let post_reference = PostReference { 355 + aturi: aturi.to_string(), 356 + cid: cid.to_string(), 357 + did: did.to_string(), 358 + collection: "app.bsky.feed.post".to_string(), 359 + post_aturi: post_aturi.clone(), 360 + discovered_at: Utc::now(), 361 + record: record.clone(), 362 + }; 363 + 364 + self.storage.upsert_post_reference(&post_reference).await?; 365 + info!("Stored feed post reference: {} -> {}", aturi, post_aturi); 366 + } 367 + 368 + Ok(()) 369 + } 370 + 371 + async fn handle_like_commit( 372 + &self, 373 + did: &str, 374 + collection: &str, 375 + rkey: &str, 376 + cid: &str, 377 + record: &Value, 378 + ) -> Result<()> { 379 + let aturi = format!("at://{}/{}/{}", did, collection, rkey); 380 + 381 + // Parse the like record 382 + let like_record: LikeRecord = match serde_json::from_value(record.clone()) { 383 + Ok(like) => like, 384 + Err(_) => return Ok(()), // Invalid record format, skip 385 + }; 386 + 387 + // Get the post prefix lookup for AT-URI matching 388 + let post_prefix_lookup = self.post_prefix_lookup().await?; 389 + 390 + // Check if the subject is a known author post 391 + if post_prefix_lookup 392 + .values() 393 + .any(|v| v == &like_record.subject.uri) 394 + { 395 + let post_reference = PostReference { 396 + aturi: aturi.to_string(), 397 + cid: cid.to_string(), 398 + did: did.to_string(), 399 + collection: collection.to_string(), 400 + post_aturi: like_record.subject.uri.clone(), 401 + discovered_at: Utc::now(), 402 + record: record.clone(), 403 + }; 404 + 405 + self.storage.upsert_post_reference(&post_reference).await?; 406 + info!( 407 + "Stored like reference: {} -> {}", 408 + aturi, like_record.subject.uri 409 + ); 410 + } 411 + 412 + Ok(()) 413 + } 414 + 415 + /// Check if an AT-URI references a post from a known author 416 + fn is_known_author_post(&self, uri: &str) -> bool { 417 + if let Ok(aturi) = ATURI::from_str(uri) { 418 + return aturi.collection == "tools.smokesignal.blahg.content.post" 419 + && self.config.author == aturi.authority; 420 + } 421 + false 422 + } 423 + 424 + /// Creates a post prefix lookup hashmap with external URL prefixes mapped to AT-URIs 425 + pub async fn post_prefix_lookup(&self) -> Result<HashMap<String, String>> { 426 + // Check if we have a cached value 427 + { 428 + let guard = self.post_prefix_cache.read().await; 429 + if let Some(cached) = guard.as_ref() { 430 + return Ok(cached.clone()); 431 + } 432 + } 433 + 434 + let posts = self.storage.get_posts().await?; 435 + let mut lookup = HashMap::new(); 436 + 437 + for post in posts { 438 + // Extract the record key from the AT-URI 439 + // AT-URI format: at://did/tools.smokesignal.blahg.content.post/rkey 440 + if let Ok(aturi) = ATURI::from_str(&post.aturi) { 441 + let record_key = aturi.record_key; 442 + let key = format!("{}/posts/{}-", self.config.external_base, record_key); 443 + lookup.insert(key, post.aturi); 444 + } 445 + } 446 + 447 + // Cache the result 448 + { 449 + let mut guard = self.post_prefix_cache.write().await; 450 + *guard = Some(lookup.clone()); 451 + } 452 + 453 + Ok(lookup) 454 + } 455 + }
+195
src/render.rs
···
··· 1 + use anyhow::Result; 2 + use async_trait::async_trait; 3 + use comrak::html::format_document_with_formatter; 4 + use comrak::nodes::Ast; 5 + use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; 6 + use comrak::{Options, Plugins}; 7 + use std::cell::RefCell; 8 + use std::io::BufWriter; 9 + use std::sync::Arc; 10 + 11 + 12 + #[async_trait] 13 + pub trait RenderManager: Send + Sync { 14 + async fn render_markdown(&self, markdown: &str) -> Result<String>; 15 + } 16 + 17 + pub struct ComrakRenderManager<'a> { 18 + syntect_adapter: Arc<SyntectAdapter>, 19 + options: Options<'a>, 20 + external_base: String, 21 + } 22 + 23 + impl ComrakRenderManager<'static> { 24 + pub fn new(external_base: &str) -> Self { 25 + let syntect_adapter = Arc::new( 26 + SyntectAdapterBuilder::new() 27 + .theme("base16-ocean.dark") 28 + .build(), 29 + ); 30 + 31 + let mut options = Options::default(); 32 + options.extension.strikethrough = true; 33 + options.extension.table = true; 34 + options.extension.autolink = true; 35 + options.extension.tasklist = true; 36 + options.extension.superscript = true; 37 + options.extension.footnotes = true; 38 + options.extension.description_lists = true; 39 + options.render.unsafe_ = false; 40 + 41 + Self { 42 + syntect_adapter, 43 + options, 44 + external_base: external_base.to_string(), 45 + } 46 + } 47 + 48 + pub fn with_theme(theme: &str, external_base: &str) -> Self { 49 + let syntect_adapter = Arc::new(SyntectAdapterBuilder::new().theme(theme).build()); 50 + 51 + let mut options = Options::default(); 52 + options.extension.strikethrough = true; 53 + options.extension.table = true; 54 + options.extension.autolink = true; 55 + options.extension.tasklist = true; 56 + options.extension.superscript = true; 57 + options.extension.footnotes = true; 58 + options.extension.description_lists = true; 59 + options.render.unsafe_ = false; 60 + 61 + Self { 62 + syntect_adapter, 63 + options, 64 + external_base: external_base.to_string(), 65 + } 66 + } 67 + 68 + pub fn with_css(external_base: &str) -> Self { 69 + let syntect_adapter = Arc::new(SyntectAdapterBuilder::new().css().build()); 70 + 71 + let mut options = Options::default(); 72 + options.extension.strikethrough = true; 73 + options.extension.table = true; 74 + options.extension.autolink = true; 75 + options.extension.tasklist = true; 76 + options.extension.superscript = true; 77 + options.extension.footnotes = true; 78 + options.extension.description_lists = true; 79 + options.render.unsafe_ = false; 80 + 81 + Self { 82 + syntect_adapter, 83 + options, 84 + external_base: external_base.to_string(), 85 + } 86 + } 87 + } 88 + 89 + fn prepend_image_base<'a>( 90 + nl: &mut comrak::nodes::NodeLink, 91 + context: &mut comrak::html::Context<String>, 92 + _node: &'a comrak::arena_tree::Node<'a, RefCell<Ast>>, 93 + entering: bool, 94 + ) { 95 + if !entering { 96 + return; 97 + } 98 + 99 + if !nl.url.starts_with("https://") && !nl.url.starts_with("/") { 100 + nl.url = format!("{}/content/{}", &context.user, nl.url) 101 + } 102 + } 103 + 104 + fn formatter<'a>( 105 + context: &mut comrak::html::Context<String>, 106 + node: &'a comrak::nodes::AstNode<'a>, 107 + entering: bool, 108 + ) -> std::io::Result<comrak::html::ChildRendering> { 109 + let mut borrow = node.data.borrow_mut(); 110 + if let comrak::nodes::NodeValue::Image(ref mut nl) = borrow.value { 111 + prepend_image_base(nl, context, node, entering); 112 + } 113 + drop(borrow); 114 + comrak::html::format_node_default(context, node, entering) 115 + } 116 + 117 + #[async_trait] 118 + impl RenderManager for ComrakRenderManager<'static> { 119 + async fn render_markdown(&self, markdown: &str) -> Result<String> { 120 + let adapter = self.syntect_adapter.as_ref(); 121 + let mut plugins = Plugins::default(); 122 + plugins.render.codefence_syntax_highlighter = Some(adapter); 123 + 124 + let arena = comrak::Arena::new(); 125 + let root = comrak::parse_document(&arena, markdown, &self.options); 126 + let mut bw = BufWriter::new(Vec::new()); 127 + format_document_with_formatter( 128 + root, 129 + &self.options, 130 + &mut bw, 131 + &plugins, 132 + formatter, 133 + self.external_base.clone(), 134 + ) 135 + .unwrap(); 136 + 137 + Ok(String::from_utf8(bw.into_inner().unwrap()).unwrap()) 138 + } 139 + } 140 + 141 + #[cfg(test)] 142 + mod tests { 143 + use super::*; 144 + 145 + #[tokio::test] 146 + async fn test_render_markdown() { 147 + let renderer = ComrakRenderManager::new("http://localhost:8080"); 148 + 149 + let markdown = "# Hello World\n\nThis is a **test**.\n\n```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```"; 150 + 151 + let html = renderer.render_markdown(markdown).await.unwrap(); 152 + 153 + assert!(html.contains("<h1>")); 154 + assert!(html.contains("Hello World")); 155 + assert!(html.contains("<strong>test</strong>")); 156 + assert!(html.contains("main")); 157 + } 158 + 159 + #[tokio::test] 160 + async fn test_render_with_extensions() { 161 + let renderer = ComrakRenderManager::new("http://localhost:8080"); 162 + 163 + let markdown = "~~strikethrough~~ and https://example.com autolink"; 164 + 165 + let html = renderer.render_markdown(markdown).await.unwrap(); 166 + 167 + assert!(html.contains("<del>")); 168 + assert!(html.contains("<a href=\"https://example.com\"")); 169 + } 170 + 171 + #[tokio::test] 172 + async fn test_syntax_highlighting() { 173 + let renderer = ComrakRenderManager::new("http://localhost:8080"); 174 + 175 + let markdown = "```rust\nlet x = 42;\n```"; 176 + 177 + let html = renderer.render_markdown(markdown).await.unwrap(); 178 + 179 + assert!(html.contains("<pre")); 180 + assert!(html.contains("style=")); 181 + } 182 + 183 + #[tokio::test] 184 + async fn test_image_url_prefixing() { 185 + let renderer = ComrakRenderManager::new("https://example.com"); 186 + 187 + let markdown = "![alt text](image.jpg) ![absolute](https://other.com/image.jpg) ![root](/root.jpg)"; 188 + 189 + let html = renderer.render_markdown(markdown).await.unwrap(); 190 + 191 + assert!(html.contains("https://example.com/image.jpg")); 192 + assert!(html.contains("https://other.com/image.jpg")); 193 + assert!(html.contains("\"/root.jpg\"")); 194 + } 195 + }
+380
src/storage/cached.rs
···
··· 1 + use std::collections::HashMap; 2 + use std::sync::Arc; 3 + 4 + use crate::errors::Result; 5 + use async_trait::async_trait; 6 + use bloomfilter::Bloom; 7 + use tokio::sync::RwLock; 8 + 9 + use super::{ContentStorage, Identity, IdentityStorage, Post, PostReference, PostStorage, Storage}; 10 + 11 + pub struct CachedPostStorage<T: Storage> { 12 + underlying_storage: Arc<T>, 13 + post_cache: Arc<RwLock<Option<HashMap<String, Post>>>>, 14 + } 15 + 16 + impl<T: Storage> CachedPostStorage<T> { 17 + pub fn new(underlying_storage: Arc<T>) -> Self { 18 + Self { 19 + underlying_storage, 20 + post_cache: Arc::new(RwLock::new(None)), 21 + } 22 + } 23 + 24 + async fn ensure_cache_populated(&self) -> Result<()> { 25 + let cache = self.post_cache.read().await; 26 + if cache.is_some() { 27 + return Ok(()); 28 + } 29 + drop(cache); 30 + 31 + let mut cache = self.post_cache.write().await; 32 + if cache.is_some() { 33 + return Ok(()); 34 + } 35 + 36 + let posts = self.underlying_storage.get_posts().await?; 37 + let mut post_map = HashMap::new(); 38 + 39 + for post in posts { 40 + post_map.insert(post.aturi.clone(), post); 41 + } 42 + 43 + *cache = Some(post_map); 44 + Ok(()) 45 + } 46 + 47 + async fn invalidate_cache(&self) -> Result<()> { 48 + let mut cache = self.post_cache.write().await; 49 + *cache = None; 50 + Ok(()) 51 + } 52 + 53 + async fn refresh_cache(&self) -> Result<()> { 54 + let mut cache = self.post_cache.write().await; 55 + let posts = self.underlying_storage.get_posts().await?; 56 + let mut post_map = HashMap::new(); 57 + 58 + for post in posts { 59 + post_map.insert(post.aturi.clone(), post); 60 + } 61 + 62 + *cache = Some(post_map); 63 + Ok(()) 64 + } 65 + } 66 + 67 + #[async_trait] 68 + impl<T: Storage> PostStorage for CachedPostStorage<T> { 69 + async fn upsert_post(&self, post: &Post) -> Result<()> { 70 + self.underlying_storage.upsert_post(post).await?; 71 + self.refresh_cache().await?; 72 + Ok(()) 73 + } 74 + 75 + async fn get_post(&self, aturi: &str) -> Result<Option<Post>> { 76 + self.ensure_cache_populated().await?; 77 + 78 + let cache = self.post_cache.read().await; 79 + if let Some(ref post_map) = *cache { 80 + if let Some(post) = post_map.get(aturi) { 81 + return Ok(Some(post.clone())); 82 + } 83 + } 84 + 85 + Ok(None) 86 + } 87 + 88 + async fn get_posts(&self) -> Result<Vec<Post>> { 89 + self.ensure_cache_populated().await?; 90 + 91 + let cache = self.post_cache.read().await; 92 + if let Some(ref post_map) = *cache { 93 + let mut posts: Vec<Post> = post_map.values().cloned().collect(); 94 + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 95 + return Ok(posts); 96 + } 97 + 98 + Ok(Vec::new()) 99 + } 100 + 101 + async fn delete_post(&self, aturi: &str) -> Result<Option<Post>> { 102 + let result = self.underlying_storage.delete_post(aturi).await?; 103 + self.refresh_cache().await?; 104 + Ok(result) 105 + } 106 + 107 + async fn upsert_post_reference(&self, post_reference: &PostReference) -> Result<bool> { 108 + self.underlying_storage 109 + .upsert_post_reference(post_reference) 110 + .await 111 + } 112 + 113 + async fn delete_post_reference(&self, aturi: &str) -> Result<()> { 114 + self.underlying_storage.delete_post_reference(aturi).await 115 + } 116 + 117 + async fn get_post_reference_count(&self, post_aturi: &str) -> Result<HashMap<String, i64>> { 118 + self.underlying_storage 119 + .get_post_reference_count(post_aturi) 120 + .await 121 + } 122 + 123 + async fn get_post_references_for_post(&self, post_aturi: &str) -> Result<Vec<PostReference>> { 124 + self.underlying_storage 125 + .get_post_references_for_post(post_aturi) 126 + .await 127 + } 128 + 129 + async fn get_post_references_for_post_for_collection( 130 + &self, 131 + post_aturi: &str, 132 + collection: &str, 133 + ) -> Result<Vec<PostReference>> { 134 + self.underlying_storage 135 + .get_post_references_for_post_for_collection(post_aturi, collection) 136 + .await 137 + } 138 + } 139 + 140 + #[async_trait] 141 + impl<T: Storage> IdentityStorage for CachedPostStorage<T> { 142 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 143 + self.underlying_storage.upsert_identity(identity).await 144 + } 145 + 146 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 147 + self.underlying_storage.get_identity_by_did(did).await 148 + } 149 + 150 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 151 + self.underlying_storage.get_identity_by_handle(handle).await 152 + } 153 + 154 + async fn delete_identity(&self, aturi: &str) -> Result<Option<Identity>> { 155 + self.underlying_storage.delete_identity(aturi).await 156 + } 157 + } 158 + 159 + #[async_trait] 160 + impl<T: Storage> Storage for CachedPostStorage<T> { 161 + async fn migrate(&self) -> Result<()> { 162 + self.underlying_storage.migrate().await 163 + } 164 + } 165 + 166 + /// A read-through cache implementation for ContentStorage that caches content by CID 167 + /// and uses a bloom filter to track CIDs that were not found in the underlying storage. 168 + pub struct CachedContentStorage<T: ContentStorage> { 169 + underlying_storage: Arc<T>, 170 + /// Cache of content by CID 171 + content_cache: Arc<RwLock<HashMap<String, Vec<u8>>>>, 172 + /// Bloom filter to track CIDs that were not found in underlying storage 173 + not_found_filter: Arc<RwLock<Bloom<String>>>, 174 + /// Maximum number of items to cache 175 + cache_size_limit: usize, 176 + } 177 + 178 + impl<T: ContentStorage> CachedContentStorage<T> { 179 + /// Create a new cached content storage with the given underlying storage. 180 + /// 181 + /// # Arguments 182 + /// * `underlying_storage` - The underlying ContentStorage implementation to wrap 183 + /// * `cache_size_limit` - Maximum number of items to cache (default: 1000) 184 + /// * `bloom_filter_capacity` - Expected number of items in the bloom filter (default: 10000) 185 + /// * `bloom_filter_error_rate` - False positive probability for bloom filter (default: 0.01) 186 + pub fn new(underlying_storage: Arc<T>) -> Self { 187 + Self::with_config(underlying_storage, 1000, 10000, 0.01) 188 + } 189 + 190 + /// Create a new cached content storage with custom configuration. 191 + pub fn with_config( 192 + underlying_storage: Arc<T>, 193 + cache_size_limit: usize, 194 + bloom_filter_capacity: usize, 195 + bloom_filter_error_rate: f64, 196 + ) -> Self { 197 + let bloom_filter = Bloom::new_for_fp_rate(bloom_filter_capacity, bloom_filter_error_rate); 198 + 199 + Self { 200 + underlying_storage, 201 + content_cache: Arc::new(RwLock::new(HashMap::new())), 202 + not_found_filter: Arc::new(RwLock::new(bloom_filter)), 203 + cache_size_limit, 204 + } 205 + } 206 + 207 + /// Check if the cache is at capacity and evict items if necessary. 208 + /// Uses a simple LRU-style eviction by clearing the cache when it's full. 209 + async fn maybe_evict_cache(&self) { 210 + let cache = self.content_cache.read().await; 211 + if cache.len() >= self.cache_size_limit { 212 + drop(cache); 213 + let mut cache = self.content_cache.write().await; 214 + if cache.len() >= self.cache_size_limit { 215 + // Simple eviction strategy: clear the entire cache 216 + // In a production system, you might want to implement LRU eviction 217 + cache.clear(); 218 + } 219 + } 220 + } 221 + 222 + /// Add a CID to the not-found bloom filter 223 + async fn add_to_not_found_filter(&self, cid: &str) { 224 + let mut filter = self.not_found_filter.write().await; 225 + filter.set(&cid.to_string()); 226 + } 227 + 228 + /// Check if a CID is in the not-found bloom filter 229 + async fn is_in_not_found_filter(&self, cid: &str) -> bool { 230 + let filter = self.not_found_filter.read().await; 231 + filter.check(&cid.to_string()) 232 + } 233 + 234 + /// Add content to the cache 235 + async fn add_to_cache(&self, cid: &str, content: Vec<u8>) { 236 + self.maybe_evict_cache().await; 237 + let mut cache = self.content_cache.write().await; 238 + cache.insert(cid.to_string(), content); 239 + } 240 + 241 + /// Get content from the cache 242 + async fn get_from_cache(&self, cid: &str) -> Option<Vec<u8>> { 243 + let cache = self.content_cache.read().await; 244 + cache.get(cid).cloned() 245 + } 246 + 247 + /// Remove content from the cache (used when content is written) 248 + async fn remove_from_cache(&self, cid: &str) { 249 + let mut cache = self.content_cache.write().await; 250 + cache.remove(cid); 251 + } 252 + 253 + /// Clear the not-found filter entry for a CID (used when content is written) 254 + async fn clear_not_found_filter(&self, _cid: &str) { 255 + // Note: Bloom filters don't support removal, so we recreate the filter 256 + // This is a limitation of bloom filters - in production you might want 257 + // to use a counting bloom filter or periodically reset the filter 258 + let mut filter = self.not_found_filter.write().await; 259 + *filter = Bloom::new_for_fp_rate(10000, 0.01); 260 + } 261 + } 262 + 263 + #[async_trait] 264 + impl<T: ContentStorage> ContentStorage for CachedContentStorage<T> { 265 + async fn content_exists(&self, cid: &str) -> Result<bool> { 266 + // First check the cache 267 + if self.get_from_cache(cid).await.is_some() { 268 + return Ok(true); 269 + } 270 + 271 + // Check the not-found filter to avoid expensive lookups 272 + if self.is_in_not_found_filter(cid).await { 273 + return Ok(false); 274 + } 275 + 276 + // Check the underlying storage 277 + let exists = self.underlying_storage.content_exists(cid).await?; 278 + 279 + // If not found, add to the not-found filter 280 + if !exists { 281 + self.add_to_not_found_filter(cid).await; 282 + } 283 + 284 + Ok(exists) 285 + } 286 + 287 + async fn write_content(&self, cid: &str, data: &[u8]) -> Result<()> { 288 + // Write to underlying storage first 289 + self.underlying_storage.write_content(cid, data).await?; 290 + 291 + // Clear any not-found filter entry 292 + self.clear_not_found_filter(cid).await; 293 + 294 + // Remove from cache (it will be cached on next read) 295 + self.remove_from_cache(cid).await; 296 + 297 + Ok(()) 298 + } 299 + 300 + async fn read_content(&self, cid: &str) -> Result<Vec<u8>> { 301 + // First check the cache 302 + if let Some(cached_content) = self.get_from_cache(cid).await { 303 + return Ok(cached_content); 304 + } 305 + 306 + // Check the not-found filter to avoid expensive lookups 307 + if self.is_in_not_found_filter(cid).await { 308 + return Err(crate::errors::BlahgError::StorageFileOperationFailed { 309 + operation: format!("Content not found: {}", cid), 310 + }); 311 + } 312 + 313 + // Read from underlying storage 314 + match self.underlying_storage.read_content(cid).await { 315 + Ok(content) => { 316 + // Cache the content 317 + self.add_to_cache(cid, content.clone()).await; 318 + Ok(content) 319 + } 320 + Err(e) => { 321 + // If it's a not-found error, add to the not-found filter 322 + if matches!( 323 + e, 324 + crate::errors::BlahgError::StorageFileOperationFailed { .. } 325 + ) { 326 + self.add_to_not_found_filter(cid).await; 327 + } 328 + Err(e) 329 + } 330 + } 331 + } 332 + } 333 + 334 + #[cfg(test)] 335 + mod tests { 336 + use super::*; 337 + use crate::storage::content::FilesystemContentStorage; 338 + use std::sync::Arc; 339 + use tempfile::TempDir; 340 + 341 + #[tokio::test] 342 + async fn test_cached_content_storage() -> Result<()> { 343 + let temp_dir = TempDir::new()?; 344 + let filesystem_storage = Arc::new(FilesystemContentStorage::new(temp_dir.path()).await?); 345 + 346 + let cached_storage = CachedContentStorage::new(filesystem_storage.clone()); 347 + 348 + let cid = "bafyreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"; 349 + let data = b"Hello, cached world!"; 350 + 351 + // Test content doesn't exist initially 352 + assert!(!cached_storage.content_exists(cid).await?); 353 + 354 + // Write content 355 + cached_storage.write_content(cid, data).await?; 356 + 357 + // Test content exists after writing 358 + assert!(cached_storage.content_exists(cid).await?); 359 + 360 + // Read content and verify - this should cache it 361 + let read_data = cached_storage.read_content(cid).await?; 362 + assert_eq!(read_data, data); 363 + 364 + // Read again - this should come from cache 365 + let read_data_cached = cached_storage.read_content(cid).await?; 366 + assert_eq!(read_data_cached, data); 367 + 368 + // Test not-found filtering 369 + let nonexistent_cid = 370 + "bafyreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi_nonexistent"; 371 + 372 + // First check should query underlying storage 373 + assert!(!cached_storage.content_exists(nonexistent_cid).await?); 374 + 375 + // Second check should use bloom filter and return false without querying underlying storage 376 + assert!(!cached_storage.content_exists(nonexistent_cid).await?); 377 + 378 + Ok(()) 379 + } 380 + }
+326
src/storage/content.rs
···
··· 1 + use std::path::{Path, PathBuf}; 2 + 3 + use crate::errors::Result; 4 + use async_trait::async_trait; 5 + use tokio::fs; 6 + 7 + #[cfg(feature = "s3")] 8 + use minio::s3::{Client as MinioClient, creds::StaticProvider, http::BaseUrl, types::S3Api}; 9 + 10 + use super::ContentStorage; 11 + 12 + pub use self::FilesystemContentStorage as FilesystemStorage; 13 + 14 + /// Parse an S3 URL in the format: s3://[key]:[secret]@hostname/bucket[/optional_prefix] 15 + /// Returns (endpoint, access_key, secret_key, bucket, prefix) 16 + #[cfg(feature = "s3")] 17 + pub fn parse_s3_url(url: &str) -> Result<(String, String, String, String, Option<String>)> { 18 + if !url.starts_with("s3://") { 19 + return Err(crate::errors::BlahgError::ConfigS3UrlInvalid { 20 + details: format!("Invalid S3 URL format: {}", url), 21 + }); 22 + } 23 + 24 + let url_without_scheme = &url[5..]; // Remove "s3://" 25 + 26 + // Split by '@' to separate credentials from hostname/path 27 + let parts: Vec<&str> = url_without_scheme.splitn(2, '@').collect(); 28 + if parts.len() != 2 { 29 + return Err(crate::errors::BlahgError::ConfigS3UrlInvalid { 30 + details: format!("Invalid S3 URL format - missing @ separator: {}", url), 31 + }); 32 + } 33 + 34 + let credentials = parts[0]; 35 + let hostname_and_path = parts[1]; 36 + 37 + // Parse credentials: key:secret 38 + let cred_parts: Vec<&str> = credentials.splitn(2, ':').collect(); 39 + if cred_parts.len() != 2 { 40 + return Err(crate::errors::BlahgError::ConfigS3UrlInvalid { 41 + details: format!( 42 + "Invalid S3 URL format - credentials must be key:secret: {}", 43 + url 44 + ), 45 + }); 46 + } 47 + 48 + let access_key = cred_parts[0].to_string(); 49 + let secret_key = cred_parts[1].to_string(); 50 + 51 + // Parse hostname and path: hostname/bucket[/prefix] 52 + let path_parts: Vec<&str> = hostname_and_path.splitn(2, '/').collect(); 53 + if path_parts.len() != 2 { 54 + return Err(crate::errors::BlahgError::ConfigS3UrlInvalid { 55 + details: format!("Invalid S3 URL format - must include bucket: {}", url), 56 + }); 57 + } 58 + 59 + let hostname = path_parts[0].to_string(); 60 + let bucket_and_prefix = path_parts[1]; 61 + 62 + // Split bucket from optional prefix 63 + let bucket_parts: Vec<&str> = bucket_and_prefix.splitn(2, '/').collect(); 64 + let bucket = bucket_parts[0].to_string(); 65 + let prefix = if bucket_parts.len() > 1 && !bucket_parts[1].is_empty() { 66 + Some(bucket_parts[1].to_string()) 67 + } else { 68 + None 69 + }; 70 + 71 + let endpoint = if hostname.starts_with("http://") || hostname.starts_with("https://") { 72 + hostname 73 + } else { 74 + format!("https://{}", hostname) 75 + }; 76 + 77 + Ok((endpoint, access_key, secret_key, bucket, prefix)) 78 + } 79 + 80 + /// Local filesystem implementation of content storage. 81 + #[derive(Debug, Clone)] 82 + pub struct FilesystemContentStorage { 83 + base_dir: PathBuf, 84 + } 85 + 86 + impl FilesystemContentStorage { 87 + /// Create a new filesystem content storage with the given base directory. 88 + pub async fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self> { 89 + let base_dir = base_dir.as_ref().to_path_buf(); 90 + 91 + // Ensure the base directory exists 92 + fs::create_dir_all(&base_dir).await?; 93 + 94 + Ok(Self { base_dir }) 95 + } 96 + 97 + /// Get the file path for a given CID. 98 + /// Uses a subdirectory structure based on the first few characters of the CID 99 + /// to avoid having too many files in a single directory. 100 + fn get_content_path(&self, cid: &str) -> PathBuf { 101 + // Use first 2 characters for first level directory 102 + // and next 2 characters for second level directory 103 + let (dir1, dir2, filename) = if cid.len() >= 4 { 104 + (&cid[0..2], &cid[2..4], cid) 105 + } else if cid.len() >= 2 { 106 + (&cid[0..2], "00", cid) 107 + } else { 108 + ("00", "00", cid) 109 + }; 110 + 111 + self.base_dir.join(dir1).join(dir2).join(filename) 112 + } 113 + } 114 + 115 + #[async_trait] 116 + impl ContentStorage for FilesystemContentStorage { 117 + async fn content_exists(&self, cid: &str) -> Result<bool> { 118 + let path = self.get_content_path(cid); 119 + Ok(fs::try_exists(&path).await?) 120 + } 121 + 122 + async fn write_content(&self, cid: &str, data: &[u8]) -> Result<()> { 123 + let path = self.get_content_path(cid); 124 + 125 + // Ensure parent directory exists 126 + if let Some(parent) = path.parent() { 127 + fs::create_dir_all(parent).await?; 128 + } 129 + 130 + // Write content atomically by writing to a temp file first 131 + let temp_path = path.with_extension("tmp"); 132 + fs::write(&temp_path, data).await?; 133 + 134 + // Rename temp file to final path (atomic on most filesystems) 135 + fs::rename(&temp_path, &path).await?; 136 + 137 + Ok(()) 138 + } 139 + 140 + async fn read_content(&self, cid: &str) -> Result<Vec<u8>> { 141 + let path = self.get_content_path(cid); 142 + Ok(fs::read(&path).await?) 143 + } 144 + } 145 + 146 + #[cfg(feature = "s3")] 147 + pub struct S3FileStorage { 148 + client: MinioClient, 149 + bucket: String, 150 + prefix: Option<String>, 151 + } 152 + 153 + #[cfg(feature = "s3")] 154 + impl S3FileStorage { 155 + /// Create a new S3FileStorage with the given credentials and bucket information 156 + pub fn new( 157 + endpoint: String, 158 + access_key: String, 159 + secret_key: String, 160 + bucket: String, 161 + prefix: Option<String>, 162 + ) -> Result<Self> { 163 + let base_url: BaseUrl = endpoint.parse().unwrap(); 164 + tracing::debug!(?base_url, "s3 file storage base url"); 165 + 166 + let static_provider = StaticProvider::new(&access_key, &secret_key, None); 167 + tracing::debug!(?static_provider, "s3 file storage static provider"); 168 + 169 + let client = MinioClient::new(base_url, Some(Box::new(static_provider)), None, None) 170 + .map_err(|e| crate::errors::BlahgError::StorageFileOperationFailed { 171 + operation: format!("Failed to create S3 client: {}", e), 172 + })?; 173 + 174 + Ok(Self { 175 + client, 176 + bucket, 177 + prefix, 178 + }) 179 + } 180 + 181 + /// Get the full object key by combining prefix with path 182 + fn get_object_key(&self, path: &str) -> String { 183 + match &self.prefix { 184 + Some(prefix) => { 185 + if path.starts_with('/') { 186 + format!("/{prefix}{path}") 187 + } else { 188 + format!("/{prefix}/{path}") 189 + } 190 + } 191 + None => { 192 + if path.starts_with('/') { 193 + path.to_string() 194 + } else { 195 + format!("/{path}") 196 + } 197 + } 198 + } 199 + } 200 + } 201 + 202 + #[cfg(feature = "s3")] 203 + #[async_trait] 204 + impl ContentStorage for S3FileStorage { 205 + async fn content_exists(&self, cid: &str) -> Result<bool> { 206 + use minio::s3::error::ErrorCode; 207 + 208 + let object_key = self.get_object_key(cid); 209 + 210 + match self 211 + .client 212 + .stat_object(&self.bucket, &object_key) 213 + .send() 214 + .await 215 + { 216 + Ok(_) => Ok(true), 217 + Err(minio::s3::error::Error::S3Error(ref s3_err)) 218 + if s3_err.code == ErrorCode::NoSuchKey => 219 + { 220 + Ok(false) 221 + } 222 + Err(e) => Err(crate::errors::BlahgError::StorageFileOperationFailed { 223 + operation: format!("Failed to check if S3 object exists: {}", e), 224 + }), 225 + } 226 + } 227 + 228 + async fn write_content(&self, cid: &str, data: &[u8]) -> Result<()> { 229 + use minio::s3::segmented_bytes::SegmentedBytes; 230 + 231 + let object_key = self.get_object_key(cid); 232 + 233 + let put_data = SegmentedBytes::from(bytes::Bytes::copy_from_slice(data)); 234 + 235 + self.client 236 + .put_object(&self.bucket, &object_key, put_data) 237 + .send() 238 + .await 239 + .map_err(|e| crate::errors::BlahgError::StorageFileOperationFailed { 240 + operation: format!("Failed to write S3 object: {}", e), 241 + })?; 242 + 243 + Ok(()) 244 + } 245 + 246 + async fn read_content(&self, cid: &str) -> Result<Vec<u8>> { 247 + let object_key = self.get_object_key(cid); 248 + 249 + let response = self 250 + .client 251 + .get_object(&self.bucket, &object_key) 252 + .send() 253 + .await 254 + .map_err(|e| crate::errors::BlahgError::StorageFileOperationFailed { 255 + operation: format!("Failed to read S3 object: {}", e), 256 + })?; 257 + 258 + let data = response 259 + .content 260 + .to_segmented_bytes() 261 + .await 262 + .map_err(|e| crate::errors::BlahgError::StorageFileOperationFailed { 263 + operation: format!("Failed to read S3 object data: {}", e), 264 + })? 265 + .to_bytes(); 266 + 267 + Ok(data.to_vec()) 268 + } 269 + } 270 + 271 + #[cfg(test)] 272 + mod tests { 273 + use super::*; 274 + use tempfile::TempDir; 275 + 276 + #[tokio::test] 277 + async fn test_filesystem_content_storage() -> Result<()> { 278 + let temp_dir = TempDir::new()?; 279 + let storage = FilesystemContentStorage::new(temp_dir.path()).await?; 280 + 281 + let cid = "bafyreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"; 282 + let data = b"Hello, world!"; 283 + 284 + // Test content doesn't exist initially 285 + assert!(!storage.content_exists(cid).await?); 286 + 287 + // Write content 288 + storage.write_content(cid, data).await?; 289 + 290 + // Test content exists after writing 291 + assert!(storage.content_exists(cid).await?); 292 + 293 + // Read content and verify 294 + let read_data = storage.read_content(cid).await?; 295 + assert_eq!(read_data, data); 296 + 297 + Ok(()) 298 + } 299 + 300 + #[tokio::test] 301 + async fn test_filesystem_path_structure() { 302 + let temp_dir = TempDir::new().unwrap(); 303 + let storage = FilesystemContentStorage::new(temp_dir.path()) 304 + .await 305 + .unwrap(); 306 + 307 + // Test normal CID 308 + let path = 309 + storage.get_content_path("bafyreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"); 310 + assert_eq!( 311 + path.file_name().unwrap(), 312 + "bafyreigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" 313 + ); 314 + assert!(path.parent().unwrap().ends_with("ba/fy")); 315 + 316 + // Test short CID 317 + let path = storage.get_content_path("ab"); 318 + assert_eq!(path.file_name().unwrap(), "ab"); 319 + assert!(path.parent().unwrap().ends_with("ab/00")); 320 + 321 + // Test very short CID 322 + let path = storage.get_content_path("a"); 323 + assert_eq!(path.file_name().unwrap(), "a"); 324 + assert!(path.parent().unwrap().ends_with("00/00")); 325 + } 326 + }
+99
src/storage/mod.rs
···
··· 1 + use crate::errors::Result; 2 + use async_trait::async_trait; 3 + use chrono::{DateTime, Utc}; 4 + use serde::{Deserialize, Serialize}; 5 + use serde_json::Value; 6 + use std::collections::HashMap; 7 + 8 + #[derive(Clone, Serialize, Deserialize, sqlx::FromRow)] 9 + pub struct Identity { 10 + pub did: String, 11 + pub handle: String, 12 + pub record: Value, 13 + pub created_at: DateTime<Utc>, 14 + pub updated_at: DateTime<Utc>, 15 + } 16 + 17 + // tools.smokesignal.blahg.content.post 18 + #[derive(Clone, Serialize, Deserialize, sqlx::FromRow)] 19 + pub struct Post { 20 + pub aturi: String, 21 + pub cid: String, 22 + pub title: String, 23 + pub slug: String, 24 + pub content: String, 25 + pub record_key: String, 26 + pub created_at: DateTime<Utc>, 27 + pub updated_at: DateTime<Utc>, 28 + pub record: Value, 29 + } 30 + 31 + #[derive(Clone, Serialize, Deserialize, sqlx::FromRow)] 32 + pub struct PostReference { 33 + pub aturi: String, 34 + pub cid: String, 35 + pub did: String, 36 + pub collection: String, 37 + pub post_aturi: String, 38 + pub discovered_at: DateTime<Utc>, 39 + pub record: Value, 40 + } 41 + 42 + #[async_trait] 43 + pub trait PostStorage: Send + Sync { 44 + async fn upsert_post(&self, post: &Post) -> Result<()>; 45 + 46 + async fn get_post(&self, aturi: &str) -> Result<Option<Post>>; 47 + 48 + async fn get_posts(&self) -> Result<Vec<Post>>; 49 + 50 + async fn delete_post(&self, aturi: &str) -> Result<Option<Post>>; 51 + 52 + async fn upsert_post_reference(&self, post_reference: &PostReference) -> Result<bool>; 53 + 54 + async fn delete_post_reference(&self, aturi: &str) -> Result<()>; 55 + 56 + async fn get_post_reference_count(&self, post_aturi: &str) -> Result<HashMap<String, i64>>; 57 + 58 + async fn get_post_references_for_post(&self, post_aturi: &str) -> Result<Vec<PostReference>>; 59 + 60 + async fn get_post_references_for_post_for_collection( 61 + &self, 62 + post_aturi: &str, 63 + collection: &str, 64 + ) -> Result<Vec<PostReference>>; 65 + } 66 + 67 + #[async_trait] 68 + pub trait IdentityStorage: Send + Sync { 69 + async fn upsert_identity(&self, identity: &Identity) -> Result<()>; 70 + 71 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>>; 72 + 73 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>>; 74 + 75 + async fn delete_identity(&self, aturi: &str) -> Result<Option<Identity>>; 76 + } 77 + 78 + #[async_trait] 79 + pub trait ContentStorage: Send + Sync { 80 + async fn content_exists(&self, cid: &str) -> Result<bool>; 81 + 82 + async fn write_content(&self, cid: &str, data: &[u8]) -> Result<()>; 83 + 84 + async fn read_content(&self, cid: &str) -> Result<Vec<u8>>; 85 + } 86 + 87 + #[async_trait] 88 + pub trait Storage: PostStorage + IdentityStorage + Send + Sync { 89 + async fn migrate(&self) -> Result<()>; 90 + } 91 + 92 + pub mod cached; 93 + pub mod content; 94 + pub mod postgres; 95 + #[cfg(feature = "sqlite")] 96 + pub mod sqlite; 97 + 98 + pub use cached::{CachedContentStorage, CachedPostStorage}; 99 + pub use content::FilesystemContentStorage;
+419
src/storage/postgres.rs
···
··· 1 + use std::collections::HashMap; 2 + use std::sync::Arc; 3 + 4 + use crate::errors::Result; 5 + use async_trait::async_trait; 6 + use atproto_identity::model::Document; 7 + use atproto_identity::storage::DidDocumentStorage; 8 + use chrono::Utc; 9 + use sqlx::Row; 10 + use sqlx::postgres::PgPool; 11 + 12 + use super::{Identity, IdentityStorage, Post, PostReference, PostStorage, Storage}; 13 + 14 + /// PostgreSQL storage implementation for blog storage operations 15 + #[derive(Debug, Clone)] 16 + pub struct PostgresStorage { 17 + pool: PgPool, 18 + } 19 + 20 + impl PostgresStorage { 21 + /// Create a new PostgreSQL storage instance with the given connection pool. 22 + pub fn new(pool: PgPool) -> Self { 23 + Self { pool } 24 + } 25 + } 26 + 27 + #[async_trait] 28 + impl PostStorage for PostgresStorage { 29 + async fn upsert_post(&self, post: &Post) -> Result<()> { 30 + sqlx::query( 31 + r#" 32 + INSERT INTO posts (aturi, cid, title, slug, content, record_key, created_at, updated_at, record) 33 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 34 + ON CONFLICT(aturi) DO UPDATE SET 35 + cid = EXCLUDED.cid, 36 + title = EXCLUDED.title, 37 + slug = EXCLUDED.slug, 38 + content = EXCLUDED.content, 39 + record_key = EXCLUDED.record_key, 40 + updated_at = EXCLUDED.updated_at, 41 + record = EXCLUDED.record 42 + "#, 43 + ) 44 + .bind(&post.aturi) 45 + .bind(&post.cid) 46 + .bind(&post.title) 47 + .bind(&post.slug) 48 + .bind(&post.content) 49 + .bind(&post.record_key) 50 + .bind(post.created_at) 51 + .bind(post.updated_at) 52 + .bind(&post.record) 53 + .execute(&self.pool) 54 + .await?; 55 + 56 + Ok(()) 57 + } 58 + 59 + async fn get_post(&self, aturi: &str) -> Result<Option<Post>> { 60 + let row = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE aturi = $1") 61 + .bind(aturi) 62 + .fetch_optional(&self.pool) 63 + .await?; 64 + 65 + Ok(row) 66 + } 67 + 68 + async fn get_posts(&self) -> Result<Vec<Post>> { 69 + let rows = sqlx::query_as::<_, Post>("SELECT * FROM posts ORDER BY created_at DESC") 70 + .fetch_all(&self.pool) 71 + .await?; 72 + 73 + Ok(rows) 74 + } 75 + 76 + async fn delete_post(&self, aturi: &str) -> Result<Option<Post>> { 77 + let post = self.get_post(aturi).await?; 78 + 79 + if post.is_some() { 80 + sqlx::query("DELETE FROM posts WHERE aturi = $1") 81 + .bind(aturi) 82 + .execute(&self.pool) 83 + .await?; 84 + } 85 + 86 + Ok(post) 87 + } 88 + 89 + async fn upsert_post_reference(&self, post_reference: &PostReference) -> Result<bool> { 90 + let existing = 91 + sqlx::query_as::<_, PostReference>("SELECT * FROM post_references WHERE aturi = $1") 92 + .bind(&post_reference.aturi) 93 + .fetch_optional(&self.pool) 94 + .await?; 95 + 96 + let is_new = existing.is_none(); 97 + 98 + sqlx::query( 99 + r#" 100 + INSERT INTO post_references (aturi, cid, did, collection, post_aturi, discovered_at, record) 101 + VALUES ($1, $2, $3, $4, $5, $6, $7) 102 + ON CONFLICT(aturi) DO UPDATE SET 103 + cid = EXCLUDED.cid, 104 + did = EXCLUDED.did, 105 + collection = EXCLUDED.collection, 106 + post_aturi = EXCLUDED.post_aturi, 107 + discovered_at = EXCLUDED.discovered_at, 108 + record = EXCLUDED.record 109 + "#, 110 + ) 111 + .bind(&post_reference.aturi) 112 + .bind(&post_reference.cid) 113 + .bind(&post_reference.did) 114 + .bind(&post_reference.collection) 115 + .bind(&post_reference.post_aturi) 116 + .bind(post_reference.discovered_at) 117 + .bind(&post_reference.record) 118 + .execute(&self.pool) 119 + .await?; 120 + 121 + Ok(is_new) 122 + } 123 + 124 + async fn delete_post_reference(&self, aturi: &str) -> Result<()> { 125 + sqlx::query("DELETE FROM post_references WHERE aturi = $1") 126 + .bind(aturi) 127 + .execute(&self.pool) 128 + .await?; 129 + 130 + Ok(()) 131 + } 132 + 133 + async fn get_post_reference_count(&self, post_aturi: &str) -> Result<HashMap<String, i64>> { 134 + let rows = sqlx::query( 135 + r#" 136 + SELECT collection, COUNT(*) as count 137 + FROM post_references 138 + WHERE post_aturi = $1 139 + GROUP BY collection 140 + "#, 141 + ) 142 + .bind(post_aturi) 143 + .fetch_all(&self.pool) 144 + .await?; 145 + 146 + let mut count_map = HashMap::new(); 147 + for row in rows { 148 + let collection: String = row.get("collection"); 149 + let count: i64 = row.get("count"); 150 + count_map.insert(collection, count); 151 + } 152 + 153 + Ok(count_map) 154 + } 155 + 156 + async fn get_post_references_for_post(&self, post_aturi: &str) -> Result<Vec<PostReference>> { 157 + let rows = sqlx::query_as::<_, PostReference>( 158 + "SELECT * FROM post_references WHERE post_aturi = $1 ORDER BY discovered_at DESC", 159 + ) 160 + .bind(post_aturi) 161 + .fetch_all(&self.pool) 162 + .await?; 163 + 164 + Ok(rows) 165 + } 166 + 167 + async fn get_post_references_for_post_for_collection( 168 + &self, 169 + post_aturi: &str, 170 + collection: &str, 171 + ) -> Result<Vec<PostReference>> { 172 + let rows = sqlx::query_as::<_, PostReference>( 173 + "SELECT * FROM post_references WHERE post_aturi = $1 AND collection = $2 ORDER BY discovered_at DESC", 174 + ) 175 + .bind(post_aturi) 176 + .bind(collection) 177 + .fetch_all(&self.pool) 178 + .await?; 179 + 180 + Ok(rows) 181 + } 182 + } 183 + 184 + #[async_trait] 185 + impl IdentityStorage for PostgresStorage { 186 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 187 + sqlx::query( 188 + r#" 189 + INSERT INTO identities (did, handle, record, created_at, updated_at) 190 + VALUES ($1, $2, $3, $4, $5) 191 + ON CONFLICT(did) DO UPDATE SET 192 + handle = EXCLUDED.handle, 193 + record = EXCLUDED.record, 194 + updated_at = EXCLUDED.updated_at 195 + "#, 196 + ) 197 + .bind(&identity.did) 198 + .bind(&identity.handle) 199 + .bind(&identity.record) 200 + .bind(identity.created_at) 201 + .bind(identity.updated_at) 202 + .execute(&self.pool) 203 + .await?; 204 + 205 + Ok(()) 206 + } 207 + 208 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 209 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE did = $1") 210 + .bind(did) 211 + .fetch_optional(&self.pool) 212 + .await?; 213 + 214 + Ok(row) 215 + } 216 + 217 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 218 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE handle = $1") 219 + .bind(handle) 220 + .fetch_optional(&self.pool) 221 + .await?; 222 + 223 + Ok(row) 224 + } 225 + 226 + async fn delete_identity(&self, did: &str) -> Result<Option<Identity>> { 227 + let identity = self.get_identity_by_did(did).await?; 228 + 229 + if identity.is_some() { 230 + sqlx::query("DELETE FROM identities WHERE did = $1") 231 + .bind(did) 232 + .execute(&self.pool) 233 + .await?; 234 + } 235 + 236 + Ok(identity) 237 + } 238 + } 239 + 240 + #[async_trait] 241 + impl Storage for PostgresStorage { 242 + async fn migrate(&self) -> Result<()> { 243 + // Create identities table with JSONB for better JSON performance 244 + sqlx::query( 245 + r#" 246 + CREATE TABLE IF NOT EXISTS identities ( 247 + did TEXT PRIMARY KEY, 248 + handle TEXT NOT NULL, 249 + record JSONB NOT NULL, 250 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 251 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 252 + ); 253 + "#, 254 + ) 255 + .execute(&self.pool) 256 + .await?; 257 + 258 + // Create indexes for identities table 259 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_identities_handle ON identities(handle)") 260 + .execute(&self.pool) 261 + .await?; 262 + 263 + sqlx::query( 264 + "CREATE INDEX IF NOT EXISTS idx_identities_created_at ON identities(created_at)", 265 + ) 266 + .execute(&self.pool) 267 + .await?; 268 + 269 + sqlx::query( 270 + "CREATE INDEX IF NOT EXISTS idx_identities_updated_at ON identities(updated_at)", 271 + ) 272 + .execute(&self.pool) 273 + .await?; 274 + 275 + // Create posts table with JSONB 276 + sqlx::query( 277 + r#" 278 + CREATE TABLE IF NOT EXISTS posts ( 279 + aturi TEXT PRIMARY KEY, 280 + cid TEXT NOT NULL, 281 + title TEXT NOT NULL, 282 + slug TEXT NOT NULL UNIQUE, 283 + content TEXT NOT NULL, 284 + record_key TEXT NOT NULL, 285 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 286 + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 287 + record JSONB NOT NULL 288 + ); 289 + "#, 290 + ) 291 + .execute(&self.pool) 292 + .await?; 293 + 294 + // Create indexes for posts table 295 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_posts_cid ON posts(cid)") 296 + .execute(&self.pool) 297 + .await?; 298 + 299 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug)") 300 + .execute(&self.pool) 301 + .await?; 302 + 303 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at)") 304 + .execute(&self.pool) 305 + .await?; 306 + 307 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_posts_updated_at ON posts(updated_at)") 308 + .execute(&self.pool) 309 + .await?; 310 + 311 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_posts_record_key ON posts(record_key)") 312 + .execute(&self.pool) 313 + .await?; 314 + 315 + // Create post_references table with JSONB 316 + sqlx::query( 317 + r#" 318 + CREATE TABLE IF NOT EXISTS post_references ( 319 + aturi TEXT PRIMARY KEY, 320 + cid TEXT NOT NULL, 321 + did TEXT NOT NULL, 322 + collection TEXT NOT NULL, 323 + post_aturi TEXT NOT NULL, 324 + discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), 325 + record JSONB NOT NULL 326 + ); 327 + "#, 328 + ) 329 + .execute(&self.pool) 330 + .await?; 331 + 332 + // Create indexes for post_references table 333 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_post_references_cid ON post_references(cid)") 334 + .execute(&self.pool) 335 + .await?; 336 + 337 + sqlx::query("CREATE INDEX IF NOT EXISTS idx_post_references_did ON post_references(did)") 338 + .execute(&self.pool) 339 + .await?; 340 + 341 + sqlx::query( 342 + "CREATE INDEX IF NOT EXISTS idx_post_references_collection ON post_references(collection)", 343 + ) 344 + .execute(&self.pool) 345 + .await?; 346 + 347 + sqlx::query( 348 + "CREATE INDEX IF NOT EXISTS idx_post_references_discovered_at ON post_references(discovered_at)", 349 + ) 350 + .execute(&self.pool) 351 + .await?; 352 + 353 + sqlx::query( 354 + "CREATE INDEX IF NOT EXISTS idx_post_references_post_aturi ON post_references(post_aturi)", 355 + ) 356 + .execute(&self.pool) 357 + .await?; 358 + 359 + Ok(()) 360 + } 361 + } 362 + 363 + /// PostgreSQL-specific DID document storage adapter 364 + pub struct PostgresStorageDidDocumentStorage { 365 + storage: Arc<PostgresStorage>, 366 + } 367 + 368 + impl PostgresStorageDidDocumentStorage { 369 + /// Create a new DID document storage instance backed by PostgreSQL. 370 + pub fn new(storage: Arc<PostgresStorage>) -> Self { 371 + Self { storage } 372 + } 373 + } 374 + 375 + #[async_trait] 376 + impl DidDocumentStorage for PostgresStorageDidDocumentStorage { 377 + async fn get_document_by_did(&self, did: &str) -> anyhow::Result<Option<Document>> { 378 + if let Some(identity) = self 379 + .storage 380 + .get_identity_by_did(did) 381 + .await 382 + .map_err(anyhow::Error::new)? 383 + { 384 + let document: Document = serde_json::from_value(identity.record)?; 385 + Ok(Some(document)) 386 + } else { 387 + Ok(None) 388 + } 389 + } 390 + 391 + async fn store_document(&self, doc: Document) -> anyhow::Result<()> { 392 + let handle = doc 393 + .also_known_as 394 + .first() 395 + .and_then(|aka| aka.strip_prefix("at://")) 396 + .unwrap_or("unknown.handle") 397 + .to_string(); 398 + 399 + // Create a simple JSON representation of the document 400 + let record = serde_json::json!(doc); 401 + 402 + let identity = Identity { 403 + did: doc.id.clone(), 404 + handle, 405 + record, 406 + created_at: Utc::now(), 407 + updated_at: Utc::now(), 408 + }; 409 + 410 + self.storage 411 + .upsert_identity(&identity) 412 + .await 413 + .map_err(anyhow::Error::new) 414 + } 415 + 416 + async fn delete_document_by_did(&self, _did: &str) -> anyhow::Result<()> { 417 + Ok(()) 418 + } 419 + }
+395
src/storage/sqlite.rs
···
··· 1 + use std::collections::HashMap; 2 + use std::sync::Arc; 3 + 4 + use crate::errors::Result; 5 + use async_trait::async_trait; 6 + use atproto_identity::model::Document; 7 + use atproto_identity::storage::DidDocumentStorage; 8 + use chrono::Utc; 9 + use sqlx::Row; 10 + use sqlx::sqlite::SqlitePool; 11 + 12 + use super::{Identity, IdentityStorage, Post, PostReference, PostStorage, Storage}; 13 + 14 + /// SQLite storage implementation for blog posts and identities. 15 + #[derive(Debug, Clone)] 16 + pub struct SqliteStorage { 17 + pool: SqlitePool, 18 + } 19 + 20 + impl SqliteStorage { 21 + /// Create a new SQLite storage instance with the given connection pool. 22 + pub fn new(pool: SqlitePool) -> Self { 23 + Self { pool } 24 + } 25 + 26 + async fn migrate(&self) -> Result<()> { 27 + // Create identities table 28 + sqlx::query( 29 + r#" 30 + CREATE TABLE IF NOT EXISTS identities ( 31 + did TEXT PRIMARY KEY, 32 + handle TEXT NOT NULL, 33 + record JSON NOT NULL, 34 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 35 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 36 + ); 37 + "#, 38 + ) 39 + .execute(&self.pool) 40 + .await?; 41 + 42 + // Create indices for identities 43 + sqlx::query( 44 + r#" 45 + CREATE INDEX IF NOT EXISTS idx_identities_handle ON identities(handle); 46 + CREATE INDEX IF NOT EXISTS idx_identities_created_at ON identities(created_at); 47 + CREATE INDEX IF NOT EXISTS idx_identities_updated_at ON identities(updated_at); 48 + "#, 49 + ) 50 + .execute(&self.pool) 51 + .await?; 52 + 53 + // Create posts table 54 + sqlx::query( 55 + r#" 56 + CREATE TABLE IF NOT EXISTS posts ( 57 + aturi TEXT PRIMARY KEY, 58 + cid TEXT NOT NULL, 59 + title TEXT NOT NULL, 60 + slug TEXT NOT NULL UNIQUE, 61 + content TEXT NOT NULL, 62 + record_key TEXT NOT NULL, 63 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 64 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 65 + record JSON NOT NULL 66 + ); 67 + "#, 68 + ) 69 + .execute(&self.pool) 70 + .await?; 71 + 72 + // Create indices for posts 73 + sqlx::query( 74 + r#" 75 + CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug); 76 + CREATE INDEX IF NOT EXISTS idx_posts_record_key ON posts(record_key); 77 + CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at); 78 + CREATE INDEX IF NOT EXISTS idx_posts_updated_at ON posts(updated_at); 79 + "#, 80 + ) 81 + .execute(&self.pool) 82 + .await?; 83 + 84 + // Create post_references table 85 + sqlx::query( 86 + r#" 87 + CREATE TABLE IF NOT EXISTS post_references ( 88 + aturi TEXT PRIMARY KEY, 89 + cid TEXT NOT NULL, 90 + did TEXT NOT NULL, 91 + collection TEXT NOT NULL, 92 + post_aturi TEXT NOT NULL, 93 + discovered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 94 + record JSON NOT NULL 95 + ); 96 + "#, 97 + ) 98 + .execute(&self.pool) 99 + .await?; 100 + 101 + // Create indices for post_references 102 + sqlx::query( 103 + r#" 104 + CREATE INDEX IF NOT EXISTS idx_post_references_did ON post_references(did); 105 + CREATE INDEX IF NOT EXISTS idx_post_references_collection ON post_references(collection); 106 + CREATE INDEX IF NOT EXISTS idx_post_references_post_aturi ON post_references(post_aturi); 107 + CREATE INDEX IF NOT EXISTS idx_post_references_discovered_at ON post_references(discovered_at); 108 + "#, 109 + ) 110 + .execute(&self.pool) 111 + .await?; 112 + 113 + Ok(()) 114 + } 115 + } 116 + 117 + #[async_trait] 118 + impl PostStorage for SqliteStorage { 119 + async fn upsert_post(&self, post: &Post) -> Result<()> { 120 + sqlx::query( 121 + r#" 122 + INSERT INTO posts (aturi, cid, title, slug, content, record_key, created_at, updated_at, record) 123 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) 124 + ON CONFLICT(aturi) DO UPDATE SET 125 + cid = EXCLUDED.cid, 126 + title = EXCLUDED.title, 127 + slug = EXCLUDED.slug, 128 + content = EXCLUDED.content, 129 + record_key = EXCLUDED.record_key, 130 + updated_at = EXCLUDED.updated_at, 131 + record = EXCLUDED.record 132 + "#, 133 + ) 134 + .bind(&post.aturi) 135 + .bind(&post.cid) 136 + .bind(&post.title) 137 + .bind(&post.slug) 138 + .bind(&post.content) 139 + .bind(&post.record_key) 140 + .bind(post.created_at) 141 + .bind(post.updated_at) 142 + .bind(&post.record) 143 + .execute(&self.pool) 144 + .await?; 145 + 146 + Ok(()) 147 + } 148 + 149 + async fn get_post(&self, aturi: &str) -> Result<Option<Post>> { 150 + let row = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE aturi = $1") 151 + .bind(aturi) 152 + .fetch_optional(&self.pool) 153 + .await?; 154 + 155 + Ok(row) 156 + } 157 + 158 + async fn get_posts(&self) -> Result<Vec<Post>> { 159 + let rows = sqlx::query_as::<_, Post>("SELECT * FROM posts ORDER BY created_at DESC") 160 + .fetch_all(&self.pool) 161 + .await?; 162 + 163 + Ok(rows) 164 + } 165 + 166 + async fn delete_post(&self, aturi: &str) -> Result<Option<Post>> { 167 + let post = self.get_post(aturi).await?; 168 + 169 + if post.is_some() { 170 + sqlx::query("DELETE FROM posts WHERE aturi = $1") 171 + .bind(aturi) 172 + .execute(&self.pool) 173 + .await?; 174 + } 175 + 176 + Ok(post) 177 + } 178 + 179 + async fn upsert_post_reference(&self, post_reference: &PostReference) -> Result<bool> { 180 + let existing = sqlx::query("SELECT 1 FROM post_references WHERE aturi = $1") 181 + .bind(&post_reference.aturi) 182 + .fetch_optional(&self.pool) 183 + .await?; 184 + 185 + let is_new = existing.is_none(); 186 + 187 + sqlx::query( 188 + r#" 189 + INSERT INTO post_references (aturi, cid, did, collection, post_aturi, discovered_at, record) 190 + VALUES ($1, $2, $3, $4, $5, $6, $7) 191 + ON CONFLICT(aturi) DO UPDATE SET 192 + cid = EXCLUDED.cid, 193 + did = EXCLUDED.did, 194 + collection = EXCLUDED.collection, 195 + post_aturi = EXCLUDED.post_aturi, 196 + record = EXCLUDED.record 197 + "#, 198 + ) 199 + .bind(&post_reference.aturi) 200 + .bind(&post_reference.cid) 201 + .bind(&post_reference.did) 202 + .bind(&post_reference.collection) 203 + .bind(&post_reference.post_aturi) 204 + .bind(post_reference.discovered_at) 205 + .bind(&post_reference.record) 206 + .execute(&self.pool) 207 + .await?; 208 + 209 + Ok(is_new) 210 + } 211 + 212 + async fn delete_post_reference(&self, aturi: &str) -> Result<()> { 213 + sqlx::query("DELETE FROM post_references WHERE aturi = $1") 214 + .bind(aturi) 215 + .execute(&self.pool) 216 + .await?; 217 + 218 + Ok(()) 219 + } 220 + 221 + async fn get_post_reference_count(&self, post_aturi: &str) -> Result<HashMap<String, i64>> { 222 + let rows = sqlx::query( 223 + r#" 224 + SELECT collection, COUNT(*) as count 225 + FROM post_references 226 + WHERE post_aturi = $1 227 + GROUP BY collection 228 + "#, 229 + ) 230 + .bind(post_aturi) 231 + .fetch_all(&self.pool) 232 + .await?; 233 + 234 + let mut count_map = HashMap::new(); 235 + for row in rows { 236 + let collection: String = row.get("collection"); 237 + let count: i64 = row.get("count"); 238 + count_map.insert(collection, count); 239 + } 240 + 241 + Ok(count_map) 242 + } 243 + 244 + async fn get_post_references_for_post(&self, post_aturi: &str) -> Result<Vec<PostReference>> { 245 + let rows = sqlx::query_as::<_, PostReference>( 246 + "SELECT * FROM post_references WHERE post_aturi = $1 ORDER BY discovered_at DESC", 247 + ) 248 + .bind(post_aturi) 249 + .fetch_all(&self.pool) 250 + .await?; 251 + 252 + Ok(rows) 253 + } 254 + 255 + async fn get_post_references_for_post_for_collection( 256 + &self, 257 + post_aturi: &str, 258 + collection: &str, 259 + ) -> Result<Vec<PostReference>> { 260 + let rows = sqlx::query_as::<_, PostReference>( 261 + "SELECT * FROM post_references WHERE post_aturi = $1 AND collection = $2 ORDER BY discovered_at DESC", 262 + ) 263 + .bind(post_aturi) 264 + .bind(collection) 265 + .fetch_all(&self.pool) 266 + .await?; 267 + 268 + Ok(rows) 269 + } 270 + } 271 + 272 + #[async_trait] 273 + impl IdentityStorage for SqliteStorage { 274 + async fn upsert_identity(&self, identity: &Identity) -> Result<()> { 275 + sqlx::query( 276 + r#" 277 + INSERT INTO identities (did, handle, record, created_at, updated_at) 278 + VALUES ($1, $2, $3, $4, $5) 279 + ON CONFLICT(did) DO UPDATE SET 280 + handle = EXCLUDED.handle, 281 + record = EXCLUDED.record, 282 + updated_at = EXCLUDED.updated_at 283 + "#, 284 + ) 285 + .bind(&identity.did) 286 + .bind(&identity.handle) 287 + .bind(&identity.record) 288 + .bind(identity.created_at) 289 + .bind(identity.updated_at) 290 + .execute(&self.pool) 291 + .await?; 292 + 293 + Ok(()) 294 + } 295 + 296 + async fn get_identity_by_did(&self, did: &str) -> Result<Option<Identity>> { 297 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE did = $1") 298 + .bind(did) 299 + .fetch_optional(&self.pool) 300 + .await?; 301 + 302 + Ok(row) 303 + } 304 + 305 + async fn get_identity_by_handle(&self, handle: &str) -> Result<Option<Identity>> { 306 + let row = sqlx::query_as::<_, Identity>("SELECT * FROM identities WHERE handle = $1") 307 + .bind(handle) 308 + .fetch_optional(&self.pool) 309 + .await?; 310 + 311 + Ok(row) 312 + } 313 + 314 + async fn delete_identity(&self, did: &str) -> Result<Option<Identity>> { 315 + let identity = self.get_identity_by_did(did).await?; 316 + 317 + if identity.is_some() { 318 + sqlx::query("DELETE FROM identities WHERE did = $1") 319 + .bind(did) 320 + .execute(&self.pool) 321 + .await?; 322 + } 323 + 324 + Ok(identity) 325 + } 326 + } 327 + 328 + #[async_trait] 329 + impl Storage for SqliteStorage { 330 + async fn migrate(&self) -> Result<()> { 331 + self.migrate().await 332 + } 333 + } 334 + 335 + /// DID document storage implementation using SQLite. 336 + pub struct SqliteStorageDidDocumentStorage { 337 + storage: Arc<SqliteStorage>, 338 + } 339 + 340 + impl SqliteStorageDidDocumentStorage { 341 + /// Create a new DID document storage instance backed by SQLite. 342 + pub fn new(storage: Arc<SqliteStorage>) -> Self { 343 + Self { storage } 344 + } 345 + } 346 + 347 + #[async_trait] 348 + impl DidDocumentStorage for SqliteStorageDidDocumentStorage { 349 + async fn get_document_by_did(&self, did: &str) -> anyhow::Result<Option<Document>> { 350 + if let Some(identity) = self 351 + .storage 352 + .get_identity_by_did(did) 353 + .await 354 + .map_err(anyhow::Error::new)? 355 + { 356 + let document: Document = serde_json::from_value(identity.record)?; 357 + Ok(Some(document)) 358 + } else { 359 + Ok(None) 360 + } 361 + } 362 + 363 + async fn store_document(&self, doc: Document) -> anyhow::Result<()> { 364 + let handle = doc 365 + .also_known_as 366 + .first() 367 + .and_then(|aka| aka.strip_prefix("at://")) 368 + .unwrap_or("unknown.handle") 369 + .to_string(); 370 + 371 + // Create a JSON representation of the document 372 + let record = serde_json::json!(doc); 373 + 374 + let identity = Identity { 375 + did: doc.id.clone(), 376 + handle, 377 + record, 378 + created_at: Utc::now(), 379 + updated_at: Utc::now(), 380 + }; 381 + 382 + self.storage 383 + .upsert_identity(&identity) 384 + .await 385 + .map_err(anyhow::Error::new) 386 + } 387 + 388 + async fn delete_document_by_did(&self, did: &str) -> anyhow::Result<()> { 389 + self.storage 390 + .delete_identity(did) 391 + .await 392 + .map_err(anyhow::Error::new)?; 393 + Ok(()) 394 + } 395 + }
+60
src/templates.rs
···
··· 1 + //! Template engine configuration for embedded and reloadable templates. 2 + //! 3 + //! Provides template engines with embedded assets (production) or 4 + //! filesystem auto-reloading (development). Feature-flag controlled. 5 + 6 + #[cfg(feature = "reload")] 7 + use minijinja_autoreload::AutoReloader; 8 + 9 + #[cfg(feature = "embed")] 10 + use minijinja::Environment; 11 + 12 + #[cfg(feature = "reload")] 13 + /// Build template environment with auto-reloading for development 14 + pub fn build_env(external_base: String) -> AutoReloader { 15 + reload_env::build_env(external_base) 16 + } 17 + 18 + #[cfg(feature = "embed")] 19 + /// Build template environment with embedded templates for production 20 + pub fn build_env(external_base: String, version: String) -> Environment<'static> { 21 + embed_env::build_env(external_base, version) 22 + } 23 + 24 + #[cfg(feature = "reload")] 25 + mod reload_env { 26 + use std::path::PathBuf; 27 + 28 + use minijinja::{Environment, path_loader}; 29 + use minijinja_autoreload::AutoReloader; 30 + 31 + pub fn build_env(external_base: String) -> AutoReloader { 32 + let inner_external_base = external_base.clone(); 33 + AutoReloader::new(move |notifier| { 34 + let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates"); 35 + let mut env = Environment::new(); 36 + env.set_trim_blocks(true); 37 + env.set_lstrip_blocks(true); 38 + env.add_global("external_base", inner_external_base.as_str()); 39 + env.set_loader(path_loader(&template_path)); 40 + notifier.set_fast_reload(true); 41 + notifier.watch_path(&template_path, true); 42 + Ok(env) 43 + }) 44 + } 45 + } 46 + 47 + #[cfg(feature = "embed")] 48 + mod embed_env { 49 + use minijinja::Environment; 50 + 51 + pub fn build_env(external_base: String, version: String) -> Environment<'static> { 52 + let mut env = Environment::new(); 53 + env.set_trim_blocks(true); 54 + env.set_lstrip_blocks(true); 55 + env.add_global("external_base", external_base); 56 + env.add_global("version", version.clone()); 57 + minijinja_embed::load_templates!(&mut env); 58 + env 59 + } 60 + }
+4
static/pico.colors.css
···
··· 1 + @charset "UTF-8";/*! 2 + * Pico CSS ✨ v2.1.1 (https://picocss.com) 3 + * Copyright 2019-2025 - Licensed under MIT 4 + */:host,:root{--pico-color-red-950:#1c0d06;--pico-color-red-900:#30130a;--pico-color-red-850:#45150c;--pico-color-red-800:#5c160d;--pico-color-red-750:#72170f;--pico-color-red-700:#861d13;--pico-color-red-650:#9b2318;--pico-color-red-600:#af291d;--pico-color-red-550:#c52f21;--pico-color-red-500:#d93526;--pico-color-red-450:#ee402e;--pico-color-red-400:#f06048;--pico-color-red-350:#f17961;--pico-color-red-300:#f38f79;--pico-color-red-250:#f5a390;--pico-color-red-200:#f5b7a8;--pico-color-red-150:#f6cabf;--pico-color-red-100:#f8dcd6;--pico-color-red-50:#faeeeb;--pico-color-red:#c52f21;--pico-color-pink-950:#25060c;--pico-color-pink-900:#380916;--pico-color-pink-850:#4b0c1f;--pico-color-pink-800:#5f0e28;--pico-color-pink-750:#740f31;--pico-color-pink-700:#88143b;--pico-color-pink-650:#9d1945;--pico-color-pink-600:#b21e4f;--pico-color-pink-550:#c72259;--pico-color-pink-500:#d92662;--pico-color-pink-450:#f42c6f;--pico-color-pink-400:#f6547e;--pico-color-pink-350:#f7708e;--pico-color-pink-300:#f8889e;--pico-color-pink-250:#f99eae;--pico-color-pink-200:#f9b4be;--pico-color-pink-150:#f9c8ce;--pico-color-pink-100:#f9dbdf;--pico-color-pink-50:#fbedef;--pico-color-pink:#d92662;--pico-color-fuchsia-950:#230518;--pico-color-fuchsia-900:#360925;--pico-color-fuchsia-850:#480b33;--pico-color-fuchsia-800:#5c0d41;--pico-color-fuchsia-750:#700e4f;--pico-color-fuchsia-700:#84135e;--pico-color-fuchsia-650:#98176d;--pico-color-fuchsia-600:#ac1c7c;--pico-color-fuchsia-550:#c1208b;--pico-color-fuchsia-500:#d9269d;--pico-color-fuchsia-450:#ed2aac;--pico-color-fuchsia-400:#f748b7;--pico-color-fuchsia-350:#f869bf;--pico-color-fuchsia-300:#f983c7;--pico-color-fuchsia-250:#fa9acf;--pico-color-fuchsia-200:#f9b1d8;--pico-color-fuchsia-150:#f9c6e1;--pico-color-fuchsia-100:#f9daea;--pico-color-fuchsia-50:#fbedf4;--pico-color-fuchsia:#c1208b;--pico-color-purple-950:#1e0820;--pico-color-purple-900:#2d0f33;--pico-color-purple-850:#3d1545;--pico-color-purple-800:#4d1a57;--pico-color-purple-750:#5e206b;--pico-color-purple-700:#6f277d;--pico-color-purple-650:#802e90;--pico-color-purple-600:#9236a4;--pico-color-purple-550:#aa40bf;--pico-color-purple-500:#b645cd;--pico-color-purple-450:#c652dc;--pico-color-purple-400:#cd68e0;--pico-color-purple-350:#d47de4;--pico-color-purple-300:#db90e8;--pico-color-purple-250:#e2a3eb;--pico-color-purple-200:#e7b6ee;--pico-color-purple-150:#edc9f1;--pico-color-purple-100:#f2dcf4;--pico-color-purple-50:#f8eef9;--pico-color-purple:#9236a4;--pico-color-violet-950:#190928;--pico-color-violet-900:#251140;--pico-color-violet-850:#321856;--pico-color-violet-800:#3f1e6d;--pico-color-violet-750:#4d2585;--pico-color-violet-700:#5b2d9c;--pico-color-violet-650:#6935b3;--pico-color-violet-600:#7540bf;--pico-color-violet-550:#8352c5;--pico-color-violet-500:#9062ca;--pico-color-violet-450:#9b71cf;--pico-color-violet-400:#a780d4;--pico-color-violet-350:#b290d9;--pico-color-violet-300:#bd9fdf;--pico-color-violet-250:#c9afe4;--pico-color-violet-200:#d3bfe8;--pico-color-violet-150:#decfed;--pico-color-violet-100:#e8dff2;--pico-color-violet-50:#f3eff7;--pico-color-violet:#7540bf;--pico-color-indigo-950:#110b31;--pico-color-indigo-900:#181546;--pico-color-indigo-850:#1f1e5e;--pico-color-indigo-800:#272678;--pico-color-indigo-750:#2f2f92;--pico-color-indigo-700:#3838ab;--pico-color-indigo-650:#4040bf;--pico-color-indigo-600:#524ed2;--pico-color-indigo-550:#655cd6;--pico-color-indigo-500:#7569da;--pico-color-indigo-450:#8577dd;--pico-color-indigo-400:#9486e1;--pico-color-indigo-350:#a294e5;--pico-color-indigo-300:#b0a3e8;--pico-color-indigo-250:#bdb2ec;--pico-color-indigo-200:#cac1ee;--pico-color-indigo-150:#d8d0f1;--pico-color-indigo-100:#e5e0f4;--pico-color-indigo-50:#f2f0f9;--pico-color-indigo:#524ed2;--pico-color-blue-950:#080f2d;--pico-color-blue-900:#0c1a41;--pico-color-blue-850:#0e2358;--pico-color-blue-800:#0f2d70;--pico-color-blue-750:#0f3888;--pico-color-blue-700:#1343a0;--pico-color-blue-650:#184eb8;--pico-color-blue-600:#1d59d0;--pico-color-blue-550:#2060df;--pico-color-blue-500:#3c71f7;--pico-color-blue-450:#5c7ef8;--pico-color-blue-400:#748bf8;--pico-color-blue-350:#8999f9;--pico-color-blue-300:#9ca7fa;--pico-color-blue-250:#aeb5fb;--pico-color-blue-200:#bfc3fa;--pico-color-blue-150:#d0d2fa;--pico-color-blue-100:#e0e1fa;--pico-color-blue-50:#f0f0fb;--pico-color-blue:#2060df;--pico-color-azure-950:#04121d;--pico-color-azure-900:#061e2f;--pico-color-azure-850:#052940;--pico-color-azure-800:#033452;--pico-color-azure-750:#014063;--pico-color-azure-700:#014c75;--pico-color-azure-650:#015887;--pico-color-azure-600:#02659a;--pico-color-azure-550:#0172ad;--pico-color-azure-500:#017fc0;--pico-color-azure-450:#018cd4;--pico-color-azure-400:#029ae8;--pico-color-azure-350:#01aaff;--pico-color-azure-300:#51b4ff;--pico-color-azure-250:#79c0ff;--pico-color-azure-200:#9bccfd;--pico-color-azure-150:#b7d9fc;--pico-color-azure-100:#d1e5fb;--pico-color-azure-50:#e9f2fc;--pico-color-azure:#0172ad;--pico-color-cyan-950:#041413;--pico-color-cyan-900:#051f1f;--pico-color-cyan-850:#052b2b;--pico-color-cyan-800:#043737;--pico-color-cyan-750:#014343;--pico-color-cyan-700:#015050;--pico-color-cyan-650:#025d5d;--pico-color-cyan-600:#046a6a;--pico-color-cyan-550:#047878;--pico-color-cyan-500:#058686;--pico-color-cyan-450:#059494;--pico-color-cyan-400:#05a2a2;--pico-color-cyan-350:#0ab1b1;--pico-color-cyan-300:#0ac2c2;--pico-color-cyan-250:#0ccece;--pico-color-cyan-200:#25dddd;--pico-color-cyan-150:#3deceb;--pico-color-cyan-100:#58faf9;--pico-color-cyan-50:#c3fcfa;--pico-color-cyan:#047878;--pico-color-jade-950:#04140c;--pico-color-jade-900:#052014;--pico-color-jade-850:#042c1b;--pico-color-jade-800:#033823;--pico-color-jade-750:#00452b;--pico-color-jade-700:#015234;--pico-color-jade-650:#005f3d;--pico-color-jade-600:#006d46;--pico-color-jade-550:#007a50;--pico-color-jade-500:#00895a;--pico-color-jade-450:#029764;--pico-color-jade-400:#00a66e;--pico-color-jade-350:#00b478;--pico-color-jade-300:#00c482;--pico-color-jade-250:#00cc88;--pico-color-jade-200:#21e299;--pico-color-jade-150:#39f1a6;--pico-color-jade-100:#70fcba;--pico-color-jade-50:#cbfce1;--pico-color-jade:#007a50;--pico-color-green-950:#0b1305;--pico-color-green-900:#131f07;--pico-color-green-850:#152b07;--pico-color-green-800:#173806;--pico-color-green-750:#1a4405;--pico-color-green-700:#205107;--pico-color-green-650:#265e09;--pico-color-green-600:#2c6c0c;--pico-color-green-550:#33790f;--pico-color-green-500:#398712;--pico-color-green-450:#409614;--pico-color-green-400:#47a417;--pico-color-green-350:#4eb31b;--pico-color-green-300:#55c21e;--pico-color-green-250:#5dd121;--pico-color-green-200:#62d926;--pico-color-green-150:#77ef3d;--pico-color-green-100:#95fb62;--pico-color-green-50:#d7fbc1;--pico-color-green:#398712;--pico-color-lime-950:#101203;--pico-color-lime-900:#191d03;--pico-color-lime-850:#202902;--pico-color-lime-800:#273500;--pico-color-lime-750:#304100;--pico-color-lime-700:#394d00;--pico-color-lime-650:#435a00;--pico-color-lime-600:#4d6600;--pico-color-lime-550:#577400;--pico-color-lime-500:#628100;--pico-color-lime-450:#6c8f00;--pico-color-lime-400:#779c00;--pico-color-lime-350:#82ab00;--pico-color-lime-300:#8eb901;--pico-color-lime-250:#99c801;--pico-color-lime-200:#a5d601;--pico-color-lime-150:#b2e51a;--pico-color-lime-100:#c1f335;--pico-color-lime-50:#defc85;--pico-color-lime:#a5d601;--pico-color-yellow-950:#141103;--pico-color-yellow-900:#1f1c02;--pico-color-yellow-850:#2b2600;--pico-color-yellow-800:#363100;--pico-color-yellow-750:#423c00;--pico-color-yellow-700:#4e4700;--pico-color-yellow-650:#5b5300;--pico-color-yellow-600:#685f00;--pico-color-yellow-550:#756b00;--pico-color-yellow-500:#827800;--pico-color-yellow-450:#908501;--pico-color-yellow-400:#9e9200;--pico-color-yellow-350:#ad9f00;--pico-color-yellow-300:#bbac00;--pico-color-yellow-250:#caba01;--pico-color-yellow-200:#d9c800;--pico-color-yellow-150:#e8d600;--pico-color-yellow-100:#f2df0d;--pico-color-yellow-50:#fdf1b4;--pico-color-yellow:#f2df0d;--pico-color-amber-950:#161003;--pico-color-amber-900:#231a03;--pico-color-amber-850:#312302;--pico-color-amber-800:#3f2d00;--pico-color-amber-750:#4d3700;--pico-color-amber-700:#5b4200;--pico-color-amber-650:#694d00;--pico-color-amber-600:#785800;--pico-color-amber-550:#876400;--pico-color-amber-500:#977000;--pico-color-amber-450:#a77c00;--pico-color-amber-400:#b78800;--pico-color-amber-350:#c79400;--pico-color-amber-300:#d8a100;--pico-color-amber-250:#e8ae01;--pico-color-amber-200:#ffbf00;--pico-color-amber-150:#fecc63;--pico-color-amber-100:#fddea6;--pico-color-amber-50:#fcefd9;--pico-color-amber:#ffbf00;--pico-color-pumpkin-950:#180f04;--pico-color-pumpkin-900:#271805;--pico-color-pumpkin-850:#372004;--pico-color-pumpkin-800:#482802;--pico-color-pumpkin-750:#593100;--pico-color-pumpkin-700:#693a00;--pico-color-pumpkin-650:#7a4400;--pico-color-pumpkin-600:#8b4f00;--pico-color-pumpkin-550:#9c5900;--pico-color-pumpkin-500:#ad6400;--pico-color-pumpkin-450:#bf6e00;--pico-color-pumpkin-400:#d27a01;--pico-color-pumpkin-350:#e48500;--pico-color-pumpkin-300:#ff9500;--pico-color-pumpkin-250:#ffa23a;--pico-color-pumpkin-200:#feb670;--pico-color-pumpkin-150:#fcca9b;--pico-color-pumpkin-100:#fcdcc1;--pico-color-pumpkin-50:#fceee3;--pico-color-pumpkin:#ff9500;--pico-color-orange-950:#1b0d06;--pico-color-orange-900:#2d1509;--pico-color-orange-850:#411a0a;--pico-color-orange-800:#561e0a;--pico-color-orange-750:#6b220a;--pico-color-orange-700:#7f270b;--pico-color-orange-650:#942d0d;--pico-color-orange-600:#a83410;--pico-color-orange-550:#bd3c13;--pico-color-orange-500:#d24317;--pico-color-orange-450:#e74b1a;--pico-color-orange-400:#f45d2c;--pico-color-orange-350:#f56b3d;--pico-color-orange-300:#f68e68;--pico-color-orange-250:#f8a283;--pico-color-orange-200:#f8b79f;--pico-color-orange-150:#f8cab9;--pico-color-orange-100:#f9dcd2;--pico-color-orange-50:#faeeea;--pico-color-orange:#d24317;--pico-color-sand-950:#111110;--pico-color-sand-900:#1c1b19;--pico-color-sand-850:#272622;--pico-color-sand-800:#32302b;--pico-color-sand-750:#3d3b35;--pico-color-sand-700:#49463f;--pico-color-sand-650:#55524a;--pico-color-sand-600:#615e55;--pico-color-sand-550:#6e6a60;--pico-color-sand-500:#7b776b;--pico-color-sand-450:#888377;--pico-color-sand-400:#959082;--pico-color-sand-350:#a39e8f;--pico-color-sand-300:#b0ab9b;--pico-color-sand-250:#beb8a7;--pico-color-sand-200:#ccc6b4;--pico-color-sand-150:#dad4c2;--pico-color-sand-100:#e8e2d2;--pico-color-sand-50:#f2f0ec;--pico-color-sand:#ccc6b4;--pico-color-grey-950:#111111;--pico-color-grey-900:#1b1b1b;--pico-color-grey-850:#262626;--pico-color-grey-800:#303030;--pico-color-grey-750:#3b3b3b;--pico-color-grey-700:#474747;--pico-color-grey-650:#525252;--pico-color-grey-600:#5e5e5e;--pico-color-grey-550:#6a6a6a;--pico-color-grey-500:#777777;--pico-color-grey-450:#808080;--pico-color-grey-400:#919191;--pico-color-grey-350:#9e9e9e;--pico-color-grey-300:#ababab;--pico-color-grey-250:#b9b9b9;--pico-color-grey-200:#c6c6c6;--pico-color-grey-150:#d4d4d4;--pico-color-grey-100:#e2e2e2;--pico-color-grey-50:#f1f1f1;--pico-color-grey:#ababab;--pico-color-zinc-950:#0f1114;--pico-color-zinc-900:#191c20;--pico-color-zinc-850:#23262c;--pico-color-zinc-800:#2d3138;--pico-color-zinc-750:#373c44;--pico-color-zinc-700:#424751;--pico-color-zinc-650:#4d535e;--pico-color-zinc-600:#5c6370;--pico-color-zinc-550:#646b79;--pico-color-zinc-500:#6f7887;--pico-color-zinc-450:#7b8495;--pico-color-zinc-400:#8891a4;--pico-color-zinc-350:#969eaf;--pico-color-zinc-300:#a4acba;--pico-color-zinc-250:#b3b9c5;--pico-color-zinc-200:#c2c7d0;--pico-color-zinc-150:#d1d5db;--pico-color-zinc-100:#e0e3e7;--pico-color-zinc-50:#f0f1f3;--pico-color-zinc:#646b79;--pico-color-slate-950:#0e1118;--pico-color-slate-900:#181c25;--pico-color-slate-850:#202632;--pico-color-slate-800:#2a3140;--pico-color-slate-750:#333c4e;--pico-color-slate-700:#3d475c;--pico-color-slate-650:#48536b;--pico-color-slate-600:#525f7a;--pico-color-slate-550:#5d6b89;--pico-color-slate-500:#687899;--pico-color-slate-450:#7385a9;--pico-color-slate-400:#8191b5;--pico-color-slate-350:#909ebe;--pico-color-slate-300:#a0acc7;--pico-color-slate-250:#b0b9d0;--pico-color-slate-200:#bfc7d9;--pico-color-slate-150:#cfd5e2;--pico-color-slate-100:#dfe3eb;--pico-color-slate-50:#eff1f4;--pico-color-slate:#525f7a;--pico-color-light:#fff;--pico-color-dark:#000}.pico-color-red-950{color:var(--pico-color-red-950)}.pico-color-red-900{color:var(--pico-color-red-900)}.pico-color-red-850{color:var(--pico-color-red-850)}.pico-color-red-800{color:var(--pico-color-red-800)}.pico-color-red-750{color:var(--pico-color-red-750)}.pico-color-red-700{color:var(--pico-color-red-700)}.pico-color-red-650{color:var(--pico-color-red-650)}.pico-color-red-600{color:var(--pico-color-red-600)}.pico-color-red-550{color:var(--pico-color-red-550)}.pico-color-red-500{color:var(--pico-color-red-500)}.pico-color-red-450{color:var(--pico-color-red-450)}.pico-color-red-400{color:var(--pico-color-red-400)}.pico-color-red-350{color:var(--pico-color-red-350)}.pico-color-red-300{color:var(--pico-color-red-300)}.pico-color-red-250{color:var(--pico-color-red-250)}.pico-color-red-200{color:var(--pico-color-red-200)}.pico-color-red-150{color:var(--pico-color-red-150)}.pico-color-red-100{color:var(--pico-color-red-100)}.pico-color-red-50{color:var(--pico-color-red-50)}.pico-color-red{color:var(--pico-color-red)}.pico-color-pink-950{color:var(--pico-color-pink-950)}.pico-color-pink-900{color:var(--pico-color-pink-900)}.pico-color-pink-850{color:var(--pico-color-pink-850)}.pico-color-pink-800{color:var(--pico-color-pink-800)}.pico-color-pink-750{color:var(--pico-color-pink-750)}.pico-color-pink-700{color:var(--pico-color-pink-700)}.pico-color-pink-650{color:var(--pico-color-pink-650)}.pico-color-pink-600{color:var(--pico-color-pink-600)}.pico-color-pink-550{color:var(--pico-color-pink-550)}.pico-color-pink-500{color:var(--pico-color-pink-500)}.pico-color-pink-450{color:var(--pico-color-pink-450)}.pico-color-pink-400{color:var(--pico-color-pink-400)}.pico-color-pink-350{color:var(--pico-color-pink-350)}.pico-color-pink-300{color:var(--pico-color-pink-300)}.pico-color-pink-250{color:var(--pico-color-pink-250)}.pico-color-pink-200{color:var(--pico-color-pink-200)}.pico-color-pink-150{color:var(--pico-color-pink-150)}.pico-color-pink-100{color:var(--pico-color-pink-100)}.pico-color-pink-50{color:var(--pico-color-pink-50)}.pico-color-pink{color:var(--pico-color-pink)}.pico-color-fuchsia-950{color:var(--pico-color-fuchsia-950)}.pico-color-fuchsia-900{color:var(--pico-color-fuchsia-900)}.pico-color-fuchsia-850{color:var(--pico-color-fuchsia-850)}.pico-color-fuchsia-800{color:var(--pico-color-fuchsia-800)}.pico-color-fuchsia-750{color:var(--pico-color-fuchsia-750)}.pico-color-fuchsia-700{color:var(--pico-color-fuchsia-700)}.pico-color-fuchsia-650{color:var(--pico-color-fuchsia-650)}.pico-color-fuchsia-600{color:var(--pico-color-fuchsia-600)}.pico-color-fuchsia-550{color:var(--pico-color-fuchsia-550)}.pico-color-fuchsia-500{color:var(--pico-color-fuchsia-500)}.pico-color-fuchsia-450{color:var(--pico-color-fuchsia-450)}.pico-color-fuchsia-400{color:var(--pico-color-fuchsia-400)}.pico-color-fuchsia-350{color:var(--pico-color-fuchsia-350)}.pico-color-fuchsia-300{color:var(--pico-color-fuchsia-300)}.pico-color-fuchsia-250{color:var(--pico-color-fuchsia-250)}.pico-color-fuchsia-200{color:var(--pico-color-fuchsia-200)}.pico-color-fuchsia-150{color:var(--pico-color-fuchsia-150)}.pico-color-fuchsia-100{color:var(--pico-color-fuchsia-100)}.pico-color-fuchsia-50{color:var(--pico-color-fuchsia-50)}.pico-color-fuchsia{color:var(--pico-color-fuchsia)}.pico-color-purple-950{color:var(--pico-color-purple-950)}.pico-color-purple-900{color:var(--pico-color-purple-900)}.pico-color-purple-850{color:var(--pico-color-purple-850)}.pico-color-purple-800{color:var(--pico-color-purple-800)}.pico-color-purple-750{color:var(--pico-color-purple-750)}.pico-color-purple-700{color:var(--pico-color-purple-700)}.pico-color-purple-650{color:var(--pico-color-purple-650)}.pico-color-purple-600{color:var(--pico-color-purple-600)}.pico-color-purple-550{color:var(--pico-color-purple-550)}.pico-color-purple-500{color:var(--pico-color-purple-500)}.pico-color-purple-450{color:var(--pico-color-purple-450)}.pico-color-purple-400{color:var(--pico-color-purple-400)}.pico-color-purple-350{color:var(--pico-color-purple-350)}.pico-color-purple-300{color:var(--pico-color-purple-300)}.pico-color-purple-250{color:var(--pico-color-purple-250)}.pico-color-purple-200{color:var(--pico-color-purple-200)}.pico-color-purple-150{color:var(--pico-color-purple-150)}.pico-color-purple-100{color:var(--pico-color-purple-100)}.pico-color-purple-50{color:var(--pico-color-purple-50)}.pico-color-purple{color:var(--pico-color-purple)}.pico-color-violet-950{color:var(--pico-color-violet-950)}.pico-color-violet-900{color:var(--pico-color-violet-900)}.pico-color-violet-850{color:var(--pico-color-violet-850)}.pico-color-violet-800{color:var(--pico-color-violet-800)}.pico-color-violet-750{color:var(--pico-color-violet-750)}.pico-color-violet-700{color:var(--pico-color-violet-700)}.pico-color-violet-650{color:var(--pico-color-violet-650)}.pico-color-violet-600{color:var(--pico-color-violet-600)}.pico-color-violet-550{color:var(--pico-color-violet-550)}.pico-color-violet-500{color:var(--pico-color-violet-500)}.pico-color-violet-450{color:var(--pico-color-violet-450)}.pico-color-violet-400{color:var(--pico-color-violet-400)}.pico-color-violet-350{color:var(--pico-color-violet-350)}.pico-color-violet-300{color:var(--pico-color-violet-300)}.pico-color-violet-250{color:var(--pico-color-violet-250)}.pico-color-violet-200{color:var(--pico-color-violet-200)}.pico-color-violet-150{color:var(--pico-color-violet-150)}.pico-color-violet-100{color:var(--pico-color-violet-100)}.pico-color-violet-50{color:var(--pico-color-violet-50)}.pico-color-violet{color:var(--pico-color-violet)}.pico-color-indigo-950{color:var(--pico-color-indigo-950)}.pico-color-indigo-900{color:var(--pico-color-indigo-900)}.pico-color-indigo-850{color:var(--pico-color-indigo-850)}.pico-color-indigo-800{color:var(--pico-color-indigo-800)}.pico-color-indigo-750{color:var(--pico-color-indigo-750)}.pico-color-indigo-700{color:var(--pico-color-indigo-700)}.pico-color-indigo-650{color:var(--pico-color-indigo-650)}.pico-color-indigo-600{color:var(--pico-color-indigo-600)}.pico-color-indigo-550{color:var(--pico-color-indigo-550)}.pico-color-indigo-500{color:var(--pico-color-indigo-500)}.pico-color-indigo-450{color:var(--pico-color-indigo-450)}.pico-color-indigo-400{color:var(--pico-color-indigo-400)}.pico-color-indigo-350{color:var(--pico-color-indigo-350)}.pico-color-indigo-300{color:var(--pico-color-indigo-300)}.pico-color-indigo-250{color:var(--pico-color-indigo-250)}.pico-color-indigo-200{color:var(--pico-color-indigo-200)}.pico-color-indigo-150{color:var(--pico-color-indigo-150)}.pico-color-indigo-100{color:var(--pico-color-indigo-100)}.pico-color-indigo-50{color:var(--pico-color-indigo-50)}.pico-color-indigo{color:var(--pico-color-indigo)}.pico-color-blue-950{color:var(--pico-color-blue-950)}.pico-color-blue-900{color:var(--pico-color-blue-900)}.pico-color-blue-850{color:var(--pico-color-blue-850)}.pico-color-blue-800{color:var(--pico-color-blue-800)}.pico-color-blue-750{color:var(--pico-color-blue-750)}.pico-color-blue-700{color:var(--pico-color-blue-700)}.pico-color-blue-650{color:var(--pico-color-blue-650)}.pico-color-blue-600{color:var(--pico-color-blue-600)}.pico-color-blue-550{color:var(--pico-color-blue-550)}.pico-color-blue-500{color:var(--pico-color-blue-500)}.pico-color-blue-450{color:var(--pico-color-blue-450)}.pico-color-blue-400{color:var(--pico-color-blue-400)}.pico-color-blue-350{color:var(--pico-color-blue-350)}.pico-color-blue-300{color:var(--pico-color-blue-300)}.pico-color-blue-250{color:var(--pico-color-blue-250)}.pico-color-blue-200{color:var(--pico-color-blue-200)}.pico-color-blue-150{color:var(--pico-color-blue-150)}.pico-color-blue-100{color:var(--pico-color-blue-100)}.pico-color-blue-50{color:var(--pico-color-blue-50)}.pico-color-blue{color:var(--pico-color-blue)}.pico-color-azure-950{color:var(--pico-color-azure-950)}.pico-color-azure-900{color:var(--pico-color-azure-900)}.pico-color-azure-850{color:var(--pico-color-azure-850)}.pico-color-azure-800{color:var(--pico-color-azure-800)}.pico-color-azure-750{color:var(--pico-color-azure-750)}.pico-color-azure-700{color:var(--pico-color-azure-700)}.pico-color-azure-650{color:var(--pico-color-azure-650)}.pico-color-azure-600{color:var(--pico-color-azure-600)}.pico-color-azure-550{color:var(--pico-color-azure-550)}.pico-color-azure-500{color:var(--pico-color-azure-500)}.pico-color-azure-450{color:var(--pico-color-azure-450)}.pico-color-azure-400{color:var(--pico-color-azure-400)}.pico-color-azure-350{color:var(--pico-color-azure-350)}.pico-color-azure-300{color:var(--pico-color-azure-300)}.pico-color-azure-250{color:var(--pico-color-azure-250)}.pico-color-azure-200{color:var(--pico-color-azure-200)}.pico-color-azure-150{color:var(--pico-color-azure-150)}.pico-color-azure-100{color:var(--pico-color-azure-100)}.pico-color-azure-50{color:var(--pico-color-azure-50)}.pico-color-azure{color:var(--pico-color-azure)}.pico-color-cyan-950{color:var(--pico-color-cyan-950)}.pico-color-cyan-900{color:var(--pico-color-cyan-900)}.pico-color-cyan-850{color:var(--pico-color-cyan-850)}.pico-color-cyan-800{color:var(--pico-color-cyan-800)}.pico-color-cyan-750{color:var(--pico-color-cyan-750)}.pico-color-cyan-700{color:var(--pico-color-cyan-700)}.pico-color-cyan-650{color:var(--pico-color-cyan-650)}.pico-color-cyan-600{color:var(--pico-color-cyan-600)}.pico-color-cyan-550{color:var(--pico-color-cyan-550)}.pico-color-cyan-500{color:var(--pico-color-cyan-500)}.pico-color-cyan-450{color:var(--pico-color-cyan-450)}.pico-color-cyan-400{color:var(--pico-color-cyan-400)}.pico-color-cyan-350{color:var(--pico-color-cyan-350)}.pico-color-cyan-300{color:var(--pico-color-cyan-300)}.pico-color-cyan-250{color:var(--pico-color-cyan-250)}.pico-color-cyan-200{color:var(--pico-color-cyan-200)}.pico-color-cyan-150{color:var(--pico-color-cyan-150)}.pico-color-cyan-100{color:var(--pico-color-cyan-100)}.pico-color-cyan-50{color:var(--pico-color-cyan-50)}.pico-color-cyan{color:var(--pico-color-cyan)}.pico-color-jade-950{color:var(--pico-color-jade-950)}.pico-color-jade-900{color:var(--pico-color-jade-900)}.pico-color-jade-850{color:var(--pico-color-jade-850)}.pico-color-jade-800{color:var(--pico-color-jade-800)}.pico-color-jade-750{color:var(--pico-color-jade-750)}.pico-color-jade-700{color:var(--pico-color-jade-700)}.pico-color-jade-650{color:var(--pico-color-jade-650)}.pico-color-jade-600{color:var(--pico-color-jade-600)}.pico-color-jade-550{color:var(--pico-color-jade-550)}.pico-color-jade-500{color:var(--pico-color-jade-500)}.pico-color-jade-450{color:var(--pico-color-jade-450)}.pico-color-jade-400{color:var(--pico-color-jade-400)}.pico-color-jade-350{color:var(--pico-color-jade-350)}.pico-color-jade-300{color:var(--pico-color-jade-300)}.pico-color-jade-250{color:var(--pico-color-jade-250)}.pico-color-jade-200{color:var(--pico-color-jade-200)}.pico-color-jade-150{color:var(--pico-color-jade-150)}.pico-color-jade-100{color:var(--pico-color-jade-100)}.pico-color-jade-50{color:var(--pico-color-jade-50)}.pico-color-jade{color:var(--pico-color-jade)}.pico-color-green-950{color:var(--pico-color-green-950)}.pico-color-green-900{color:var(--pico-color-green-900)}.pico-color-green-850{color:var(--pico-color-green-850)}.pico-color-green-800{color:var(--pico-color-green-800)}.pico-color-green-750{color:var(--pico-color-green-750)}.pico-color-green-700{color:var(--pico-color-green-700)}.pico-color-green-650{color:var(--pico-color-green-650)}.pico-color-green-600{color:var(--pico-color-green-600)}.pico-color-green-550{color:var(--pico-color-green-550)}.pico-color-green-500{color:var(--pico-color-green-500)}.pico-color-green-450{color:var(--pico-color-green-450)}.pico-color-green-400{color:var(--pico-color-green-400)}.pico-color-green-350{color:var(--pico-color-green-350)}.pico-color-green-300{color:var(--pico-color-green-300)}.pico-color-green-250{color:var(--pico-color-green-250)}.pico-color-green-200{color:var(--pico-color-green-200)}.pico-color-green-150{color:var(--pico-color-green-150)}.pico-color-green-100{color:var(--pico-color-green-100)}.pico-color-green-50{color:var(--pico-color-green-50)}.pico-color-green{color:var(--pico-color-green)}.pico-color-lime-950{color:var(--pico-color-lime-950)}.pico-color-lime-900{color:var(--pico-color-lime-900)}.pico-color-lime-850{color:var(--pico-color-lime-850)}.pico-color-lime-800{color:var(--pico-color-lime-800)}.pico-color-lime-750{color:var(--pico-color-lime-750)}.pico-color-lime-700{color:var(--pico-color-lime-700)}.pico-color-lime-650{color:var(--pico-color-lime-650)}.pico-color-lime-600{color:var(--pico-color-lime-600)}.pico-color-lime-550{color:var(--pico-color-lime-550)}.pico-color-lime-500{color:var(--pico-color-lime-500)}.pico-color-lime-450{color:var(--pico-color-lime-450)}.pico-color-lime-400{color:var(--pico-color-lime-400)}.pico-color-lime-350{color:var(--pico-color-lime-350)}.pico-color-lime-300{color:var(--pico-color-lime-300)}.pico-color-lime-250{color:var(--pico-color-lime-250)}.pico-color-lime-200{color:var(--pico-color-lime-200)}.pico-color-lime-150{color:var(--pico-color-lime-150)}.pico-color-lime-100{color:var(--pico-color-lime-100)}.pico-color-lime-50{color:var(--pico-color-lime-50)}.pico-color-lime{color:var(--pico-color-lime)}.pico-color-yellow-950{color:var(--pico-color-yellow-950)}.pico-color-yellow-900{color:var(--pico-color-yellow-900)}.pico-color-yellow-850{color:var(--pico-color-yellow-850)}.pico-color-yellow-800{color:var(--pico-color-yellow-800)}.pico-color-yellow-750{color:var(--pico-color-yellow-750)}.pico-color-yellow-700{color:var(--pico-color-yellow-700)}.pico-color-yellow-650{color:var(--pico-color-yellow-650)}.pico-color-yellow-600{color:var(--pico-color-yellow-600)}.pico-color-yellow-550{color:var(--pico-color-yellow-550)}.pico-color-yellow-500{color:var(--pico-color-yellow-500)}.pico-color-yellow-450{color:var(--pico-color-yellow-450)}.pico-color-yellow-400{color:var(--pico-color-yellow-400)}.pico-color-yellow-350{color:var(--pico-color-yellow-350)}.pico-color-yellow-300{color:var(--pico-color-yellow-300)}.pico-color-yellow-250{color:var(--pico-color-yellow-250)}.pico-color-yellow-200{color:var(--pico-color-yellow-200)}.pico-color-yellow-150{color:var(--pico-color-yellow-150)}.pico-color-yellow-100{color:var(--pico-color-yellow-100)}.pico-color-yellow-50{color:var(--pico-color-yellow-50)}.pico-color-yellow{color:var(--pico-color-yellow)}.pico-color-amber-950{color:var(--pico-color-amber-950)}.pico-color-amber-900{color:var(--pico-color-amber-900)}.pico-color-amber-850{color:var(--pico-color-amber-850)}.pico-color-amber-800{color:var(--pico-color-amber-800)}.pico-color-amber-750{color:var(--pico-color-amber-750)}.pico-color-amber-700{color:var(--pico-color-amber-700)}.pico-color-amber-650{color:var(--pico-color-amber-650)}.pico-color-amber-600{color:var(--pico-color-amber-600)}.pico-color-amber-550{color:var(--pico-color-amber-550)}.pico-color-amber-500{color:var(--pico-color-amber-500)}.pico-color-amber-450{color:var(--pico-color-amber-450)}.pico-color-amber-400{color:var(--pico-color-amber-400)}.pico-color-amber-350{color:var(--pico-color-amber-350)}.pico-color-amber-300{color:var(--pico-color-amber-300)}.pico-color-amber-250{color:var(--pico-color-amber-250)}.pico-color-amber-200{color:var(--pico-color-amber-200)}.pico-color-amber-150{color:var(--pico-color-amber-150)}.pico-color-amber-100{color:var(--pico-color-amber-100)}.pico-color-amber-50{color:var(--pico-color-amber-50)}.pico-color-amber{color:var(--pico-color-amber)}.pico-color-pumpkin-950{color:var(--pico-color-pumpkin-950)}.pico-color-pumpkin-900{color:var(--pico-color-pumpkin-900)}.pico-color-pumpkin-850{color:var(--pico-color-pumpkin-850)}.pico-color-pumpkin-800{color:var(--pico-color-pumpkin-800)}.pico-color-pumpkin-750{color:var(--pico-color-pumpkin-750)}.pico-color-pumpkin-700{color:var(--pico-color-pumpkin-700)}.pico-color-pumpkin-650{color:var(--pico-color-pumpkin-650)}.pico-color-pumpkin-600{color:var(--pico-color-pumpkin-600)}.pico-color-pumpkin-550{color:var(--pico-color-pumpkin-550)}.pico-color-pumpkin-500{color:var(--pico-color-pumpkin-500)}.pico-color-pumpkin-450{color:var(--pico-color-pumpkin-450)}.pico-color-pumpkin-400{color:var(--pico-color-pumpkin-400)}.pico-color-pumpkin-350{color:var(--pico-color-pumpkin-350)}.pico-color-pumpkin-300{color:var(--pico-color-pumpkin-300)}.pico-color-pumpkin-250{color:var(--pico-color-pumpkin-250)}.pico-color-pumpkin-200{color:var(--pico-color-pumpkin-200)}.pico-color-pumpkin-150{color:var(--pico-color-pumpkin-150)}.pico-color-pumpkin-100{color:var(--pico-color-pumpkin-100)}.pico-color-pumpkin-50{color:var(--pico-color-pumpkin-50)}.pico-color-pumpkin{color:var(--pico-color-pumpkin)}.pico-color-orange-950{color:var(--pico-color-orange-950)}.pico-color-orange-900{color:var(--pico-color-orange-900)}.pico-color-orange-850{color:var(--pico-color-orange-850)}.pico-color-orange-800{color:var(--pico-color-orange-800)}.pico-color-orange-750{color:var(--pico-color-orange-750)}.pico-color-orange-700{color:var(--pico-color-orange-700)}.pico-color-orange-650{color:var(--pico-color-orange-650)}.pico-color-orange-600{color:var(--pico-color-orange-600)}.pico-color-orange-550{color:var(--pico-color-orange-550)}.pico-color-orange-500{color:var(--pico-color-orange-500)}.pico-color-orange-450{color:var(--pico-color-orange-450)}.pico-color-orange-400{color:var(--pico-color-orange-400)}.pico-color-orange-350{color:var(--pico-color-orange-350)}.pico-color-orange-300{color:var(--pico-color-orange-300)}.pico-color-orange-250{color:var(--pico-color-orange-250)}.pico-color-orange-200{color:var(--pico-color-orange-200)}.pico-color-orange-150{color:var(--pico-color-orange-150)}.pico-color-orange-100{color:var(--pico-color-orange-100)}.pico-color-orange-50{color:var(--pico-color-orange-50)}.pico-color-orange{color:var(--pico-color-orange)}.pico-color-sand-950{color:var(--pico-color-sand-950)}.pico-color-sand-900{color:var(--pico-color-sand-900)}.pico-color-sand-850{color:var(--pico-color-sand-850)}.pico-color-sand-800{color:var(--pico-color-sand-800)}.pico-color-sand-750{color:var(--pico-color-sand-750)}.pico-color-sand-700{color:var(--pico-color-sand-700)}.pico-color-sand-650{color:var(--pico-color-sand-650)}.pico-color-sand-600{color:var(--pico-color-sand-600)}.pico-color-sand-550{color:var(--pico-color-sand-550)}.pico-color-sand-500{color:var(--pico-color-sand-500)}.pico-color-sand-450{color:var(--pico-color-sand-450)}.pico-color-sand-400{color:var(--pico-color-sand-400)}.pico-color-sand-350{color:var(--pico-color-sand-350)}.pico-color-sand-300{color:var(--pico-color-sand-300)}.pico-color-sand-250{color:var(--pico-color-sand-250)}.pico-color-sand-200{color:var(--pico-color-sand-200)}.pico-color-sand-150{color:var(--pico-color-sand-150)}.pico-color-sand-100{color:var(--pico-color-sand-100)}.pico-color-sand-50{color:var(--pico-color-sand-50)}.pico-color-sand{color:var(--pico-color-sand)}.pico-color-grey-950{color:var(--pico-color-grey-950)}.pico-color-grey-900{color:var(--pico-color-grey-900)}.pico-color-grey-850{color:var(--pico-color-grey-850)}.pico-color-grey-800{color:var(--pico-color-grey-800)}.pico-color-grey-750{color:var(--pico-color-grey-750)}.pico-color-grey-700{color:var(--pico-color-grey-700)}.pico-color-grey-650{color:var(--pico-color-grey-650)}.pico-color-grey-600{color:var(--pico-color-grey-600)}.pico-color-grey-550{color:var(--pico-color-grey-550)}.pico-color-grey-500{color:var(--pico-color-grey-500)}.pico-color-grey-450{color:var(--pico-color-grey-450)}.pico-color-grey-400{color:var(--pico-color-grey-400)}.pico-color-grey-350{color:var(--pico-color-grey-350)}.pico-color-grey-300{color:var(--pico-color-grey-300)}.pico-color-grey-250{color:var(--pico-color-grey-250)}.pico-color-grey-200{color:var(--pico-color-grey-200)}.pico-color-grey-150{color:var(--pico-color-grey-150)}.pico-color-grey-100{color:var(--pico-color-grey-100)}.pico-color-grey-50{color:var(--pico-color-grey-50)}.pico-color-grey{color:var(--pico-color-grey)}.pico-color-zinc-950{color:var(--pico-color-zinc-950)}.pico-color-zinc-900{color:var(--pico-color-zinc-900)}.pico-color-zinc-850{color:var(--pico-color-zinc-850)}.pico-color-zinc-800{color:var(--pico-color-zinc-800)}.pico-color-zinc-750{color:var(--pico-color-zinc-750)}.pico-color-zinc-700{color:var(--pico-color-zinc-700)}.pico-color-zinc-650{color:var(--pico-color-zinc-650)}.pico-color-zinc-600{color:var(--pico-color-zinc-600)}.pico-color-zinc-550{color:var(--pico-color-zinc-550)}.pico-color-zinc-500{color:var(--pico-color-zinc-500)}.pico-color-zinc-450{color:var(--pico-color-zinc-450)}.pico-color-zinc-400{color:var(--pico-color-zinc-400)}.pico-color-zinc-350{color:var(--pico-color-zinc-350)}.pico-color-zinc-300{color:var(--pico-color-zinc-300)}.pico-color-zinc-250{color:var(--pico-color-zinc-250)}.pico-color-zinc-200{color:var(--pico-color-zinc-200)}.pico-color-zinc-150{color:var(--pico-color-zinc-150)}.pico-color-zinc-100{color:var(--pico-color-zinc-100)}.pico-color-zinc-50{color:var(--pico-color-zinc-50)}.pico-color-zinc{color:var(--pico-color-zinc)}.pico-color-slate-950{color:var(--pico-color-slate-950)}.pico-color-slate-900{color:var(--pico-color-slate-900)}.pico-color-slate-850{color:var(--pico-color-slate-850)}.pico-color-slate-800{color:var(--pico-color-slate-800)}.pico-color-slate-750{color:var(--pico-color-slate-750)}.pico-color-slate-700{color:var(--pico-color-slate-700)}.pico-color-slate-650{color:var(--pico-color-slate-650)}.pico-color-slate-600{color:var(--pico-color-slate-600)}.pico-color-slate-550{color:var(--pico-color-slate-550)}.pico-color-slate-500{color:var(--pico-color-slate-500)}.pico-color-slate-450{color:var(--pico-color-slate-450)}.pico-color-slate-400{color:var(--pico-color-slate-400)}.pico-color-slate-350{color:var(--pico-color-slate-350)}.pico-color-slate-300{color:var(--pico-color-slate-300)}.pico-color-slate-250{color:var(--pico-color-slate-250)}.pico-color-slate-200{color:var(--pico-color-slate-200)}.pico-color-slate-150{color:var(--pico-color-slate-150)}.pico-color-slate-100{color:var(--pico-color-slate-100)}.pico-color-slate-50{color:var(--pico-color-slate-50)}.pico-color-slate{color:var(--pico-color-slate)}.pico-background-red-950{background-color:var(--pico-color-red-950);color:var(--pico-color-light)}.pico-background-red-900{background-color:var(--pico-color-red-900);color:var(--pico-color-light)}.pico-background-red-850{background-color:var(--pico-color-red-850);color:var(--pico-color-light)}.pico-background-red-800{background-color:var(--pico-color-red-800);color:var(--pico-color-light)}.pico-background-red-750{background-color:var(--pico-color-red-750);color:var(--pico-color-light)}.pico-background-red-700{background-color:var(--pico-color-red-700);color:var(--pico-color-light)}.pico-background-red-650{background-color:var(--pico-color-red-650);color:var(--pico-color-light)}.pico-background-red-600{background-color:var(--pico-color-red-600);color:var(--pico-color-light)}.pico-background-red-550{background-color:var(--pico-color-red-550);color:var(--pico-color-light)}.pico-background-red-500{background-color:var(--pico-color-red-500);color:var(--pico-color-light)}.pico-background-red-450{background-color:var(--pico-color-red-450);color:var(--pico-color-light)}.pico-background-red-400{background-color:var(--pico-color-red-400);color:var(--pico-color-dark)}.pico-background-red-350{background-color:var(--pico-color-red-350);color:var(--pico-color-dark)}.pico-background-red-300{background-color:var(--pico-color-red-300);color:var(--pico-color-dark)}.pico-background-red-250{background-color:var(--pico-color-red-250);color:var(--pico-color-dark)}.pico-background-red-200{background-color:var(--pico-color-red-200);color:var(--pico-color-dark)}.pico-background-red-150{background-color:var(--pico-color-red-150);color:var(--pico-color-dark)}.pico-background-red-100{background-color:var(--pico-color-red-100);color:var(--pico-color-dark)}.pico-background-red-50{background-color:var(--pico-color-red-50);color:var(--pico-color-dark)}.pico-background-red{background-color:var(--pico-color-red);color:var(--pico-color-light)}.pico-background-pink-950{background-color:var(--pico-color-pink-950);color:var(--pico-color-light)}.pico-background-pink-900{background-color:var(--pico-color-pink-900);color:var(--pico-color-light)}.pico-background-pink-850{background-color:var(--pico-color-pink-850);color:var(--pico-color-light)}.pico-background-pink-800{background-color:var(--pico-color-pink-800);color:var(--pico-color-light)}.pico-background-pink-750{background-color:var(--pico-color-pink-750);color:var(--pico-color-light)}.pico-background-pink-700{background-color:var(--pico-color-pink-700);color:var(--pico-color-light)}.pico-background-pink-650{background-color:var(--pico-color-pink-650);color:var(--pico-color-light)}.pico-background-pink-600{background-color:var(--pico-color-pink-600);color:var(--pico-color-light)}.pico-background-pink-550{background-color:var(--pico-color-pink-550);color:var(--pico-color-light)}.pico-background-pink-500{background-color:var(--pico-color-pink-500);color:var(--pico-color-light)}.pico-background-pink-450{background-color:var(--pico-color-pink-450);color:var(--pico-color-light)}.pico-background-pink-400{background-color:var(--pico-color-pink-400);color:var(--pico-color-dark)}.pico-background-pink-350{background-color:var(--pico-color-pink-350);color:var(--pico-color-dark)}.pico-background-pink-300{background-color:var(--pico-color-pink-300);color:var(--pico-color-dark)}.pico-background-pink-250{background-color:var(--pico-color-pink-250);color:var(--pico-color-dark)}.pico-background-pink-200{background-color:var(--pico-color-pink-200);color:var(--pico-color-dark)}.pico-background-pink-150{background-color:var(--pico-color-pink-150);color:var(--pico-color-dark)}.pico-background-pink-100{background-color:var(--pico-color-pink-100);color:var(--pico-color-dark)}.pico-background-pink-50{background-color:var(--pico-color-pink-50);color:var(--pico-color-dark)}.pico-background-pink{background-color:var(--pico-color-pink);color:var(--pico-color-light)}.pico-background-fuchsia-950{background-color:var(--pico-color-fuchsia-950);color:var(--pico-color-light)}.pico-background-fuchsia-900{background-color:var(--pico-color-fuchsia-900);color:var(--pico-color-light)}.pico-background-fuchsia-850{background-color:var(--pico-color-fuchsia-850);color:var(--pico-color-light)}.pico-background-fuchsia-800{background-color:var(--pico-color-fuchsia-800);color:var(--pico-color-light)}.pico-background-fuchsia-750{background-color:var(--pico-color-fuchsia-750);color:var(--pico-color-light)}.pico-background-fuchsia-700{background-color:var(--pico-color-fuchsia-700);color:var(--pico-color-light)}.pico-background-fuchsia-650{background-color:var(--pico-color-fuchsia-650);color:var(--pico-color-light)}.pico-background-fuchsia-600{background-color:var(--pico-color-fuchsia-600);color:var(--pico-color-light)}.pico-background-fuchsia-550{background-color:var(--pico-color-fuchsia-550);color:var(--pico-color-light)}.pico-background-fuchsia-500{background-color:var(--pico-color-fuchsia-500);color:var(--pico-color-light)}.pico-background-fuchsia-450{background-color:var(--pico-color-fuchsia-450);color:var(--pico-color-light)}.pico-background-fuchsia-400{background-color:var(--pico-color-fuchsia-400);color:var(--pico-color-dark)}.pico-background-fuchsia-350{background-color:var(--pico-color-fuchsia-350);color:var(--pico-color-dark)}.pico-background-fuchsia-300{background-color:var(--pico-color-fuchsia-300);color:var(--pico-color-dark)}.pico-background-fuchsia-250{background-color:var(--pico-color-fuchsia-250);color:var(--pico-color-dark)}.pico-background-fuchsia-200{background-color:var(--pico-color-fuchsia-200);color:var(--pico-color-dark)}.pico-background-fuchsia-150{background-color:var(--pico-color-fuchsia-150);color:var(--pico-color-dark)}.pico-background-fuchsia-100{background-color:var(--pico-color-fuchsia-100);color:var(--pico-color-dark)}.pico-background-fuchsia-50{background-color:var(--pico-color-fuchsia-50);color:var(--pico-color-dark)}.pico-background-fuchsia{background-color:var(--pico-color-fuchsia);color:var(--pico-color-light)}.pico-background-purple-950{background-color:var(--pico-color-purple-950);color:var(--pico-color-light)}.pico-background-purple-900{background-color:var(--pico-color-purple-900);color:var(--pico-color-light)}.pico-background-purple-850{background-color:var(--pico-color-purple-850);color:var(--pico-color-light)}.pico-background-purple-800{background-color:var(--pico-color-purple-800);color:var(--pico-color-light)}.pico-background-purple-750{background-color:var(--pico-color-purple-750);color:var(--pico-color-light)}.pico-background-purple-700{background-color:var(--pico-color-purple-700);color:var(--pico-color-light)}.pico-background-purple-650{background-color:var(--pico-color-purple-650);color:var(--pico-color-light)}.pico-background-purple-600{background-color:var(--pico-color-purple-600);color:var(--pico-color-light)}.pico-background-purple-550{background-color:var(--pico-color-purple-550);color:var(--pico-color-light)}.pico-background-purple-500{background-color:var(--pico-color-purple-500);color:var(--pico-color-light)}.pico-background-purple-450{background-color:var(--pico-color-purple-450);color:var(--pico-color-dark)}.pico-background-purple-400{background-color:var(--pico-color-purple-400);color:var(--pico-color-dark)}.pico-background-purple-350{background-color:var(--pico-color-purple-350);color:var(--pico-color-dark)}.pico-background-purple-300{background-color:var(--pico-color-purple-300);color:var(--pico-color-dark)}.pico-background-purple-250{background-color:var(--pico-color-purple-250);color:var(--pico-color-dark)}.pico-background-purple-200{background-color:var(--pico-color-purple-200);color:var(--pico-color-dark)}.pico-background-purple-150{background-color:var(--pico-color-purple-150);color:var(--pico-color-dark)}.pico-background-purple-100{background-color:var(--pico-color-purple-100);color:var(--pico-color-dark)}.pico-background-purple-50{background-color:var(--pico-color-purple-50);color:var(--pico-color-dark)}.pico-background-purple{background-color:var(--pico-color-purple);color:var(--pico-color-light)}.pico-background-violet-950{background-color:var(--pico-color-violet-950);color:var(--pico-color-light)}.pico-background-violet-900{background-color:var(--pico-color-violet-900);color:var(--pico-color-light)}.pico-background-violet-850{background-color:var(--pico-color-violet-850);color:var(--pico-color-light)}.pico-background-violet-800{background-color:var(--pico-color-violet-800);color:var(--pico-color-light)}.pico-background-violet-750{background-color:var(--pico-color-violet-750);color:var(--pico-color-light)}.pico-background-violet-700{background-color:var(--pico-color-violet-700);color:var(--pico-color-light)}.pico-background-violet-650{background-color:var(--pico-color-violet-650);color:var(--pico-color-light)}.pico-background-violet-600{background-color:var(--pico-color-violet-600);color:var(--pico-color-light)}.pico-background-violet-550{background-color:var(--pico-color-violet-550);color:var(--pico-color-light)}.pico-background-violet-500{background-color:var(--pico-color-violet-500);color:var(--pico-color-light)}.pico-background-violet-450{background-color:var(--pico-color-violet-450);color:var(--pico-color-dark)}.pico-background-violet-400{background-color:var(--pico-color-violet-400);color:var(--pico-color-dark)}.pico-background-violet-350{background-color:var(--pico-color-violet-350);color:var(--pico-color-dark)}.pico-background-violet-300{background-color:var(--pico-color-violet-300);color:var(--pico-color-dark)}.pico-background-violet-250{background-color:var(--pico-color-violet-250);color:var(--pico-color-dark)}.pico-background-violet-200{background-color:var(--pico-color-violet-200);color:var(--pico-color-dark)}.pico-background-violet-150{background-color:var(--pico-color-violet-150);color:var(--pico-color-dark)}.pico-background-violet-100{background-color:var(--pico-color-violet-100);color:var(--pico-color-dark)}.pico-background-violet-50{background-color:var(--pico-color-violet-50);color:var(--pico-color-dark)}.pico-background-violet{background-color:var(--pico-color-violet);color:var(--pico-color-light)}.pico-background-indigo-950{background-color:var(--pico-color-indigo-950);color:var(--pico-color-light)}.pico-background-indigo-900{background-color:var(--pico-color-indigo-900);color:var(--pico-color-light)}.pico-background-indigo-850{background-color:var(--pico-color-indigo-850);color:var(--pico-color-light)}.pico-background-indigo-800{background-color:var(--pico-color-indigo-800);color:var(--pico-color-light)}.pico-background-indigo-750{background-color:var(--pico-color-indigo-750);color:var(--pico-color-light)}.pico-background-indigo-700{background-color:var(--pico-color-indigo-700);color:var(--pico-color-light)}.pico-background-indigo-650{background-color:var(--pico-color-indigo-650);color:var(--pico-color-light)}.pico-background-indigo-600{background-color:var(--pico-color-indigo-600);color:var(--pico-color-light)}.pico-background-indigo-550{background-color:var(--pico-color-indigo-550);color:var(--pico-color-light)}.pico-background-indigo-500{background-color:var(--pico-color-indigo-500);color:var(--pico-color-light)}.pico-background-indigo-450{background-color:var(--pico-color-indigo-450);color:var(--pico-color-dark)}.pico-background-indigo-400{background-color:var(--pico-color-indigo-400);color:var(--pico-color-dark)}.pico-background-indigo-350{background-color:var(--pico-color-indigo-350);color:var(--pico-color-dark)}.pico-background-indigo-300{background-color:var(--pico-color-indigo-300);color:var(--pico-color-dark)}.pico-background-indigo-250{background-color:var(--pico-color-indigo-250);color:var(--pico-color-dark)}.pico-background-indigo-200{background-color:var(--pico-color-indigo-200);color:var(--pico-color-dark)}.pico-background-indigo-150{background-color:var(--pico-color-indigo-150);color:var(--pico-color-dark)}.pico-background-indigo-100{background-color:var(--pico-color-indigo-100);color:var(--pico-color-dark)}.pico-background-indigo-50{background-color:var(--pico-color-indigo-50);color:var(--pico-color-dark)}.pico-background-indigo{background-color:var(--pico-color-indigo);color:var(--pico-color-light)}.pico-background-blue-950{background-color:var(--pico-color-blue-950);color:var(--pico-color-light)}.pico-background-blue-900{background-color:var(--pico-color-blue-900);color:var(--pico-color-light)}.pico-background-blue-850{background-color:var(--pico-color-blue-850);color:var(--pico-color-light)}.pico-background-blue-800{background-color:var(--pico-color-blue-800);color:var(--pico-color-light)}.pico-background-blue-750{background-color:var(--pico-color-blue-750);color:var(--pico-color-light)}.pico-background-blue-700{background-color:var(--pico-color-blue-700);color:var(--pico-color-light)}.pico-background-blue-650{background-color:var(--pico-color-blue-650);color:var(--pico-color-light)}.pico-background-blue-600{background-color:var(--pico-color-blue-600);color:var(--pico-color-light)}.pico-background-blue-550{background-color:var(--pico-color-blue-550);color:var(--pico-color-light)}.pico-background-blue-500{background-color:var(--pico-color-blue-500);color:var(--pico-color-light)}.pico-background-blue-450{background-color:var(--pico-color-blue-450);color:var(--pico-color-dark)}.pico-background-blue-400{background-color:var(--pico-color-blue-400);color:var(--pico-color-dark)}.pico-background-blue-350{background-color:var(--pico-color-blue-350);color:var(--pico-color-dark)}.pico-background-blue-300{background-color:var(--pico-color-blue-300);color:var(--pico-color-dark)}.pico-background-blue-250{background-color:var(--pico-color-blue-250);color:var(--pico-color-dark)}.pico-background-blue-200{background-color:var(--pico-color-blue-200);color:var(--pico-color-dark)}.pico-background-blue-150{background-color:var(--pico-color-blue-150);color:var(--pico-color-dark)}.pico-background-blue-100{background-color:var(--pico-color-blue-100);color:var(--pico-color-dark)}.pico-background-blue-50{background-color:var(--pico-color-blue-50);color:var(--pico-color-dark)}.pico-background-blue{background-color:var(--pico-color-blue);color:var(--pico-color-light)}.pico-background-azure-950{background-color:var(--pico-color-azure-950);color:var(--pico-color-light)}.pico-background-azure-900{background-color:var(--pico-color-azure-900);color:var(--pico-color-light)}.pico-background-azure-850{background-color:var(--pico-color-azure-850);color:var(--pico-color-light)}.pico-background-azure-800{background-color:var(--pico-color-azure-800);color:var(--pico-color-light)}.pico-background-azure-750{background-color:var(--pico-color-azure-750);color:var(--pico-color-light)}.pico-background-azure-700{background-color:var(--pico-color-azure-700);color:var(--pico-color-light)}.pico-background-azure-650{background-color:var(--pico-color-azure-650);color:var(--pico-color-light)}.pico-background-azure-600{background-color:var(--pico-color-azure-600);color:var(--pico-color-light)}.pico-background-azure-550{background-color:var(--pico-color-azure-550);color:var(--pico-color-light)}.pico-background-azure-500{background-color:var(--pico-color-azure-500);color:var(--pico-color-light)}.pico-background-azure-450{background-color:var(--pico-color-azure-450);color:var(--pico-color-light)}.pico-background-azure-400{background-color:var(--pico-color-azure-400);color:var(--pico-color-light)}.pico-background-azure-350{background-color:var(--pico-color-azure-350);color:var(--pico-color-dark)}.pico-background-azure-300{background-color:var(--pico-color-azure-300);color:var(--pico-color-dark)}.pico-background-azure-250{background-color:var(--pico-color-azure-250);color:var(--pico-color-dark)}.pico-background-azure-200{background-color:var(--pico-color-azure-200);color:var(--pico-color-dark)}.pico-background-azure-150{background-color:var(--pico-color-azure-150);color:var(--pico-color-dark)}.pico-background-azure-100{background-color:var(--pico-color-azure-100);color:var(--pico-color-dark)}.pico-background-azure-50{background-color:var(--pico-color-azure-50);color:var(--pico-color-dark)}.pico-background-azure{background-color:var(--pico-color-azure);color:var(--pico-color-light)}.pico-background-cyan-950{background-color:var(--pico-color-cyan-950);color:var(--pico-color-light)}.pico-background-cyan-900{background-color:var(--pico-color-cyan-900);color:var(--pico-color-light)}.pico-background-cyan-850{background-color:var(--pico-color-cyan-850);color:var(--pico-color-light)}.pico-background-cyan-800{background-color:var(--pico-color-cyan-800);color:var(--pico-color-light)}.pico-background-cyan-750{background-color:var(--pico-color-cyan-750);color:var(--pico-color-light)}.pico-background-cyan-700{background-color:var(--pico-color-cyan-700);color:var(--pico-color-light)}.pico-background-cyan-650{background-color:var(--pico-color-cyan-650);color:var(--pico-color-light)}.pico-background-cyan-600{background-color:var(--pico-color-cyan-600);color:var(--pico-color-light)}.pico-background-cyan-550{background-color:var(--pico-color-cyan-550);color:var(--pico-color-light)}.pico-background-cyan-500{background-color:var(--pico-color-cyan-500);color:var(--pico-color-light)}.pico-background-cyan-450{background-color:var(--pico-color-cyan-450);color:var(--pico-color-light)}.pico-background-cyan-400{background-color:var(--pico-color-cyan-400);color:var(--pico-color-light)}.pico-background-cyan-350{background-color:var(--pico-color-cyan-350);color:var(--pico-color-light)}.pico-background-cyan-300{background-color:var(--pico-color-cyan-300);color:var(--pico-color-dark)}.pico-background-cyan-250{background-color:var(--pico-color-cyan-250);color:var(--pico-color-dark)}.pico-background-cyan-200{background-color:var(--pico-color-cyan-200);color:var(--pico-color-dark)}.pico-background-cyan-150{background-color:var(--pico-color-cyan-150);color:var(--pico-color-dark)}.pico-background-cyan-100{background-color:var(--pico-color-cyan-100);color:var(--pico-color-dark)}.pico-background-cyan-50{background-color:var(--pico-color-cyan-50);color:var(--pico-color-dark)}.pico-background-cyan{background-color:var(--pico-color-cyan);color:var(--pico-color-light)}.pico-background-jade-950{background-color:var(--pico-color-jade-950);color:var(--pico-color-light)}.pico-background-jade-900{background-color:var(--pico-color-jade-900);color:var(--pico-color-light)}.pico-background-jade-850{background-color:var(--pico-color-jade-850);color:var(--pico-color-light)}.pico-background-jade-800{background-color:var(--pico-color-jade-800);color:var(--pico-color-light)}.pico-background-jade-750{background-color:var(--pico-color-jade-750);color:var(--pico-color-light)}.pico-background-jade-700{background-color:var(--pico-color-jade-700);color:var(--pico-color-light)}.pico-background-jade-650{background-color:var(--pico-color-jade-650);color:var(--pico-color-light)}.pico-background-jade-600{background-color:var(--pico-color-jade-600);color:var(--pico-color-light)}.pico-background-jade-550{background-color:var(--pico-color-jade-550);color:var(--pico-color-light)}.pico-background-jade-500{background-color:var(--pico-color-jade-500);color:var(--pico-color-light)}.pico-background-jade-450{background-color:var(--pico-color-jade-450);color:var(--pico-color-light)}.pico-background-jade-400{background-color:var(--pico-color-jade-400);color:var(--pico-color-light)}.pico-background-jade-350{background-color:var(--pico-color-jade-350);color:var(--pico-color-light)}.pico-background-jade-300{background-color:var(--pico-color-jade-300);color:var(--pico-color-dark)}.pico-background-jade-250{background-color:var(--pico-color-jade-250);color:var(--pico-color-dark)}.pico-background-jade-200{background-color:var(--pico-color-jade-200);color:var(--pico-color-dark)}.pico-background-jade-150{background-color:var(--pico-color-jade-150);color:var(--pico-color-dark)}.pico-background-jade-100{background-color:var(--pico-color-jade-100);color:var(--pico-color-dark)}.pico-background-jade-50{background-color:var(--pico-color-jade-50);color:var(--pico-color-dark)}.pico-background-jade{background-color:var(--pico-color-jade);color:var(--pico-color-light)}.pico-background-green-950{background-color:var(--pico-color-green-950);color:var(--pico-color-light)}.pico-background-green-900{background-color:var(--pico-color-green-900);color:var(--pico-color-light)}.pico-background-green-850{background-color:var(--pico-color-green-850);color:var(--pico-color-light)}.pico-background-green-800{background-color:var(--pico-color-green-800);color:var(--pico-color-light)}.pico-background-green-750{background-color:var(--pico-color-green-750);color:var(--pico-color-light)}.pico-background-green-700{background-color:var(--pico-color-green-700);color:var(--pico-color-light)}.pico-background-green-650{background-color:var(--pico-color-green-650);color:var(--pico-color-light)}.pico-background-green-600{background-color:var(--pico-color-green-600);color:var(--pico-color-light)}.pico-background-green-550{background-color:var(--pico-color-green-550);color:var(--pico-color-light)}.pico-background-green-500{background-color:var(--pico-color-green-500);color:var(--pico-color-light)}.pico-background-green-450{background-color:var(--pico-color-green-450);color:var(--pico-color-light)}.pico-background-green-400{background-color:var(--pico-color-green-400);color:var(--pico-color-light)}.pico-background-green-350{background-color:var(--pico-color-green-350);color:var(--pico-color-dark)}.pico-background-green-300{background-color:var(--pico-color-green-300);color:var(--pico-color-dark)}.pico-background-green-250{background-color:var(--pico-color-green-250);color:var(--pico-color-dark)}.pico-background-green-200{background-color:var(--pico-color-green-200);color:var(--pico-color-dark)}.pico-background-green-150{background-color:var(--pico-color-green-150);color:var(--pico-color-dark)}.pico-background-green-100{background-color:var(--pico-color-green-100);color:var(--pico-color-dark)}.pico-background-green-50{background-color:var(--pico-color-green-50);color:var(--pico-color-dark)}.pico-background-green{background-color:var(--pico-color-green);color:var(--pico-color-light)}.pico-background-lime-950{background-color:var(--pico-color-lime-950);color:var(--pico-color-light)}.pico-background-lime-900{background-color:var(--pico-color-lime-900);color:var(--pico-color-light)}.pico-background-lime-850{background-color:var(--pico-color-lime-850);color:var(--pico-color-light)}.pico-background-lime-800{background-color:var(--pico-color-lime-800);color:var(--pico-color-light)}.pico-background-lime-750{background-color:var(--pico-color-lime-750);color:var(--pico-color-light)}.pico-background-lime-700{background-color:var(--pico-color-lime-700);color:var(--pico-color-light)}.pico-background-lime-650{background-color:var(--pico-color-lime-650);color:var(--pico-color-light)}.pico-background-lime-600{background-color:var(--pico-color-lime-600);color:var(--pico-color-light)}.pico-background-lime-550{background-color:var(--pico-color-lime-550);color:var(--pico-color-light)}.pico-background-lime-500{background-color:var(--pico-color-lime-500);color:var(--pico-color-light)}.pico-background-lime-450{background-color:var(--pico-color-lime-450);color:var(--pico-color-light)}.pico-background-lime-400{background-color:var(--pico-color-lime-400);color:var(--pico-color-light)}.pico-background-lime-350{background-color:var(--pico-color-lime-350);color:var(--pico-color-dark)}.pico-background-lime-300{background-color:var(--pico-color-lime-300);color:var(--pico-color-dark)}.pico-background-lime-250{background-color:var(--pico-color-lime-250);color:var(--pico-color-dark)}.pico-background-lime-200{background-color:var(--pico-color-lime-200);color:var(--pico-color-dark)}.pico-background-lime-150{background-color:var(--pico-color-lime-150);color:var(--pico-color-dark)}.pico-background-lime-100{background-color:var(--pico-color-lime-100);color:var(--pico-color-dark)}.pico-background-lime-50{background-color:var(--pico-color-lime-50);color:var(--pico-color-dark)}.pico-background-lime{background-color:var(--pico-color-lime);color:var(--pico-color-dark)}.pico-background-yellow-950{background-color:var(--pico-color-yellow-950);color:var(--pico-color-light)}.pico-background-yellow-900{background-color:var(--pico-color-yellow-900);color:var(--pico-color-light)}.pico-background-yellow-850{background-color:var(--pico-color-yellow-850);color:var(--pico-color-light)}.pico-background-yellow-800{background-color:var(--pico-color-yellow-800);color:var(--pico-color-light)}.pico-background-yellow-750{background-color:var(--pico-color-yellow-750);color:var(--pico-color-light)}.pico-background-yellow-700{background-color:var(--pico-color-yellow-700);color:var(--pico-color-light)}.pico-background-yellow-650{background-color:var(--pico-color-yellow-650);color:var(--pico-color-light)}.pico-background-yellow-600{background-color:var(--pico-color-yellow-600);color:var(--pico-color-light)}.pico-background-yellow-550{background-color:var(--pico-color-yellow-550);color:var(--pico-color-light)}.pico-background-yellow-500{background-color:var(--pico-color-yellow-500);color:var(--pico-color-light)}.pico-background-yellow-450{background-color:var(--pico-color-yellow-450);color:var(--pico-color-light)}.pico-background-yellow-400{background-color:var(--pico-color-yellow-400);color:var(--pico-color-dark)}.pico-background-yellow-350{background-color:var(--pico-color-yellow-350);color:var(--pico-color-dark)}.pico-background-yellow-300{background-color:var(--pico-color-yellow-300);color:var(--pico-color-dark)}.pico-background-yellow-250{background-color:var(--pico-color-yellow-250);color:var(--pico-color-dark)}.pico-background-yellow-200{background-color:var(--pico-color-yellow-200);color:var(--pico-color-dark)}.pico-background-yellow-150{background-color:var(--pico-color-yellow-150);color:var(--pico-color-dark)}.pico-background-yellow-100{background-color:var(--pico-color-yellow-100);color:var(--pico-color-dark)}.pico-background-yellow-50{background-color:var(--pico-color-yellow-50);color:var(--pico-color-dark)}.pico-background-yellow{background-color:var(--pico-color-yellow);color:var(--pico-color-dark)}.pico-background-amber-950{background-color:var(--pico-color-amber-950);color:var(--pico-color-light)}.pico-background-amber-900{background-color:var(--pico-color-amber-900);color:var(--pico-color-light)}.pico-background-amber-850{background-color:var(--pico-color-amber-850);color:var(--pico-color-light)}.pico-background-amber-800{background-color:var(--pico-color-amber-800);color:var(--pico-color-light)}.pico-background-amber-750{background-color:var(--pico-color-amber-750);color:var(--pico-color-light)}.pico-background-amber-700{background-color:var(--pico-color-amber-700);color:var(--pico-color-light)}.pico-background-amber-650{background-color:var(--pico-color-amber-650);color:var(--pico-color-light)}.pico-background-amber-600{background-color:var(--pico-color-amber-600);color:var(--pico-color-light)}.pico-background-amber-550{background-color:var(--pico-color-amber-550);color:var(--pico-color-light)}.pico-background-amber-500{background-color:var(--pico-color-amber-500);color:var(--pico-color-light)}.pico-background-amber-450{background-color:var(--pico-color-amber-450);color:var(--pico-color-light)}.pico-background-amber-400{background-color:var(--pico-color-amber-400);color:var(--pico-color-dark)}.pico-background-amber-350{background-color:var(--pico-color-amber-350);color:var(--pico-color-dark)}.pico-background-amber-300{background-color:var(--pico-color-amber-300);color:var(--pico-color-dark)}.pico-background-amber-250{background-color:var(--pico-color-amber-250);color:var(--pico-color-dark)}.pico-background-amber-200{background-color:var(--pico-color-amber-200);color:var(--pico-color-dark)}.pico-background-amber-150{background-color:var(--pico-color-amber-150);color:var(--pico-color-dark)}.pico-background-amber-100{background-color:var(--pico-color-amber-100);color:var(--pico-color-dark)}.pico-background-amber-50{background-color:var(--pico-color-amber-50);color:var(--pico-color-dark)}.pico-background-amber{background-color:var(--pico-color-amber);color:var(--pico-color-dark)}.pico-background-pumpkin-950{background-color:var(--pico-color-pumpkin-950);color:var(--pico-color-light)}.pico-background-pumpkin-900{background-color:var(--pico-color-pumpkin-900);color:var(--pico-color-light)}.pico-background-pumpkin-850{background-color:var(--pico-color-pumpkin-850);color:var(--pico-color-light)}.pico-background-pumpkin-800{background-color:var(--pico-color-pumpkin-800);color:var(--pico-color-light)}.pico-background-pumpkin-750{background-color:var(--pico-color-pumpkin-750);color:var(--pico-color-light)}.pico-background-pumpkin-700{background-color:var(--pico-color-pumpkin-700);color:var(--pico-color-light)}.pico-background-pumpkin-650{background-color:var(--pico-color-pumpkin-650);color:var(--pico-color-light)}.pico-background-pumpkin-600{background-color:var(--pico-color-pumpkin-600);color:var(--pico-color-light)}.pico-background-pumpkin-550{background-color:var(--pico-color-pumpkin-550);color:var(--pico-color-light)}.pico-background-pumpkin-500{background-color:var(--pico-color-pumpkin-500);color:var(--pico-color-light)}.pico-background-pumpkin-450{background-color:var(--pico-color-pumpkin-450);color:var(--pico-color-light)}.pico-background-pumpkin-400{background-color:var(--pico-color-pumpkin-400);color:var(--pico-color-dark)}.pico-background-pumpkin-350{background-color:var(--pico-color-pumpkin-350);color:var(--pico-color-dark)}.pico-background-pumpkin-300{background-color:var(--pico-color-pumpkin-300);color:var(--pico-color-dark)}.pico-background-pumpkin-250{background-color:var(--pico-color-pumpkin-250);color:var(--pico-color-dark)}.pico-background-pumpkin-200{background-color:var(--pico-color-pumpkin-200);color:var(--pico-color-dark)}.pico-background-pumpkin-150{background-color:var(--pico-color-pumpkin-150);color:var(--pico-color-dark)}.pico-background-pumpkin-100{background-color:var(--pico-color-pumpkin-100);color:var(--pico-color-dark)}.pico-background-pumpkin-50{background-color:var(--pico-color-pumpkin-50);color:var(--pico-color-dark)}.pico-background-pumpkin{background-color:var(--pico-color-pumpkin);color:var(--pico-color-dark)}.pico-background-orange-950{background-color:var(--pico-color-orange-950);color:var(--pico-color-light)}.pico-background-orange-900{background-color:var(--pico-color-orange-900);color:var(--pico-color-light)}.pico-background-orange-850{background-color:var(--pico-color-orange-850);color:var(--pico-color-light)}.pico-background-orange-800{background-color:var(--pico-color-orange-800);color:var(--pico-color-light)}.pico-background-orange-750{background-color:var(--pico-color-orange-750);color:var(--pico-color-light)}.pico-background-orange-700{background-color:var(--pico-color-orange-700);color:var(--pico-color-light)}.pico-background-orange-650{background-color:var(--pico-color-orange-650);color:var(--pico-color-light)}.pico-background-orange-600{background-color:var(--pico-color-orange-600);color:var(--pico-color-light)}.pico-background-orange-550{background-color:var(--pico-color-orange-550);color:var(--pico-color-light)}.pico-background-orange-500{background-color:var(--pico-color-orange-500);color:var(--pico-color-light)}.pico-background-orange-450{background-color:var(--pico-color-orange-450);color:var(--pico-color-light)}.pico-background-orange-400{background-color:var(--pico-color-orange-400);color:var(--pico-color-dark)}.pico-background-orange-350{background-color:var(--pico-color-orange-350);color:var(--pico-color-dark)}.pico-background-orange-300{background-color:var(--pico-color-orange-300);color:var(--pico-color-dark)}.pico-background-orange-250{background-color:var(--pico-color-orange-250);color:var(--pico-color-dark)}.pico-background-orange-200{background-color:var(--pico-color-orange-200);color:var(--pico-color-dark)}.pico-background-orange-150{background-color:var(--pico-color-orange-150);color:var(--pico-color-dark)}.pico-background-orange-100{background-color:var(--pico-color-orange-100);color:var(--pico-color-dark)}.pico-background-orange-50{background-color:var(--pico-color-orange-50);color:var(--pico-color-dark)}.pico-background-orange{background-color:var(--pico-color-orange);color:var(--pico-color-light)}.pico-background-sand-950{background-color:var(--pico-color-sand-950);color:var(--pico-color-light)}.pico-background-sand-900{background-color:var(--pico-color-sand-900);color:var(--pico-color-light)}.pico-background-sand-850{background-color:var(--pico-color-sand-850);color:var(--pico-color-light)}.pico-background-sand-800{background-color:var(--pico-color-sand-800);color:var(--pico-color-light)}.pico-background-sand-750{background-color:var(--pico-color-sand-750);color:var(--pico-color-light)}.pico-background-sand-700{background-color:var(--pico-color-sand-700);color:var(--pico-color-light)}.pico-background-sand-650{background-color:var(--pico-color-sand-650);color:var(--pico-color-light)}.pico-background-sand-600{background-color:var(--pico-color-sand-600);color:var(--pico-color-light)}.pico-background-sand-550{background-color:var(--pico-color-sand-550);color:var(--pico-color-light)}.pico-background-sand-500{background-color:var(--pico-color-sand-500);color:var(--pico-color-light)}.pico-background-sand-450{background-color:var(--pico-color-sand-450);color:var(--pico-color-dark)}.pico-background-sand-400{background-color:var(--pico-color-sand-400);color:var(--pico-color-dark)}.pico-background-sand-350{background-color:var(--pico-color-sand-350);color:var(--pico-color-dark)}.pico-background-sand-300{background-color:var(--pico-color-sand-300);color:var(--pico-color-dark)}.pico-background-sand-250{background-color:var(--pico-color-sand-250);color:var(--pico-color-dark)}.pico-background-sand-200{background-color:var(--pico-color-sand-200);color:var(--pico-color-dark)}.pico-background-sand-150{background-color:var(--pico-color-sand-150);color:var(--pico-color-dark)}.pico-background-sand-100{background-color:var(--pico-color-sand-100);color:var(--pico-color-dark)}.pico-background-sand-50{background-color:var(--pico-color-sand-50);color:var(--pico-color-dark)}.pico-background-sand{background-color:var(--pico-color-sand);color:var(--pico-color-dark)}.pico-background-grey-950{background-color:var(--pico-color-grey-950);color:var(--pico-color-light)}.pico-background-grey-900{background-color:var(--pico-color-grey-900);color:var(--pico-color-light)}.pico-background-grey-850{background-color:var(--pico-color-grey-850);color:var(--pico-color-light)}.pico-background-grey-800{background-color:var(--pico-color-grey-800);color:var(--pico-color-light)}.pico-background-grey-750{background-color:var(--pico-color-grey-750);color:var(--pico-color-light)}.pico-background-grey-700{background-color:var(--pico-color-grey-700);color:var(--pico-color-light)}.pico-background-grey-650{background-color:var(--pico-color-grey-650);color:var(--pico-color-light)}.pico-background-grey-600{background-color:var(--pico-color-grey-600);color:var(--pico-color-light)}.pico-background-grey-550{background-color:var(--pico-color-grey-550);color:var(--pico-color-light)}.pico-background-grey-500{background-color:var(--pico-color-grey-500);color:var(--pico-color-light)}.pico-background-grey-450{background-color:var(--pico-color-grey-450);color:var(--pico-color-dark)}.pico-background-grey-400{background-color:var(--pico-color-grey-400);color:var(--pico-color-dark)}.pico-background-grey-350{background-color:var(--pico-color-grey-350);color:var(--pico-color-dark)}.pico-background-grey-300{background-color:var(--pico-color-grey-300);color:var(--pico-color-dark)}.pico-background-grey-250{background-color:var(--pico-color-grey-250);color:var(--pico-color-dark)}.pico-background-grey-200{background-color:var(--pico-color-grey-200);color:var(--pico-color-dark)}.pico-background-grey-150{background-color:var(--pico-color-grey-150);color:var(--pico-color-dark)}.pico-background-grey-100{background-color:var(--pico-color-grey-100);color:var(--pico-color-dark)}.pico-background-grey-50{background-color:var(--pico-color-grey-50);color:var(--pico-color-dark)}.pico-background-grey{background-color:var(--pico-color-grey);color:var(--pico-color-dark)}.pico-background-zinc-950{background-color:var(--pico-color-zinc-950);color:var(--pico-color-light)}.pico-background-zinc-900{background-color:var(--pico-color-zinc-900);color:var(--pico-color-light)}.pico-background-zinc-850{background-color:var(--pico-color-zinc-850);color:var(--pico-color-light)}.pico-background-zinc-800{background-color:var(--pico-color-zinc-800);color:var(--pico-color-light)}.pico-background-zinc-750{background-color:var(--pico-color-zinc-750);color:var(--pico-color-light)}.pico-background-zinc-700{background-color:var(--pico-color-zinc-700);color:var(--pico-color-light)}.pico-background-zinc-650{background-color:var(--pico-color-zinc-650);color:var(--pico-color-light)}.pico-background-zinc-600{background-color:var(--pico-color-zinc-600);color:var(--pico-color-light)}.pico-background-zinc-550{background-color:var(--pico-color-zinc-550);color:var(--pico-color-light)}.pico-background-zinc-500{background-color:var(--pico-color-zinc-500);color:var(--pico-color-light)}.pico-background-zinc-450{background-color:var(--pico-color-zinc-450);color:var(--pico-color-dark)}.pico-background-zinc-400{background-color:var(--pico-color-zinc-400);color:var(--pico-color-dark)}.pico-background-zinc-350{background-color:var(--pico-color-zinc-350);color:var(--pico-color-dark)}.pico-background-zinc-300{background-color:var(--pico-color-zinc-300);color:var(--pico-color-dark)}.pico-background-zinc-250{background-color:var(--pico-color-zinc-250);color:var(--pico-color-dark)}.pico-background-zinc-200{background-color:var(--pico-color-zinc-200);color:var(--pico-color-dark)}.pico-background-zinc-150{background-color:var(--pico-color-zinc-150);color:var(--pico-color-dark)}.pico-background-zinc-100{background-color:var(--pico-color-zinc-100);color:var(--pico-color-dark)}.pico-background-zinc-50{background-color:var(--pico-color-zinc-50);color:var(--pico-color-dark)}.pico-background-zinc{background-color:var(--pico-color-zinc);color:var(--pico-color-light)}.pico-background-slate-950{background-color:var(--pico-color-slate-950);color:var(--pico-color-light)}.pico-background-slate-900{background-color:var(--pico-color-slate-900);color:var(--pico-color-light)}.pico-background-slate-850{background-color:var(--pico-color-slate-850);color:var(--pico-color-light)}.pico-background-slate-800{background-color:var(--pico-color-slate-800);color:var(--pico-color-light)}.pico-background-slate-750{background-color:var(--pico-color-slate-750);color:var(--pico-color-light)}.pico-background-slate-700{background-color:var(--pico-color-slate-700);color:var(--pico-color-light)}.pico-background-slate-650{background-color:var(--pico-color-slate-650);color:var(--pico-color-light)}.pico-background-slate-600{background-color:var(--pico-color-slate-600);color:var(--pico-color-light)}.pico-background-slate-550{background-color:var(--pico-color-slate-550);color:var(--pico-color-light)}.pico-background-slate-500{background-color:var(--pico-color-slate-500);color:var(--pico-color-light)}.pico-background-slate-450{background-color:var(--pico-color-slate-450);color:var(--pico-color-dark)}.pico-background-slate-400{background-color:var(--pico-color-slate-400);color:var(--pico-color-dark)}.pico-background-slate-350{background-color:var(--pico-color-slate-350);color:var(--pico-color-dark)}.pico-background-slate-300{background-color:var(--pico-color-slate-300);color:var(--pico-color-dark)}.pico-background-slate-250{background-color:var(--pico-color-slate-250);color:var(--pico-color-dark)}.pico-background-slate-200{background-color:var(--pico-color-slate-200);color:var(--pico-color-dark)}.pico-background-slate-150{background-color:var(--pico-color-slate-150);color:var(--pico-color-dark)}.pico-background-slate-100{background-color:var(--pico-color-slate-100);color:var(--pico-color-dark)}.pico-background-slate-50{background-color:var(--pico-color-slate-50);color:var(--pico-color-dark)}.pico-background-slate{background-color:var(--pico-color-slate);color:var(--pico-color-light)}
+4
static/pico.css
···
··· 1 + @charset "UTF-8";/*! 2 + * Pico CSS ✨ v2.1.1 (https://picocss.com) 3 + * Copyright 2019-2025 - Licensed under MIT 4 + */:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:host,:root{--pico-font-size:106.25%}}@media (min-width:768px){:host,:root{--pico-font-size:112.5%}}@media (min-width:1024px){:host,:root{--pico-font-size:118.75%}}@media (min-width:1280px){:host,:root{--pico-font-size:125%}}@media (min-width:1536px){:host,:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}[role=search]{--pico-border-radius:5rem}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button])::before{filter:brightness(0) invert(1)}:host(:not([data-theme=dark])),:root:not([data-theme=dark]),[data-theme=light]{color-scheme:light;--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(148, 134, 225, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:rgb(231, 234, 239.5);--pico-primary:#655cd6;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(101, 92, 214, 0.5);--pico-primary-hover:#4040bf;--pico-primary-hover-background:#4040bf;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(148, 134, 225, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:rgb(252.5, 230.5, 191.5);--pico-mark-color:#0f1114;--pico-ins-color:rgb(28.5, 105.5, 84);--pico-del-color:rgb(136, 56.5, 53);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(243, 244.5, 246.75);--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(251, 251.5, 252.25);--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(183.5, 105.5, 106.5);--pico-form-element-invalid-active-border-color:rgb(200.25, 79.25, 72.25);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:rgb(76, 154.5, 137.5);--pico-form-element-valid-active-border-color:rgb(39, 152.75, 118.75);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(251, 251.5, 252.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme=dark])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:host(:not([data-theme])),:root:not([data-theme]){color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(162, 148, 229, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#a294e5;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(162, 148, 229, 0.5);--pico-primary-hover:#bdb2ec;--pico-primary-hover-background:#655cd6;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(162, 148, 229, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}:host(:not([data-theme])) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}}[data-theme=dark]{color-scheme:dark;--pico-background-color:rgb(19, 22.5, 30.5);--pico-color:#c2c7d0;--pico-text-selection-color:rgba(162, 148, 229, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#a294e5;--pico-primary-background:#524ed2;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(162, 148, 229, 0.5);--pico-primary-hover:#bdb2ec;--pico-primary-hover-background:#655cd6;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(162, 148, 229, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:rgb(205.5, 126, 123);--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:rgb(26, 30.5, 40.25);--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:rgb(28, 33, 43.5);--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:rgb(26, 30.5, 40.25);--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:rgb(149.5, 74, 80);--pico-form-element-invalid-active-border-color:rgb(183.25, 63.5, 59);--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:rgb(22, 137, 105.5);--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:rgb(26, 30.5, 40.25);--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(7.5, 8.5, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E")}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:host),:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal)}@media (min-width:576px){body>footer,body>header,body>main{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){body>footer,body>header,body>main{max-width:700px}}@media (min-width:1024px){body>footer,body>header,body>main{max-width:950px}}@media (min-width:1280px){body>footer,body>header,body>main{max-width:1200px}}@media (min-width:1536px){body>footer,body>header,body>main{max-width:1450px}}section{margin-bottom:var(--pico-block-spacing-vertical)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}[type=file]::file-selector-button:focus,[type=reset]:focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:host),svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code,pre samp{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre,samp{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd,samp{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code,pre>samp{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html,form){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html,form)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html,form):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html,form):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:host,:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog>article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog>article{max-width:510px}}@media (min-width:768px){dialog>article{max-width:700px}}dialog>article>header>*{margin-bottom:0}dialog>article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog>article>footer{text-align:right}dialog>article>footer [role=button],dialog>article>footer button{margin-bottom:0}dialog>article>footer [role=button]:not(:first-of-type),dialog>article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog>article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog>article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input,[role=button]){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}}
+32
templates/base.html
···
··· 1 + <!DOCTYPE html> 2 + <html lang="en" data-theme="auto"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <title>{% block title %}{{ title | default("Blahg") }}{% endblock %}</title> 7 + <link rel="stylesheet" href="/static/pico.css"> 8 + <link rel="stylesheet" href="/static/pico.colors.css"> 9 + {% block head %}{% endblock %} 10 + </head> 11 + <body> 12 + <header> 13 + <hgroup> 14 + <h1>Smoke Signal Blog</h1> 15 + </hgroup> 16 + <nav> 17 + <ul> 18 + <li><a href="/">Blog</a></li> 19 + <li><a href="https://discourse.smokesignal.events/">Discourse</a></li> 20 + <li><a href="https://smokesignal.events/">Site</a></li> 21 + </ul> 22 + </nav> 23 + </header> 24 + {% block content %}{% endblock %} 25 + <footer> 26 + <p> 27 + Powered by <a href="https://tangled.sh/@smokesignal.events/blahg">blahg</a> - 28 + An ATProtocol powered blog 29 + </p> 30 + </footer> 31 + </body> 32 + </html>
+33
templates/index.html
···
··· 1 + {% extends "base.html" %} 2 + {% block title %}Smoke Signal Blog{% endblock %} 3 + {% block head %} 4 + <meta name="description" content="The Smoke Signal Events blog."> 5 + <link rel="canonical" href="{{ external_base }}/"> 6 + <meta property="og:title" content="Smoke Signal Events Blog" /> 7 + <meta property="og:type" content="website" /> 8 + <meta property="og:url" content="{{ external_base }}/" /> 9 + <meta property="og:description" content="The Smoke Signal Events blog." /> 10 + <meta property="og:site_name" content="Smoke Signal Events Blog" /> 11 + {% endblock %} 12 + {% block content %} 13 + <main> 14 + <section> 15 + <p>This is the blog for <a href="https://smokesignal.events/">smokesignal.events</a>, an event and RSVP management and discovery application built on top of ATProtocol.</p> 16 + </section> 17 + </main> 18 + <main> 19 + <section> 20 + {% if posts %} 21 + <ul> 22 + {% for post in posts %} 23 + <li> 24 + <a href="/posts/{{ post.slug }}">{{ post.title }}</a> published {{ post.created_at }} 25 + </li> 26 + {% endfor %} 27 + </ul> 28 + {% else %} 29 + <p><strong><em>There are no posts to display.</em></strong></p> 30 + {% endif %} 31 + </section> 32 + </main> 33 + {% endblock %}
+41
templates/post.html
···
··· 1 + {% extends "base.html" %} 2 + {% block title %}{{ post.title }} - Blahg{% endblock %} 3 + {% block head %} 4 + <meta name="description" content="{{ post.title }} posted by @smokesignal.events on {{ post.created_at }}"> 5 + <link rel="canonical" href="{{ external_base }}/"> 6 + <meta property="og:title" content="{{ post.title }}" /> 7 + <meta property="og:type" content="website" /> 8 + <meta property="og:url" content="{{ external_base }}/posts/{{ post.slug }}" /> 9 + <meta property="og:description" content="{{ post.title }} posted by @smokesignal.events on {{ post.created_at }}" /> 10 + <meta property="og:site_name" content="Smoke Signal Events Blog" /> 11 + {% endblock %} 12 + {% block content %} 13 + <main> 14 + <section> 15 + <hgroup> 16 + <h1>{{ post.title }}</h1> 17 + <p>Published by <a href="#">@smokesignal.events</a> on {{ post.created_at }}.</p> 18 + </hgroup> 19 + {% autoescape false %}{{ post_content }}{% endautoescape %} 20 + </section> 21 + </main> 22 + <main> 23 + <section> 24 + {% if total_activity %} 25 + <p>This post has had {{ total_activity }} interaction{% if total_activity > 1 %}s{% endif %}.</p> 26 + <ul> 27 + {% for collection in collection_counts %} 28 + {% set collection_count = (collection_counts[collection] | default(0)) %} 29 + {% if collection_count > 0 %} 30 + <li> 31 + <a href="/posts/{{ post.slug }}/{{ collection }}" rel="nofollow">{{ collection }} ({{ collection_count }})</a> 32 + </li> 33 + {% endif %} 34 + {% endfor %} 35 + </ul> 36 + {% else %} 37 + <p>No one has interacted with this post.</p> 38 + {% endif %} 39 + </section> 40 + </main> 41 + {% endblock %}
+31
templates/post_references.html
···
··· 1 + {% extends "base.html" %} 2 + {% block title %}{{ post.title }} - Blahg{% endblock %} 3 + {% block head %} 4 + <meta name="robots" content="noindex"> 5 + {% endblock %} 6 + {% block content %} 7 + <main> 8 + <section> 9 + <hgroup> 10 + <h1> 11 + <a href="/posts/{{ post.slug }}">Back to Post</a> 12 + </h1> 13 + </section> 14 + </main> 15 + <main> 16 + <section> 17 + {% if post_references %} 18 + <ul> 19 + {% for post_reference in post_references %} 20 + <li> 21 + <a href="web+{{ post_reference.aturi }}">{{ post_reference.aturi }}</a> 22 + </li> 23 + {% endfor %} 24 + </ul> 25 + {% else %} 26 + <p><strong><em>There are no references in this collection to display.</em></strong></p> 27 + {% endif %} 28 + 29 + </section> 30 + </main> 31 + {% endblock %}
+70
tools.smokesignal.blahg.content.post.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "tools.smokesignal.blahg.content.post", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A blagh post", 8 + "key": "any", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "title": { 13 + "type": "string", 14 + "maxGraphemes": 200, 15 + "maxLength": 2000 16 + }, 17 + "content": { 18 + "type": "blob", 19 + "description": "The content of the post", 20 + "accept": [ 21 + "text/plain", 22 + "text/html", 23 + "text/markdown" 24 + ], 25 + "maxSize": 1000000 26 + }, 27 + "langs": { 28 + "type": "array", 29 + "description": "Indicates human language of text content.", 30 + "maxLength": 3, 31 + "items": { 32 + "type": "string", 33 + "format": "language" 34 + } 35 + }, 36 + "attachments": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "#attachment" 41 + } 42 + }, 43 + "publishedAt": { 44 + "type": "string", 45 + "format": "datetime" 46 + } 47 + } 48 + } 49 + }, 50 + "attachment": { 51 + "type": "object", 52 + "required": [ 53 + "content" 54 + ], 55 + "properties": { 56 + "content": { 57 + "type": "blob", 58 + "accept": [ 59 + "image/*" 60 + ], 61 + "maxSize": 3000000 62 + }, 63 + "alt": { 64 + "type": "string", 65 + "description": "Alt text description of the content, for accessibility." 66 + } 67 + } 68 + } 69 + } 70 + }