Server tools to backfill, tail, mirror, and verify PLC logs

fjall mirror #2

open opened by ptr.pet targeting main from ptr.pet/Allegedly: main

implements a mirror using fjall, todo signature verification

Labels

None yet.

Participants 1
AT URI
at://did:plc:dfl62fgb7wtjj3fcbb72naae/sh.tangled.repo.pull/3mfph5se4rq22
+4610 -1171
Diff #0
+2
.gitignore
··· 1 1 /target 2 2 weekly/ 3 + /.envrc 4 + /.direnv
+1151 -568
Cargo.lock
··· 3 3 version = 4 4 4 5 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 6 name = "adler2" 16 7 version = "2.0.1" 17 8 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 19 10 20 11 [[package]] 21 12 name = "aho-corasick" 22 - version = "1.1.3" 13 + version = "1.1.4" 23 14 source = "registry+https://github.com/rust-lang/crates.io-index" 24 - checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 15 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 25 16 dependencies = [ 26 17 "memchr", 27 18 ] ··· 32 23 dependencies = [ 33 24 "anyhow", 34 25 "async-compression", 26 + "async-trait", 27 + "bincode", 35 28 "chrono", 29 + "cid", 36 30 "clap", 31 + "criterion", 32 + "data-encoding", 33 + "fjall", 37 34 "futures", 38 35 "governor", 39 36 "http-body-util", 40 37 "log", 38 + "multibase", 41 39 "native-tls", 42 40 "opentelemetry", 43 41 "opentelemetry-otlp", ··· 47 45 "reqwest", 48 46 "reqwest-middleware", 49 47 "reqwest-retry", 48 + "rmp-serde", 50 49 "rustls", 51 50 "serde", 51 + "serde_bytes", 52 52 "serde_json", 53 - "thiserror 2.0.16", 53 + "tempfile", 54 + "thiserror 2.0.18", 54 55 "tokio", 55 56 "tokio-postgres", 56 57 "tokio-stream", ··· 76 77 ] 77 78 78 79 [[package]] 80 + name = "alloca" 81 + version = "0.4.0" 82 + source = "registry+https://github.com/rust-lang/crates.io-index" 83 + checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" 84 + dependencies = [ 85 + "cc", 86 + ] 87 + 88 + [[package]] 79 89 name = "allocator-api2" 80 90 version = "0.2.21" 81 91 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 91 101 ] 92 102 93 103 [[package]] 104 + name = "anes" 105 + version = "0.1.6" 106 + source = "registry+https://github.com/rust-lang/crates.io-index" 107 + checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 108 + 109 + [[package]] 94 110 name = "anstream" 95 - version = "0.6.20" 111 + version = "0.6.21" 96 112 source = "registry+https://github.com/rust-lang/crates.io-index" 97 - checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" 113 + checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" 98 114 dependencies = [ 99 115 "anstyle", 100 116 "anstyle-parse", ··· 107 123 108 124 [[package]] 109 125 name = "anstyle" 110 - version = "1.0.11" 126 + version = "1.0.13" 111 127 source = "registry+https://github.com/rust-lang/crates.io-index" 112 - checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 128 + checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 113 129 114 130 [[package]] 115 131 name = "anstyle-parse" ··· 122 138 123 139 [[package]] 124 140 name = "anstyle-query" 125 - version = "1.1.4" 141 + version = "1.1.5" 126 142 source = "registry+https://github.com/rust-lang/crates.io-index" 127 - checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" 143 + checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 128 144 dependencies = [ 129 - "windows-sys 0.60.2", 145 + "windows-sys 0.61.2", 130 146 ] 131 147 132 148 [[package]] 133 149 name = "anstyle-wincon" 134 - version = "3.0.10" 150 + version = "3.0.11" 135 151 source = "registry+https://github.com/rust-lang/crates.io-index" 136 - checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" 152 + checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" 137 153 dependencies = [ 138 154 "anstyle", 139 155 "once_cell_polyfill", 140 - "windows-sys 0.60.2", 156 + "windows-sys 0.61.2", 141 157 ] 142 158 143 159 [[package]] 144 160 name = "anyhow" 145 - version = "1.0.99" 161 + version = "1.0.102" 146 162 source = "registry+https://github.com/rust-lang/crates.io-index" 147 - checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 163 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 148 164 149 165 [[package]] 150 166 name = "asn1-rs" ··· 158 174 "nom", 159 175 "num-traits", 160 176 "rusticata-macros", 161 - "thiserror 2.0.16", 177 + "thiserror 2.0.18", 162 178 "time", 163 179 ] 164 180 ··· 187 203 188 204 [[package]] 189 205 name = "async-compression" 190 - version = "0.4.30" 206 + version = "0.4.40" 191 207 source = "registry+https://github.com/rust-lang/crates.io-index" 192 - checksum = "977eb15ea9efd848bb8a4a1a2500347ed7f0bf794edf0dc3ddcf439f43d36b23" 208 + checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" 193 209 dependencies = [ 194 210 "compression-codecs", 195 211 "compression-core", 196 - "futures-core", 197 212 "futures-io", 198 213 "pin-project-lite", 199 214 "tokio", ··· 224 239 225 240 [[package]] 226 241 name = "aws-lc-rs" 227 - version = "1.14.0" 242 + version = "1.16.0" 228 243 source = "registry+https://github.com/rust-lang/crates.io-index" 229 - checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" 244 + checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" 230 245 dependencies = [ 231 246 "aws-lc-sys", 232 247 "zeroize", ··· 234 249 235 250 [[package]] 236 251 name = "aws-lc-sys" 237 - version = "0.31.0" 252 + version = "0.37.1" 238 253 source = "registry+https://github.com/rust-lang/crates.io-index" 239 - checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" 254 + checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" 240 255 dependencies = [ 241 - "bindgen", 242 256 "cc", 243 257 "cmake", 244 258 "dunce", ··· 246 260 ] 247 261 248 262 [[package]] 249 - name = "backtrace" 250 - version = "0.3.75" 263 + name = "base-x" 264 + version = "0.2.11" 251 265 source = "registry+https://github.com/rust-lang/crates.io-index" 252 - checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 266 + checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 267 + 268 + [[package]] 269 + name = "base256emoji" 270 + version = "1.0.2" 271 + source = "registry+https://github.com/rust-lang/crates.io-index" 272 + checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" 253 273 dependencies = [ 254 - "addr2line", 255 - "cfg-if", 256 - "libc", 257 - "miniz_oxide", 258 - "object", 259 - "rustc-demangle", 260 - "windows-targets 0.52.6", 274 + "const-str", 275 + "match-lookup", 261 276 ] 262 277 263 278 [[package]] ··· 267 282 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 268 283 269 284 [[package]] 270 - name = "bindgen" 271 - version = "0.72.1" 285 + name = "bincode" 286 + version = "1.3.3" 272 287 source = "registry+https://github.com/rust-lang/crates.io-index" 273 - checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" 288 + checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 274 289 dependencies = [ 275 - "bitflags 2.9.4", 276 - "cexpr", 277 - "clang-sys", 278 - "itertools", 279 - "log", 280 - "prettyplease", 281 - "proc-macro2", 282 - "quote", 283 - "regex", 284 - "rustc-hash", 285 - "shlex", 286 - "syn", 290 + "serde", 287 291 ] 288 292 289 293 [[package]] ··· 294 298 295 299 [[package]] 296 300 name = "bitflags" 297 - version = "2.9.4" 301 + version = "2.11.0" 298 302 source = "registry+https://github.com/rust-lang/crates.io-index" 299 - checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 303 + checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" 300 304 301 305 [[package]] 302 306 name = "block-buffer" ··· 330 334 331 335 [[package]] 332 336 name = "bumpalo" 333 - version = "3.19.0" 337 + version = "3.20.2" 334 338 source = "registry+https://github.com/rust-lang/crates.io-index" 335 - checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 339 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 336 340 337 341 [[package]] 338 342 name = "byteorder" ··· 341 345 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 342 346 343 347 [[package]] 348 + name = "byteorder-lite" 349 + version = "0.1.0" 350 + source = "registry+https://github.com/rust-lang/crates.io-index" 351 + checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 352 + 353 + [[package]] 344 354 name = "bytes" 345 - version = "1.10.1" 355 + version = "1.11.1" 346 356 source = "registry+https://github.com/rust-lang/crates.io-index" 347 - checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 357 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" 358 + 359 + [[package]] 360 + name = "byteview" 361 + version = "0.10.1" 362 + source = "registry+https://github.com/rust-lang/crates.io-index" 363 + checksum = "1c53ba0f290bfc610084c05582d9c5d421662128fc69f4bf236707af6fd321b9" 364 + 365 + [[package]] 366 + name = "cast" 367 + version = "0.3.0" 368 + source = "registry+https://github.com/rust-lang/crates.io-index" 369 + checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 348 370 349 371 [[package]] 350 372 name = "cc" 351 - version = "1.2.37" 373 + version = "1.2.56" 352 374 source = "registry+https://github.com/rust-lang/crates.io-index" 353 - checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" 375 + checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" 354 376 dependencies = [ 355 377 "find-msvc-tools", 356 378 "jobserver", ··· 359 381 ] 360 382 361 383 [[package]] 362 - name = "cexpr" 363 - version = "0.6.0" 364 - source = "registry+https://github.com/rust-lang/crates.io-index" 365 - checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 366 - dependencies = [ 367 - "nom", 368 - ] 369 - 370 - [[package]] 371 384 name = "cfg-if" 372 - version = "1.0.3" 385 + version = "1.0.4" 373 386 source = "registry+https://github.com/rust-lang/crates.io-index" 374 - checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 387 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 375 388 376 389 [[package]] 377 390 name = "cfg_aliases" ··· 381 394 382 395 [[package]] 383 396 name = "chrono" 384 - version = "0.4.42" 397 + version = "0.4.43" 385 398 source = "registry+https://github.com/rust-lang/crates.io-index" 386 - checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 399 + checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" 387 400 dependencies = [ 388 401 "iana-time-zone", 389 402 "js-sys", 390 403 "num-traits", 391 404 "serde", 392 405 "wasm-bindgen", 393 - "windows-link 0.2.0", 406 + "windows-link", 407 + ] 408 + 409 + [[package]] 410 + name = "ciborium" 411 + version = "0.2.2" 412 + source = "registry+https://github.com/rust-lang/crates.io-index" 413 + checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 414 + dependencies = [ 415 + "ciborium-io", 416 + "ciborium-ll", 417 + "serde", 418 + ] 419 + 420 + [[package]] 421 + name = "ciborium-io" 422 + version = "0.2.2" 423 + source = "registry+https://github.com/rust-lang/crates.io-index" 424 + checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 425 + 426 + [[package]] 427 + name = "ciborium-ll" 428 + version = "0.2.2" 429 + source = "registry+https://github.com/rust-lang/crates.io-index" 430 + checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 431 + dependencies = [ 432 + "ciborium-io", 433 + "half", 394 434 ] 395 435 396 436 [[package]] 397 - name = "clang-sys" 398 - version = "1.8.1" 437 + name = "cid" 438 + version = "0.11.1" 399 439 source = "registry+https://github.com/rust-lang/crates.io-index" 400 - checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 440 + checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" 401 441 dependencies = [ 402 - "glob", 403 - "libc", 404 - "libloading", 442 + "core2", 443 + "multibase", 444 + "multihash", 445 + "unsigned-varint", 405 446 ] 406 447 407 448 [[package]] 408 449 name = "clap" 409 - version = "4.5.47" 450 + version = "4.5.60" 410 451 source = "registry+https://github.com/rust-lang/crates.io-index" 411 - checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" 452 + checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" 412 453 dependencies = [ 413 454 "clap_builder", 414 455 "clap_derive", ··· 416 457 417 458 [[package]] 418 459 name = "clap_builder" 419 - version = "4.5.47" 460 + version = "4.5.60" 420 461 source = "registry+https://github.com/rust-lang/crates.io-index" 421 - checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" 462 + checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" 422 463 dependencies = [ 423 464 "anstream", 424 465 "anstyle", ··· 428 469 429 470 [[package]] 430 471 name = "clap_derive" 431 - version = "4.5.47" 472 + version = "4.5.55" 432 473 source = "registry+https://github.com/rust-lang/crates.io-index" 433 - checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" 474 + checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" 434 475 dependencies = [ 435 476 "heck", 436 477 "proc-macro2", ··· 440 481 441 482 [[package]] 442 483 name = "clap_lex" 443 - version = "0.7.5" 484 + version = "1.0.0" 444 485 source = "registry+https://github.com/rust-lang/crates.io-index" 445 - checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 486 + checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" 446 487 447 488 [[package]] 448 489 name = "cmake" 449 - version = "0.1.54" 490 + version = "0.1.57" 450 491 source = "registry+https://github.com/rust-lang/crates.io-index" 451 - checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 492 + checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" 452 493 dependencies = [ 453 494 "cc", 454 495 ] ··· 460 501 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 461 502 462 503 [[package]] 504 + name = "compare" 505 + version = "0.0.6" 506 + source = "registry+https://github.com/rust-lang/crates.io-index" 507 + checksum = "ea0095f6103c2a8b44acd6fd15960c801dafebf02e21940360833e0673f48ba7" 508 + 509 + [[package]] 463 510 name = "compression-codecs" 464 - version = "0.4.30" 511 + version = "0.4.37" 465 512 source = "registry+https://github.com/rust-lang/crates.io-index" 466 - checksum = "485abf41ac0c8047c07c87c72c8fb3eb5197f6e9d7ded615dfd1a00ae00a0f64" 513 + checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" 467 514 dependencies = [ 468 515 "brotli", 469 516 "compression-core", ··· 475 522 476 523 [[package]] 477 524 name = "compression-core" 478 - version = "0.4.29" 525 + version = "0.4.31" 479 526 source = "registry+https://github.com/rust-lang/crates.io-index" 480 - checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 527 + checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" 528 + 529 + [[package]] 530 + name = "const-str" 531 + version = "0.4.3" 532 + source = "registry+https://github.com/rust-lang/crates.io-index" 533 + checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" 481 534 482 535 [[package]] 483 536 name = "core-foundation" ··· 506 559 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 507 560 508 561 [[package]] 562 + name = "core2" 563 + version = "0.4.0" 564 + source = "registry+https://github.com/rust-lang/crates.io-index" 565 + checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" 566 + dependencies = [ 567 + "memchr", 568 + ] 569 + 570 + [[package]] 509 571 name = "cpufeatures" 510 572 version = "0.2.17" 511 573 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 524 586 ] 525 587 526 588 [[package]] 589 + name = "criterion" 590 + version = "0.8.2" 591 + source = "registry+https://github.com/rust-lang/crates.io-index" 592 + checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" 593 + dependencies = [ 594 + "alloca", 595 + "anes", 596 + "cast", 597 + "ciborium", 598 + "clap", 599 + "criterion-plot", 600 + "itertools 0.13.0", 601 + "num-traits", 602 + "oorandom", 603 + "page_size", 604 + "plotters", 605 + "rayon", 606 + "regex", 607 + "serde", 608 + "serde_json", 609 + "tinytemplate", 610 + "walkdir", 611 + ] 612 + 613 + [[package]] 614 + name = "criterion-plot" 615 + version = "0.8.2" 616 + source = "registry+https://github.com/rust-lang/crates.io-index" 617 + checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" 618 + dependencies = [ 619 + "cast", 620 + "itertools 0.13.0", 621 + ] 622 + 623 + [[package]] 624 + name = "crossbeam-deque" 625 + version = "0.8.6" 626 + source = "registry+https://github.com/rust-lang/crates.io-index" 627 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 628 + dependencies = [ 629 + "crossbeam-epoch", 630 + "crossbeam-utils", 631 + ] 632 + 633 + [[package]] 634 + name = "crossbeam-epoch" 635 + version = "0.9.18" 636 + source = "registry+https://github.com/rust-lang/crates.io-index" 637 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 638 + dependencies = [ 639 + "crossbeam-utils", 640 + ] 641 + 642 + [[package]] 643 + name = "crossbeam-skiplist" 644 + version = "0.1.3" 645 + source = "registry+https://github.com/rust-lang/crates.io-index" 646 + checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" 647 + dependencies = [ 648 + "crossbeam-epoch", 649 + "crossbeam-utils", 650 + ] 651 + 652 + [[package]] 527 653 name = "crossbeam-utils" 528 654 version = "0.8.21" 529 655 source = "registry+https://github.com/rust-lang/crates.io-index" 530 656 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 531 657 532 658 [[package]] 659 + name = "crunchy" 660 + version = "0.2.4" 661 + source = "registry+https://github.com/rust-lang/crates.io-index" 662 + checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" 663 + 664 + [[package]] 533 665 name = "crypto-common" 534 - version = "0.1.6" 666 + version = "0.1.7" 535 667 source = "registry+https://github.com/rust-lang/crates.io-index" 536 - checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 668 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 537 669 dependencies = [ 538 670 "generic-array", 539 671 "typenum", ··· 550 682 "hashbrown 0.14.5", 551 683 "lock_api", 552 684 "once_cell", 553 - "parking_lot_core 0.9.11", 685 + "parking_lot_core 0.9.12", 554 686 ] 555 687 556 688 [[package]] 557 689 name = "data-encoding" 558 - version = "2.9.0" 690 + version = "2.10.0" 559 691 source = "registry+https://github.com/rust-lang/crates.io-index" 560 - checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 692 + checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" 693 + 694 + [[package]] 695 + name = "data-encoding-macro" 696 + version = "0.1.19" 697 + source = "registry+https://github.com/rust-lang/crates.io-index" 698 + checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" 699 + dependencies = [ 700 + "data-encoding", 701 + "data-encoding-macro-internal", 702 + ] 703 + 704 + [[package]] 705 + name = "data-encoding-macro-internal" 706 + version = "0.1.17" 707 + source = "registry+https://github.com/rust-lang/crates.io-index" 708 + checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" 709 + dependencies = [ 710 + "data-encoding", 711 + "syn", 712 + ] 561 713 562 714 [[package]] 563 715 name = "der-parser" ··· 575 727 576 728 [[package]] 577 729 name = "deranged" 578 - version = "0.5.3" 730 + version = "0.5.6" 579 731 source = "registry+https://github.com/rust-lang/crates.io-index" 580 - checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" 732 + checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" 581 733 dependencies = [ 582 734 "powerfmt", 583 735 ] ··· 626 778 ] 627 779 628 780 [[package]] 781 + name = "enum_dispatch" 782 + version = "0.3.13" 783 + source = "registry+https://github.com/rust-lang/crates.io-index" 784 + checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 785 + dependencies = [ 786 + "once_cell", 787 + "proc-macro2", 788 + "quote", 789 + "syn", 790 + ] 791 + 792 + [[package]] 629 793 name = "equivalent" 630 794 version = "1.0.2" 631 795 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 638 802 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 639 803 dependencies = [ 640 804 "libc", 641 - "windows-sys 0.61.0", 805 + "windows-sys 0.61.2", 642 806 ] 643 807 644 808 [[package]] ··· 655 819 656 820 [[package]] 657 821 name = "find-msvc-tools" 658 - version = "0.1.1" 822 + version = "0.1.9" 659 823 source = "registry+https://github.com/rust-lang/crates.io-index" 660 - checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" 824 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" 825 + 826 + [[package]] 827 + name = "fjall" 828 + version = "3.0.2" 829 + source = "registry+https://github.com/rust-lang/crates.io-index" 830 + checksum = "5a2799b4198427a08c774838e44d0b77f677208f19a1927671cd2cd36bb30d69" 831 + dependencies = [ 832 + "byteorder-lite", 833 + "byteview", 834 + "dashmap", 835 + "flume", 836 + "log", 837 + "lsm-tree", 838 + "lz4_flex", 839 + "tempfile", 840 + "xxhash-rust", 841 + ] 661 842 662 843 [[package]] 663 844 name = "flate2" 664 - version = "1.1.2" 845 + version = "1.1.9" 665 846 source = "registry+https://github.com/rust-lang/crates.io-index" 666 - checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 847 + checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" 667 848 dependencies = [ 668 849 "crc32fast", 669 850 "miniz_oxide", 851 + ] 852 + 853 + [[package]] 854 + name = "flume" 855 + version = "0.12.0" 856 + source = "registry+https://github.com/rust-lang/crates.io-index" 857 + checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" 858 + dependencies = [ 859 + "spin", 670 860 ] 671 861 672 862 [[package]] ··· 682 872 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 683 873 684 874 [[package]] 875 + name = "foldhash" 876 + version = "0.2.0" 877 + source = "registry+https://github.com/rust-lang/crates.io-index" 878 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" 879 + 880 + [[package]] 685 881 name = "foreign-types" 686 882 version = "0.3.2" 687 883 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 713 909 714 910 [[package]] 715 911 name = "futures" 716 - version = "0.3.31" 912 + version = "0.3.32" 717 913 source = "registry+https://github.com/rust-lang/crates.io-index" 718 - checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 914 + checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" 719 915 dependencies = [ 720 916 "futures-channel", 721 917 "futures-core", ··· 728 924 729 925 [[package]] 730 926 name = "futures-channel" 731 - version = "0.3.31" 927 + version = "0.3.32" 732 928 source = "registry+https://github.com/rust-lang/crates.io-index" 733 - checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 929 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" 734 930 dependencies = [ 735 931 "futures-core", 736 932 "futures-sink", ··· 738 934 739 935 [[package]] 740 936 name = "futures-core" 741 - version = "0.3.31" 937 + version = "0.3.32" 742 938 source = "registry+https://github.com/rust-lang/crates.io-index" 743 - checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 939 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" 744 940 745 941 [[package]] 746 942 name = "futures-executor" 747 - version = "0.3.31" 943 + version = "0.3.32" 748 944 source = "registry+https://github.com/rust-lang/crates.io-index" 749 - checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 945 + checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" 750 946 dependencies = [ 751 947 "futures-core", 752 948 "futures-task", ··· 755 951 756 952 [[package]] 757 953 name = "futures-io" 758 - version = "0.3.31" 954 + version = "0.3.32" 759 955 source = "registry+https://github.com/rust-lang/crates.io-index" 760 - checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 956 + checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" 761 957 762 958 [[package]] 763 959 name = "futures-macro" 764 - version = "0.3.31" 960 + version = "0.3.32" 765 961 source = "registry+https://github.com/rust-lang/crates.io-index" 766 - checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 962 + checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" 767 963 dependencies = [ 768 964 "proc-macro2", 769 965 "quote", ··· 772 968 773 969 [[package]] 774 970 name = "futures-sink" 775 - version = "0.3.31" 971 + version = "0.3.32" 776 972 source = "registry+https://github.com/rust-lang/crates.io-index" 777 - checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 973 + checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" 778 974 779 975 [[package]] 780 976 name = "futures-task" 781 - version = "0.3.31" 977 + version = "0.3.32" 782 978 source = "registry+https://github.com/rust-lang/crates.io-index" 783 - checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 979 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" 784 980 785 981 [[package]] 786 982 name = "futures-timer" ··· 790 986 791 987 [[package]] 792 988 name = "futures-util" 793 - version = "0.3.31" 989 + version = "0.3.32" 794 990 source = "registry+https://github.com/rust-lang/crates.io-index" 795 - checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 991 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" 796 992 dependencies = [ 797 993 "futures-channel", 798 994 "futures-core", ··· 802 998 "futures-task", 803 999 "memchr", 804 1000 "pin-project-lite", 805 - "pin-utils", 806 1001 "slab", 807 1002 ] 808 1003 ··· 818 1013 819 1014 [[package]] 820 1015 name = "getrandom" 821 - version = "0.2.16" 1016 + version = "0.2.17" 822 1017 source = "registry+https://github.com/rust-lang/crates.io-index" 823 - checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1018 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 824 1019 dependencies = [ 825 1020 "cfg-if", 826 1021 "js-sys", ··· 831 1026 832 1027 [[package]] 833 1028 name = "getrandom" 834 - version = "0.3.3" 1029 + version = "0.3.4" 835 1030 source = "registry+https://github.com/rust-lang/crates.io-index" 836 - checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1031 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 837 1032 dependencies = [ 838 1033 "cfg-if", 839 1034 "js-sys", 840 1035 "libc", 841 1036 "r-efi", 842 - "wasi 0.14.5+wasi-0.2.4", 1037 + "wasip2", 843 1038 "wasm-bindgen", 844 1039 ] 845 1040 846 1041 [[package]] 847 - name = "gimli" 848 - version = "0.31.1" 1042 + name = "getrandom" 1043 + version = "0.4.1" 849 1044 source = "registry+https://github.com/rust-lang/crates.io-index" 850 - checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 851 - 852 - [[package]] 853 - name = "glob" 854 - version = "0.3.3" 855 - source = "registry+https://github.com/rust-lang/crates.io-index" 856 - checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 1045 + checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" 1046 + dependencies = [ 1047 + "cfg-if", 1048 + "libc", 1049 + "r-efi", 1050 + "wasip2", 1051 + "wasip3", 1052 + ] 857 1053 858 1054 [[package]] 859 1055 name = "governor" 860 - version = "0.10.1" 1056 + version = "0.10.4" 861 1057 source = "registry+https://github.com/rust-lang/crates.io-index" 862 - checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" 1058 + checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8" 863 1059 dependencies = [ 864 1060 "cfg-if", 865 1061 "dashmap", 866 1062 "futures-sink", 867 1063 "futures-timer", 868 1064 "futures-util", 869 - "getrandom 0.3.3", 870 - "hashbrown 0.15.5", 1065 + "getrandom 0.3.4", 1066 + "hashbrown 0.16.1", 871 1067 "nonzero_ext", 872 - "parking_lot 0.12.4", 1068 + "parking_lot 0.12.5", 873 1069 "portable-atomic", 874 1070 "quanta", 875 1071 "rand 0.9.2", ··· 880 1076 881 1077 [[package]] 882 1078 name = "h2" 883 - version = "0.4.12" 1079 + version = "0.4.13" 884 1080 source = "registry+https://github.com/rust-lang/crates.io-index" 885 - checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 1081 + checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" 886 1082 dependencies = [ 887 1083 "atomic-waker", 888 1084 "bytes", ··· 895 1091 "tokio", 896 1092 "tokio-util", 897 1093 "tracing", 1094 + ] 1095 + 1096 + [[package]] 1097 + name = "half" 1098 + version = "2.7.1" 1099 + source = "registry+https://github.com/rust-lang/crates.io-index" 1100 + checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" 1101 + dependencies = [ 1102 + "cfg-if", 1103 + "crunchy", 1104 + "zerocopy", 898 1105 ] 899 1106 900 1107 [[package]] ··· 909 1116 source = "registry+https://github.com/rust-lang/crates.io-index" 910 1117 checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" 911 1118 dependencies = [ 1119 + "foldhash 0.1.5", 1120 + ] 1121 + 1122 + [[package]] 1123 + name = "hashbrown" 1124 + version = "0.16.1" 1125 + source = "registry+https://github.com/rust-lang/crates.io-index" 1126 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 1127 + dependencies = [ 912 1128 "allocator-api2", 913 1129 "equivalent", 914 - "foldhash", 1130 + "foldhash 0.2.0", 915 1131 ] 916 1132 917 1133 [[package]] ··· 955 1171 956 1172 [[package]] 957 1173 name = "http" 958 - version = "1.3.1" 1174 + version = "1.4.0" 959 1175 source = "registry+https://github.com/rust-lang/crates.io-index" 960 - checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 1176 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" 961 1177 dependencies = [ 962 1178 "bytes", 963 - "fnv", 964 1179 "itoa", 965 1180 ] 966 1181 ··· 1001 1216 1002 1217 [[package]] 1003 1218 name = "hyper" 1004 - version = "1.7.0" 1219 + version = "1.8.1" 1005 1220 source = "registry+https://github.com/rust-lang/crates.io-index" 1006 - checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" 1221 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 1007 1222 dependencies = [ 1008 1223 "atomic-waker", 1009 1224 "bytes", ··· 1057 1272 1058 1273 [[package]] 1059 1274 name = "hyper-util" 1060 - version = "0.1.16" 1275 + version = "0.1.20" 1061 1276 source = "registry+https://github.com/rust-lang/crates.io-index" 1062 - checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" 1277 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 1063 1278 dependencies = [ 1064 1279 "base64", 1065 1280 "bytes", 1066 1281 "futures-channel", 1067 - "futures-core", 1068 1282 "futures-util", 1069 1283 "http", 1070 1284 "http-body", ··· 1073 1287 "libc", 1074 1288 "percent-encoding", 1075 1289 "pin-project-lite", 1076 - "socket2 0.6.0", 1290 + "socket2", 1077 1291 "system-configuration", 1078 1292 "tokio", 1079 1293 "tower-service", ··· 1083 1297 1084 1298 [[package]] 1085 1299 name = "iana-time-zone" 1086 - version = "0.1.63" 1300 + version = "0.1.65" 1087 1301 source = "registry+https://github.com/rust-lang/crates.io-index" 1088 - checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1302 + checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" 1089 1303 dependencies = [ 1090 1304 "android_system_properties", 1091 1305 "core-foundation-sys", ··· 1107 1321 1108 1322 [[package]] 1109 1323 name = "icu_collections" 1110 - version = "2.0.0" 1324 + version = "2.1.1" 1111 1325 source = "registry+https://github.com/rust-lang/crates.io-index" 1112 - checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 1326 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 1113 1327 dependencies = [ 1114 1328 "displaydoc", 1115 1329 "potential_utf", ··· 1120 1334 1121 1335 [[package]] 1122 1336 name = "icu_locale_core" 1123 - version = "2.0.0" 1337 + version = "2.1.1" 1124 1338 source = "registry+https://github.com/rust-lang/crates.io-index" 1125 - checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 1339 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 1126 1340 dependencies = [ 1127 1341 "displaydoc", 1128 1342 "litemap", ··· 1133 1347 1134 1348 [[package]] 1135 1349 name = "icu_normalizer" 1136 - version = "2.0.0" 1350 + version = "2.1.1" 1137 1351 source = "registry+https://github.com/rust-lang/crates.io-index" 1138 - checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 1352 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 1139 1353 dependencies = [ 1140 - "displaydoc", 1141 1354 "icu_collections", 1142 1355 "icu_normalizer_data", 1143 1356 "icu_properties", ··· 1148 1361 1149 1362 [[package]] 1150 1363 name = "icu_normalizer_data" 1151 - version = "2.0.0" 1364 + version = "2.1.1" 1152 1365 source = "registry+https://github.com/rust-lang/crates.io-index" 1153 - checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 1366 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 1154 1367 1155 1368 [[package]] 1156 1369 name = "icu_properties" 1157 - version = "2.0.1" 1370 + version = "2.1.2" 1158 1371 source = "registry+https://github.com/rust-lang/crates.io-index" 1159 - checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 1372 + checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" 1160 1373 dependencies = [ 1161 - "displaydoc", 1162 1374 "icu_collections", 1163 1375 "icu_locale_core", 1164 1376 "icu_properties_data", 1165 1377 "icu_provider", 1166 - "potential_utf", 1167 1378 "zerotrie", 1168 1379 "zerovec", 1169 1380 ] 1170 1381 1171 1382 [[package]] 1172 1383 name = "icu_properties_data" 1173 - version = "2.0.1" 1384 + version = "2.1.2" 1174 1385 source = "registry+https://github.com/rust-lang/crates.io-index" 1175 - checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 1386 + checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" 1176 1387 1177 1388 [[package]] 1178 1389 name = "icu_provider" 1179 - version = "2.0.0" 1390 + version = "2.1.1" 1180 1391 source = "registry+https://github.com/rust-lang/crates.io-index" 1181 - checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 1392 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 1182 1393 dependencies = [ 1183 1394 "displaydoc", 1184 1395 "icu_locale_core", 1185 - "stable_deref_trait", 1186 - "tinystr", 1187 1396 "writeable", 1188 1397 "yoke", 1189 1398 "zerofrom", ··· 1192 1401 ] 1193 1402 1194 1403 [[package]] 1404 + name = "id-arena" 1405 + version = "2.3.0" 1406 + source = "registry+https://github.com/rust-lang/crates.io-index" 1407 + checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" 1408 + 1409 + [[package]] 1195 1410 name = "idna" 1196 1411 version = "1.1.0" 1197 1412 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1214 1429 1215 1430 [[package]] 1216 1431 name = "indexmap" 1217 - version = "2.11.4" 1432 + version = "2.13.0" 1218 1433 source = "registry+https://github.com/rust-lang/crates.io-index" 1219 - checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 1434 + checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" 1220 1435 dependencies = [ 1221 1436 "equivalent", 1222 - "hashbrown 0.15.5", 1437 + "hashbrown 0.16.1", 1438 + "serde", 1439 + "serde_core", 1223 1440 ] 1224 1441 1225 1442 [[package]] ··· 1235 1452 ] 1236 1453 1237 1454 [[package]] 1238 - name = "io-uring" 1239 - version = "0.7.10" 1455 + name = "interval-heap" 1456 + version = "0.0.5" 1240 1457 source = "registry+https://github.com/rust-lang/crates.io-index" 1241 - checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 1458 + checksum = "11274e5e8e89b8607cfedc2910b6626e998779b48a019151c7604d0adcb86ac6" 1242 1459 dependencies = [ 1243 - "bitflags 2.9.4", 1244 - "cfg-if", 1245 - "libc", 1460 + "compare", 1246 1461 ] 1247 1462 1248 1463 [[package]] ··· 1253 1468 1254 1469 [[package]] 1255 1470 name = "iri-string" 1256 - version = "0.7.8" 1471 + version = "0.7.10" 1257 1472 source = "registry+https://github.com/rust-lang/crates.io-index" 1258 - checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" 1473 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 1259 1474 dependencies = [ 1260 1475 "memchr", 1261 1476 "serde", ··· 1263 1478 1264 1479 [[package]] 1265 1480 name = "is_terminal_polyfill" 1266 - version = "1.70.1" 1481 + version = "1.70.2" 1267 1482 source = "registry+https://github.com/rust-lang/crates.io-index" 1268 - checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1483 + checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 1269 1484 1270 1485 [[package]] 1271 1486 name = "itertools" ··· 1277 1492 ] 1278 1493 1279 1494 [[package]] 1495 + name = "itertools" 1496 + version = "0.14.0" 1497 + source = "registry+https://github.com/rust-lang/crates.io-index" 1498 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 1499 + dependencies = [ 1500 + "either", 1501 + ] 1502 + 1503 + [[package]] 1280 1504 name = "itoa" 1281 - version = "1.0.15" 1505 + version = "1.0.17" 1282 1506 source = "registry+https://github.com/rust-lang/crates.io-index" 1283 - checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1507 + checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 1284 1508 1285 1509 [[package]] 1286 1510 name = "jobserver" ··· 1288 1512 source = "registry+https://github.com/rust-lang/crates.io-index" 1289 1513 checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1290 1514 dependencies = [ 1291 - "getrandom 0.3.3", 1515 + "getrandom 0.3.4", 1292 1516 "libc", 1293 1517 ] 1294 1518 1295 1519 [[package]] 1296 1520 name = "js-sys" 1297 - version = "0.3.78" 1521 + version = "0.3.86" 1298 1522 source = "registry+https://github.com/rust-lang/crates.io-index" 1299 - checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" 1523 + checksum = "d36139f1c97c42c0c86a411910b04e48d4939a0376e6e0f989420cbdee0120e5" 1300 1524 dependencies = [ 1301 1525 "once_cell", 1302 1526 "wasm-bindgen", ··· 1309 1533 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1310 1534 1311 1535 [[package]] 1312 - name = "libc" 1313 - version = "0.2.175" 1536 + name = "leb128fmt" 1537 + version = "0.1.0" 1314 1538 source = "registry+https://github.com/rust-lang/crates.io-index" 1315 - checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1539 + checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 1316 1540 1317 1541 [[package]] 1318 - name = "libloading" 1319 - version = "0.8.9" 1542 + name = "libc" 1543 + version = "0.2.182" 1320 1544 source = "registry+https://github.com/rust-lang/crates.io-index" 1321 - checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" 1322 - dependencies = [ 1323 - "cfg-if", 1324 - "windows-link 0.2.0", 1325 - ] 1545 + checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" 1326 1546 1327 1547 [[package]] 1328 1548 name = "libredox" 1329 - version = "0.1.9" 1549 + version = "0.1.12" 1330 1550 source = "registry+https://github.com/rust-lang/crates.io-index" 1331 - checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 1551 + checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 1332 1552 dependencies = [ 1333 - "bitflags 2.9.4", 1553 + "bitflags 2.11.0", 1334 1554 "libc", 1335 - "redox_syscall 0.5.17", 1336 1555 ] 1337 1556 1338 1557 [[package]] ··· 1343 1562 1344 1563 [[package]] 1345 1564 name = "litemap" 1346 - version = "0.8.0" 1565 + version = "0.8.1" 1347 1566 source = "registry+https://github.com/rust-lang/crates.io-index" 1348 - checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1567 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 1349 1568 1350 1569 [[package]] 1351 1570 name = "lock_api" 1352 - version = "0.4.13" 1571 + version = "0.4.14" 1353 1572 source = "registry+https://github.com/rust-lang/crates.io-index" 1354 - checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1573 + checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 1355 1574 dependencies = [ 1356 - "autocfg", 1357 1575 "scopeguard", 1358 1576 ] 1359 1577 1360 1578 [[package]] 1361 1579 name = "log" 1362 - version = "0.4.28" 1580 + version = "0.4.29" 1363 1581 source = "registry+https://github.com/rust-lang/crates.io-index" 1364 - checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 1582 + checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" 1365 1583 1366 1584 [[package]] 1367 1585 name = "lru-slab" ··· 1370 1588 checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1371 1589 1372 1590 [[package]] 1591 + name = "lsm-tree" 1592 + version = "3.0.2" 1593 + source = "registry+https://github.com/rust-lang/crates.io-index" 1594 + checksum = "86e8d0b8e0cf2531a437788ce94d95570dbaabfe9888db20022c2d5ccec9b221" 1595 + dependencies = [ 1596 + "byteorder-lite", 1597 + "byteview", 1598 + "crossbeam-skiplist", 1599 + "enum_dispatch", 1600 + "interval-heap", 1601 + "log", 1602 + "lz4_flex", 1603 + "quick_cache", 1604 + "rustc-hash", 1605 + "self_cell", 1606 + "sfa", 1607 + "tempfile", 1608 + "varint-rs", 1609 + "xxhash-rust", 1610 + ] 1611 + 1612 + [[package]] 1613 + name = "lz4_flex" 1614 + version = "0.11.5" 1615 + source = "registry+https://github.com/rust-lang/crates.io-index" 1616 + checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" 1617 + dependencies = [ 1618 + "twox-hash", 1619 + ] 1620 + 1621 + [[package]] 1622 + name = "match-lookup" 1623 + version = "0.1.2" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" 1626 + dependencies = [ 1627 + "proc-macro2", 1628 + "quote", 1629 + "syn", 1630 + ] 1631 + 1632 + [[package]] 1373 1633 name = "matchers" 1374 1634 version = "0.2.0" 1375 1635 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1390 1650 1391 1651 [[package]] 1392 1652 name = "memchr" 1393 - version = "2.7.5" 1653 + version = "2.8.0" 1394 1654 source = "registry+https://github.com/rust-lang/crates.io-index" 1395 - checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1655 + checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 1396 1656 1397 1657 [[package]] 1398 1658 name = "mime" ··· 1413 1673 checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1414 1674 dependencies = [ 1415 1675 "adler2", 1676 + "simd-adler32", 1416 1677 ] 1417 1678 1418 1679 [[package]] 1419 1680 name = "mio" 1420 - version = "1.0.4" 1681 + version = "1.1.1" 1421 1682 source = "registry+https://github.com/rust-lang/crates.io-index" 1422 - checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1683 + checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 1423 1684 dependencies = [ 1424 1685 "libc", 1425 1686 "wasi 0.11.1+wasi-snapshot-preview1", 1426 - "windows-sys 0.59.0", 1687 + "windows-sys 0.61.2", 1688 + ] 1689 + 1690 + [[package]] 1691 + name = "multibase" 1692 + version = "0.9.2" 1693 + source = "registry+https://github.com/rust-lang/crates.io-index" 1694 + checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" 1695 + dependencies = [ 1696 + "base-x", 1697 + "base256emoji", 1698 + "data-encoding", 1699 + "data-encoding-macro", 1700 + ] 1701 + 1702 + [[package]] 1703 + name = "multihash" 1704 + version = "0.19.3" 1705 + source = "registry+https://github.com/rust-lang/crates.io-index" 1706 + checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" 1707 + dependencies = [ 1708 + "core2", 1709 + "unsigned-varint", 1427 1710 ] 1428 1711 1429 1712 [[package]] 1430 1713 name = "native-tls" 1431 - version = "0.2.14" 1714 + version = "0.2.18" 1432 1715 source = "registry+https://github.com/rust-lang/crates.io-index" 1433 - checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1716 + checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" 1434 1717 dependencies = [ 1435 1718 "libc", 1436 1719 "log", ··· 1438 1721 "openssl-probe", 1439 1722 "openssl-sys", 1440 1723 "schannel", 1441 - "security-framework 2.11.1", 1724 + "security-framework", 1442 1725 "security-framework-sys", 1443 1726 "tempfile", 1444 1727 ] ··· 1449 1732 source = "registry+https://github.com/rust-lang/crates.io-index" 1450 1733 checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 1451 1734 dependencies = [ 1452 - "bitflags 2.9.4", 1735 + "bitflags 2.11.0", 1453 1736 "cfg-if", 1454 1737 "cfg_aliases", 1455 1738 "libc", ··· 1473 1756 1474 1757 [[package]] 1475 1758 name = "nu-ansi-term" 1476 - version = "0.50.1" 1759 + version = "0.50.3" 1477 1760 source = "registry+https://github.com/rust-lang/crates.io-index" 1478 - checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 1761 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 1479 1762 dependencies = [ 1480 - "windows-sys 0.52.0", 1763 + "windows-sys 0.61.2", 1481 1764 ] 1482 1765 1483 1766 [[package]] ··· 1492 1775 1493 1776 [[package]] 1494 1777 name = "num-conv" 1495 - version = "0.1.0" 1778 + version = "0.2.0" 1496 1779 source = "registry+https://github.com/rust-lang/crates.io-index" 1497 - checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1780 + checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" 1498 1781 1499 1782 [[package]] 1500 1783 name = "num-integer" ··· 1515 1798 ] 1516 1799 1517 1800 [[package]] 1518 - name = "object" 1519 - version = "0.36.7" 1801 + name = "objc2-core-foundation" 1802 + version = "0.3.2" 1803 + source = "registry+https://github.com/rust-lang/crates.io-index" 1804 + checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" 1805 + dependencies = [ 1806 + "bitflags 2.11.0", 1807 + ] 1808 + 1809 + [[package]] 1810 + name = "objc2-system-configuration" 1811 + version = "0.3.2" 1520 1812 source = "registry+https://github.com/rust-lang/crates.io-index" 1521 - checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1813 + checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" 1522 1814 dependencies = [ 1523 - "memchr", 1815 + "objc2-core-foundation", 1524 1816 ] 1525 1817 1526 1818 [[package]] ··· 1540 1832 1541 1833 [[package]] 1542 1834 name = "once_cell_polyfill" 1543 - version = "1.70.1" 1835 + version = "1.70.2" 1544 1836 source = "registry+https://github.com/rust-lang/crates.io-index" 1545 - checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1837 + checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 1838 + 1839 + [[package]] 1840 + name = "oorandom" 1841 + version = "11.1.5" 1842 + source = "registry+https://github.com/rust-lang/crates.io-index" 1843 + checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" 1546 1844 1547 1845 [[package]] 1548 1846 name = "openssl" 1549 - version = "0.10.73" 1847 + version = "0.10.75" 1550 1848 source = "registry+https://github.com/rust-lang/crates.io-index" 1551 - checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1849 + checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 1552 1850 dependencies = [ 1553 - "bitflags 2.9.4", 1851 + "bitflags 2.11.0", 1554 1852 "cfg-if", 1555 1853 "foreign-types", 1556 1854 "libc", ··· 1572 1870 1573 1871 [[package]] 1574 1872 name = "openssl-probe" 1575 - version = "0.1.6" 1873 + version = "0.2.1" 1576 1874 source = "registry+https://github.com/rust-lang/crates.io-index" 1577 - checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1875 + checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" 1578 1876 1579 1877 [[package]] 1580 1878 name = "openssl-sys" 1581 - version = "0.9.109" 1879 + version = "0.9.111" 1582 1880 source = "registry+https://github.com/rust-lang/crates.io-index" 1583 - checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1881 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 1584 1882 dependencies = [ 1585 1883 "cc", 1586 1884 "libc", ··· 1598 1896 "futures-sink", 1599 1897 "js-sys", 1600 1898 "pin-project-lite", 1601 - "thiserror 2.0.16", 1899 + "thiserror 2.0.18", 1602 1900 "tracing", 1603 1901 ] 1604 1902 ··· 1628 1926 "opentelemetry_sdk", 1629 1927 "prost", 1630 1928 "reqwest", 1631 - "thiserror 2.0.16", 1929 + "thiserror 2.0.18", 1632 1930 "tracing", 1633 1931 ] 1634 1932 ··· 1657 1955 "percent-encoding", 1658 1956 "rand 0.9.2", 1659 1957 "serde_json", 1660 - "thiserror 2.0.16", 1958 + "thiserror 2.0.18", 1661 1959 "tokio", 1662 1960 "tokio-stream", 1663 1961 ] 1664 1962 1665 1963 [[package]] 1964 + name = "page_size" 1965 + version = "0.6.0" 1966 + source = "registry+https://github.com/rust-lang/crates.io-index" 1967 + checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" 1968 + dependencies = [ 1969 + "libc", 1970 + "winapi", 1971 + ] 1972 + 1973 + [[package]] 1666 1974 name = "parking_lot" 1667 1975 version = "0.11.2" 1668 1976 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1675 1983 1676 1984 [[package]] 1677 1985 name = "parking_lot" 1678 - version = "0.12.4" 1986 + version = "0.12.5" 1679 1987 source = "registry+https://github.com/rust-lang/crates.io-index" 1680 - checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1988 + checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 1681 1989 dependencies = [ 1682 1990 "lock_api", 1683 - "parking_lot_core 0.9.11", 1991 + "parking_lot_core 0.9.12", 1684 1992 ] 1685 1993 1686 1994 [[package]] ··· 1699 2007 1700 2008 [[package]] 1701 2009 name = "parking_lot_core" 1702 - version = "0.9.11" 2010 + version = "0.9.12" 1703 2011 source = "registry+https://github.com/rust-lang/crates.io-index" 1704 - checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 2012 + checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 1705 2013 dependencies = [ 1706 2014 "cfg-if", 1707 2015 "libc", 1708 - "redox_syscall 0.5.17", 2016 + "redox_syscall 0.5.18", 1709 2017 "smallvec", 1710 - "windows-targets 0.52.6", 2018 + "windows-link", 1711 2019 ] 1712 2020 1713 2021 [[package]] 1714 2022 name = "pem" 1715 - version = "3.0.5" 2023 + version = "3.0.6" 1716 2024 source = "registry+https://github.com/rust-lang/crates.io-index" 1717 - checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 2025 + checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" 1718 2026 dependencies = [ 1719 2027 "base64", 1720 - "serde", 2028 + "serde_core", 1721 2029 ] 1722 2030 1723 2031 [[package]] ··· 1728 2036 1729 2037 [[package]] 1730 2038 name = "phf" 1731 - version = "0.11.3" 2039 + version = "0.13.1" 1732 2040 source = "registry+https://github.com/rust-lang/crates.io-index" 1733 - checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 2041 + checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" 1734 2042 dependencies = [ 1735 2043 "phf_shared", 2044 + "serde", 1736 2045 ] 1737 2046 1738 2047 [[package]] 1739 2048 name = "phf_shared" 1740 - version = "0.11.3" 2049 + version = "0.13.1" 1741 2050 source = "registry+https://github.com/rust-lang/crates.io-index" 1742 - checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 2051 + checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" 1743 2052 dependencies = [ 1744 2053 "siphasher", 1745 2054 ] ··· 1783 2092 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1784 2093 1785 2094 [[package]] 2095 + name = "plotters" 2096 + version = "0.3.7" 2097 + source = "registry+https://github.com/rust-lang/crates.io-index" 2098 + checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 2099 + dependencies = [ 2100 + "num-traits", 2101 + "plotters-backend", 2102 + "plotters-svg", 2103 + "wasm-bindgen", 2104 + "web-sys", 2105 + ] 2106 + 2107 + [[package]] 2108 + name = "plotters-backend" 2109 + version = "0.3.7" 2110 + source = "registry+https://github.com/rust-lang/crates.io-index" 2111 + checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 2112 + 2113 + [[package]] 2114 + name = "plotters-svg" 2115 + version = "0.3.7" 2116 + source = "registry+https://github.com/rust-lang/crates.io-index" 2117 + checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 2118 + dependencies = [ 2119 + "plotters-backend", 2120 + ] 2121 + 2122 + [[package]] 1786 2123 name = "poem" 1787 2124 version = "3.1.12" 1788 2125 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1800 2137 "hyper-util", 1801 2138 "mime", 1802 2139 "nix", 1803 - "parking_lot 0.12.4", 2140 + "parking_lot 0.12.5", 1804 2141 "percent-encoding", 1805 2142 "pin-project-lite", 1806 2143 "poem-derive", ··· 1815 2152 "serde_urlencoded", 1816 2153 "smallvec", 1817 2154 "sync_wrapper", 1818 - "thiserror 2.0.16", 2155 + "thiserror 2.0.18", 1819 2156 "tokio", 1820 2157 "tokio-rustls", 1821 2158 "tokio-util", ··· 1838 2175 1839 2176 [[package]] 1840 2177 name = "portable-atomic" 1841 - version = "1.11.1" 2178 + version = "1.13.1" 1842 2179 source = "registry+https://github.com/rust-lang/crates.io-index" 1843 - checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 2180 + checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" 1844 2181 1845 2182 [[package]] 1846 2183 name = "postgres-native-tls" 1847 - version = "0.5.1" 2184 + version = "0.5.2" 1848 2185 source = "registry+https://github.com/rust-lang/crates.io-index" 1849 - checksum = "a1f39498473c92f7b6820ae970382c1d83178a3454c618161cb772e8598d9f6f" 2186 + checksum = "ac73153d92e4bde922bd6f1dfba7f1ab8132266c031153b55e20a1521cd36d49" 1850 2187 dependencies = [ 1851 2188 "native-tls", 1852 2189 "tokio", ··· 1856 2193 1857 2194 [[package]] 1858 2195 name = "postgres-protocol" 1859 - version = "0.6.8" 2196 + version = "0.6.10" 1860 2197 source = "registry+https://github.com/rust-lang/crates.io-index" 1861 - checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" 2198 + checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" 1862 2199 dependencies = [ 1863 2200 "base64", 1864 2201 "byteorder", ··· 1874 2211 1875 2212 [[package]] 1876 2213 name = "postgres-types" 1877 - version = "0.2.9" 2214 + version = "0.2.12" 1878 2215 source = "registry+https://github.com/rust-lang/crates.io-index" 1879 - checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" 2216 + checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" 1880 2217 dependencies = [ 1881 2218 "bytes", 1882 2219 "chrono", 1883 2220 "fallible-iterator", 1884 2221 "postgres-protocol", 1885 - "serde", 2222 + "serde_core", 1886 2223 "serde_json", 1887 2224 ] 1888 2225 1889 2226 [[package]] 1890 2227 name = "potential_utf" 1891 - version = "0.1.3" 2228 + version = "0.1.4" 1892 2229 source = "registry+https://github.com/rust-lang/crates.io-index" 1893 - checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" 2230 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 1894 2231 dependencies = [ 1895 2232 "zerovec", 1896 2233 ] ··· 1931 2268 1932 2269 [[package]] 1933 2270 name = "proc-macro2" 1934 - version = "1.0.101" 2271 + version = "1.0.106" 1935 2272 source = "registry+https://github.com/rust-lang/crates.io-index" 1936 - checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" 2273 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 1937 2274 dependencies = [ 1938 2275 "unicode-ident", 1939 2276 ] ··· 1955 2292 checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" 1956 2293 dependencies = [ 1957 2294 "anyhow", 1958 - "itertools", 2295 + "itertools 0.14.0", 1959 2296 "proc-macro2", 1960 2297 "quote", 1961 2298 "syn", ··· 1977 2314 ] 1978 2315 1979 2316 [[package]] 2317 + name = "quick_cache" 2318 + version = "0.6.18" 2319 + source = "registry+https://github.com/rust-lang/crates.io-index" 2320 + checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" 2321 + dependencies = [ 2322 + "equivalent", 2323 + "hashbrown 0.16.1", 2324 + ] 2325 + 2326 + [[package]] 1980 2327 name = "quinn" 1981 2328 version = "0.11.9" 1982 2329 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1989 2336 "quinn-udp", 1990 2337 "rustc-hash", 1991 2338 "rustls", 1992 - "socket2 0.6.0", 1993 - "thiserror 2.0.16", 2339 + "socket2", 2340 + "thiserror 2.0.18", 1994 2341 "tokio", 1995 2342 "tracing", 1996 2343 "web-time", ··· 2003 2350 checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" 2004 2351 dependencies = [ 2005 2352 "bytes", 2006 - "getrandom 0.3.3", 2353 + "getrandom 0.3.4", 2007 2354 "lru-slab", 2008 2355 "rand 0.9.2", 2009 2356 "ring", ··· 2011 2358 "rustls", 2012 2359 "rustls-pki-types", 2013 2360 "slab", 2014 - "thiserror 2.0.16", 2361 + "thiserror 2.0.18", 2015 2362 "tinyvec", 2016 2363 "tracing", 2017 2364 "web-time", ··· 2026 2373 "cfg_aliases", 2027 2374 "libc", 2028 2375 "once_cell", 2029 - "socket2 0.6.0", 2376 + "socket2", 2030 2377 "tracing", 2031 2378 "windows-sys 0.60.2", 2032 2379 ] 2033 2380 2034 2381 [[package]] 2035 2382 name = "quote" 2036 - version = "1.0.40" 2383 + version = "1.0.44" 2037 2384 source = "registry+https://github.com/rust-lang/crates.io-index" 2038 - checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 2385 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 2039 2386 dependencies = [ 2040 2387 "proc-macro2", 2041 2388 ] ··· 2064 2411 checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" 2065 2412 dependencies = [ 2066 2413 "rand_chacha 0.9.0", 2067 - "rand_core 0.9.3", 2414 + "rand_core 0.9.5", 2068 2415 ] 2069 2416 2070 2417 [[package]] ··· 2084 2431 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2085 2432 dependencies = [ 2086 2433 "ppv-lite86", 2087 - "rand_core 0.9.3", 2434 + "rand_core 0.9.5", 2088 2435 ] 2089 2436 2090 2437 [[package]] ··· 2093 2440 source = "registry+https://github.com/rust-lang/crates.io-index" 2094 2441 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2095 2442 dependencies = [ 2096 - "getrandom 0.2.16", 2443 + "getrandom 0.2.17", 2097 2444 ] 2098 2445 2099 2446 [[package]] 2100 2447 name = "rand_core" 2101 - version = "0.9.3" 2448 + version = "0.9.5" 2102 2449 source = "registry+https://github.com/rust-lang/crates.io-index" 2103 - checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2450 + checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" 2104 2451 dependencies = [ 2105 - "getrandom 0.3.3", 2452 + "getrandom 0.3.4", 2106 2453 ] 2107 2454 2108 2455 [[package]] ··· 2111 2458 source = "registry+https://github.com/rust-lang/crates.io-index" 2112 2459 checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" 2113 2460 dependencies = [ 2114 - "bitflags 2.9.4", 2461 + "bitflags 2.11.0", 2462 + ] 2463 + 2464 + [[package]] 2465 + name = "rayon" 2466 + version = "1.11.0" 2467 + source = "registry+https://github.com/rust-lang/crates.io-index" 2468 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 2469 + dependencies = [ 2470 + "either", 2471 + "rayon-core", 2472 + ] 2473 + 2474 + [[package]] 2475 + name = "rayon-core" 2476 + version = "1.13.0" 2477 + source = "registry+https://github.com/rust-lang/crates.io-index" 2478 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 2479 + dependencies = [ 2480 + "crossbeam-deque", 2481 + "crossbeam-utils", 2115 2482 ] 2116 2483 2117 2484 [[package]] ··· 2137 2504 2138 2505 [[package]] 2139 2506 name = "redox_syscall" 2140 - version = "0.5.17" 2507 + version = "0.5.18" 2141 2508 source = "registry+https://github.com/rust-lang/crates.io-index" 2142 - checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" 2509 + checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 2143 2510 dependencies = [ 2144 - "bitflags 2.9.4", 2511 + "bitflags 2.11.0", 2145 2512 ] 2146 2513 2147 2514 [[package]] 2148 2515 name = "regex" 2149 - version = "1.11.2" 2516 + version = "1.12.3" 2150 2517 source = "registry+https://github.com/rust-lang/crates.io-index" 2151 - checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" 2518 + checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" 2152 2519 dependencies = [ 2153 2520 "aho-corasick", 2154 2521 "memchr", ··· 2158 2525 2159 2526 [[package]] 2160 2527 name = "regex-automata" 2161 - version = "0.4.10" 2528 + version = "0.4.14" 2162 2529 source = "registry+https://github.com/rust-lang/crates.io-index" 2163 - checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" 2530 + checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" 2164 2531 dependencies = [ 2165 2532 "aho-corasick", 2166 2533 "memchr", ··· 2169 2536 2170 2537 [[package]] 2171 2538 name = "regex-syntax" 2172 - version = "0.8.6" 2539 + version = "0.8.9" 2173 2540 source = "registry+https://github.com/rust-lang/crates.io-index" 2174 - checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" 2541 + checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" 2175 2542 2176 2543 [[package]] 2177 2544 name = "reqwest" 2178 - version = "0.12.23" 2545 + version = "0.12.28" 2179 2546 source = "registry+https://github.com/rust-lang/crates.io-index" 2180 - checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" 2547 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 2181 2548 dependencies = [ 2182 - "async-compression", 2183 2549 "base64", 2184 2550 "bytes", 2185 2551 "encoding_rs", ··· 2246 2612 "anyhow", 2247 2613 "async-trait", 2248 2614 "futures", 2249 - "getrandom 0.2.16", 2615 + "getrandom 0.2.17", 2250 2616 "http", 2251 2617 "hyper", 2252 2618 "parking_lot 0.11.2", ··· 2285 2651 dependencies = [ 2286 2652 "cc", 2287 2653 "cfg-if", 2288 - "getrandom 0.2.16", 2654 + "getrandom 0.2.17", 2289 2655 "libc", 2290 2656 "untrusted", 2291 2657 "windows-sys 0.52.0", 2292 2658 ] 2293 2659 2294 2660 [[package]] 2295 - name = "rustc-demangle" 2296 - version = "0.1.26" 2661 + name = "rmp" 2662 + version = "0.8.15" 2297 2663 source = "registry+https://github.com/rust-lang/crates.io-index" 2298 - checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" 2664 + checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" 2665 + dependencies = [ 2666 + "num-traits", 2667 + ] 2668 + 2669 + [[package]] 2670 + name = "rmp-serde" 2671 + version = "1.3.1" 2672 + source = "registry+https://github.com/rust-lang/crates.io-index" 2673 + checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" 2674 + dependencies = [ 2675 + "rmp", 2676 + "serde", 2677 + ] 2299 2678 2300 2679 [[package]] 2301 2680 name = "rustc-hash" ··· 2314 2693 2315 2694 [[package]] 2316 2695 name = "rustix" 2317 - version = "1.1.2" 2696 + version = "1.1.3" 2318 2697 source = "registry+https://github.com/rust-lang/crates.io-index" 2319 - checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 2698 + checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" 2320 2699 dependencies = [ 2321 - "bitflags 2.9.4", 2700 + "bitflags 2.11.0", 2322 2701 "errno", 2323 2702 "libc", 2324 2703 "linux-raw-sys", 2325 - "windows-sys 0.61.0", 2704 + "windows-sys 0.61.2", 2326 2705 ] 2327 2706 2328 2707 [[package]] 2329 2708 name = "rustls" 2330 - version = "0.23.32" 2709 + version = "0.23.36" 2331 2710 source = "registry+https://github.com/rust-lang/crates.io-index" 2332 - checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" 2711 + checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" 2333 2712 dependencies = [ 2334 2713 "aws-lc-rs", 2335 2714 "log", ··· 2343 2722 2344 2723 [[package]] 2345 2724 name = "rustls-native-certs" 2346 - version = "0.8.1" 2725 + version = "0.8.3" 2347 2726 source = "registry+https://github.com/rust-lang/crates.io-index" 2348 - checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 2727 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" 2349 2728 dependencies = [ 2350 2729 "openssl-probe", 2351 2730 "rustls-pki-types", 2352 2731 "schannel", 2353 - "security-framework 3.5.0", 2732 + "security-framework", 2354 2733 ] 2355 2734 2356 2735 [[package]] ··· 2364 2743 2365 2744 [[package]] 2366 2745 name = "rustls-pki-types" 2367 - version = "1.12.0" 2746 + version = "1.14.0" 2368 2747 source = "registry+https://github.com/rust-lang/crates.io-index" 2369 - checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 2748 + checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" 2370 2749 dependencies = [ 2371 2750 "web-time", 2372 2751 "zeroize", ··· 2374 2753 2375 2754 [[package]] 2376 2755 name = "rustls-webpki" 2377 - version = "0.103.5" 2756 + version = "0.103.9" 2378 2757 source = "registry+https://github.com/rust-lang/crates.io-index" 2379 - checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" 2758 + checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" 2380 2759 dependencies = [ 2381 2760 "aws-lc-rs", 2382 2761 "ring", ··· 2392 2771 2393 2772 [[package]] 2394 2773 name = "ryu" 2395 - version = "1.0.20" 2774 + version = "1.0.23" 2775 + source = "registry+https://github.com/rust-lang/crates.io-index" 2776 + checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 2777 + 2778 + [[package]] 2779 + name = "same-file" 2780 + version = "1.0.6" 2396 2781 source = "registry+https://github.com/rust-lang/crates.io-index" 2397 - checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 2782 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 2783 + dependencies = [ 2784 + "winapi-util", 2785 + ] 2398 2786 2399 2787 [[package]] 2400 2788 name = "schannel" ··· 2402 2790 source = "registry+https://github.com/rust-lang/crates.io-index" 2403 2791 checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 2404 2792 dependencies = [ 2405 - "windows-sys 0.61.0", 2793 + "windows-sys 0.61.2", 2406 2794 ] 2407 2795 2408 2796 [[package]] ··· 2413 2801 2414 2802 [[package]] 2415 2803 name = "security-framework" 2416 - version = "2.11.1" 2804 + version = "3.7.0" 2417 2805 source = "registry+https://github.com/rust-lang/crates.io-index" 2418 - checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2806 + checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" 2419 2807 dependencies = [ 2420 - "bitflags 2.9.4", 2421 - "core-foundation 0.9.4", 2808 + "bitflags 2.11.0", 2809 + "core-foundation 0.10.1", 2422 2810 "core-foundation-sys", 2423 2811 "libc", 2424 2812 "security-framework-sys", 2425 2813 ] 2426 2814 2427 2815 [[package]] 2428 - name = "security-framework" 2429 - version = "3.5.0" 2816 + name = "security-framework-sys" 2817 + version = "2.17.0" 2430 2818 source = "registry+https://github.com/rust-lang/crates.io-index" 2431 - checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" 2819 + checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" 2432 2820 dependencies = [ 2433 - "bitflags 2.9.4", 2434 - "core-foundation 0.10.1", 2435 2821 "core-foundation-sys", 2436 2822 "libc", 2437 - "security-framework-sys", 2438 2823 ] 2439 2824 2440 2825 [[package]] 2441 - name = "security-framework-sys" 2442 - version = "2.15.0" 2826 + name = "self_cell" 2827 + version = "1.2.2" 2443 2828 source = "registry+https://github.com/rust-lang/crates.io-index" 2444 - checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 2445 - dependencies = [ 2446 - "core-foundation-sys", 2447 - "libc", 2448 - ] 2829 + checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" 2830 + 2831 + [[package]] 2832 + name = "semver" 2833 + version = "1.0.27" 2834 + source = "registry+https://github.com/rust-lang/crates.io-index" 2835 + checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" 2449 2836 2450 2837 [[package]] 2451 2838 name = "serde" 2452 - version = "1.0.226" 2839 + version = "1.0.228" 2453 2840 source = "registry+https://github.com/rust-lang/crates.io-index" 2454 - checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" 2841 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2455 2842 dependencies = [ 2456 2843 "serde_core", 2457 2844 "serde_derive", 2458 2845 ] 2459 2846 2460 2847 [[package]] 2848 + name = "serde_bytes" 2849 + version = "0.11.19" 2850 + source = "registry+https://github.com/rust-lang/crates.io-index" 2851 + checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" 2852 + dependencies = [ 2853 + "serde", 2854 + "serde_core", 2855 + ] 2856 + 2857 + [[package]] 2461 2858 name = "serde_core" 2462 - version = "1.0.226" 2859 + version = "1.0.228" 2463 2860 source = "registry+https://github.com/rust-lang/crates.io-index" 2464 - checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" 2861 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 2465 2862 dependencies = [ 2466 2863 "serde_derive", 2467 2864 ] 2468 2865 2469 2866 [[package]] 2470 2867 name = "serde_derive" 2471 - version = "1.0.226" 2868 + version = "1.0.228" 2472 2869 source = "registry+https://github.com/rust-lang/crates.io-index" 2473 - checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" 2870 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 2474 2871 dependencies = [ 2475 2872 "proc-macro2", 2476 2873 "quote", ··· 2479 2876 2480 2877 [[package]] 2481 2878 name = "serde_json" 2482 - version = "1.0.143" 2879 + version = "1.0.149" 2483 2880 source = "registry+https://github.com/rust-lang/crates.io-index" 2484 - checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" 2881 + checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" 2485 2882 dependencies = [ 2486 2883 "itoa", 2487 2884 "memchr", 2488 - "ryu", 2489 2885 "serde", 2886 + "serde_core", 2887 + "zmij", 2490 2888 ] 2491 2889 2492 2890 [[package]] ··· 2502 2900 ] 2503 2901 2504 2902 [[package]] 2903 + name = "sfa" 2904 + version = "1.0.0" 2905 + source = "registry+https://github.com/rust-lang/crates.io-index" 2906 + checksum = "a1296838937cab56cd6c4eeeb8718ec777383700c33f060e2869867bd01d1175" 2907 + dependencies = [ 2908 + "byteorder-lite", 2909 + "log", 2910 + "xxhash-rust", 2911 + ] 2912 + 2913 + [[package]] 2505 2914 name = "sha1" 2506 2915 version = "0.10.6" 2507 2916 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2540 2949 2541 2950 [[package]] 2542 2951 name = "signal-hook-registry" 2543 - version = "1.4.6" 2952 + version = "1.4.8" 2544 2953 source = "registry+https://github.com/rust-lang/crates.io-index" 2545 - checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 2954 + checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" 2546 2955 dependencies = [ 2956 + "errno", 2547 2957 "libc", 2548 2958 ] 2549 2959 2550 2960 [[package]] 2961 + name = "simd-adler32" 2962 + version = "0.3.8" 2963 + source = "registry+https://github.com/rust-lang/crates.io-index" 2964 + checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" 2965 + 2966 + [[package]] 2551 2967 name = "siphasher" 2552 - version = "1.0.1" 2968 + version = "1.0.2" 2553 2969 source = "registry+https://github.com/rust-lang/crates.io-index" 2554 - checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 2970 + checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" 2555 2971 2556 2972 [[package]] 2557 2973 name = "slab" 2558 - version = "0.4.11" 2974 + version = "0.4.12" 2559 2975 source = "registry+https://github.com/rust-lang/crates.io-index" 2560 - checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 2976 + checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" 2561 2977 2562 2978 [[package]] 2563 2979 name = "smallvec" ··· 2567 2983 2568 2984 [[package]] 2569 2985 name = "socket2" 2570 - version = "0.5.10" 2986 + version = "0.6.2" 2571 2987 source = "registry+https://github.com/rust-lang/crates.io-index" 2572 - checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 2988 + checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" 2573 2989 dependencies = [ 2574 2990 "libc", 2575 - "windows-sys 0.52.0", 2991 + "windows-sys 0.60.2", 2576 2992 ] 2577 2993 2578 2994 [[package]] 2579 - name = "socket2" 2580 - version = "0.6.0" 2995 + name = "spin" 2996 + version = "0.9.8" 2581 2997 source = "registry+https://github.com/rust-lang/crates.io-index" 2582 - checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" 2998 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2583 2999 dependencies = [ 2584 - "libc", 2585 - "windows-sys 0.59.0", 3000 + "lock_api", 2586 3001 ] 2587 3002 2588 3003 [[package]] ··· 2596 3011 2597 3012 [[package]] 2598 3013 name = "stable_deref_trait" 2599 - version = "1.2.0" 3014 + version = "1.2.1" 2600 3015 source = "registry+https://github.com/rust-lang/crates.io-index" 2601 - checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3016 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 2602 3017 2603 3018 [[package]] 2604 3019 name = "stringprep" ··· 2625 3040 2626 3041 [[package]] 2627 3042 name = "syn" 2628 - version = "2.0.106" 3043 + version = "2.0.117" 2629 3044 source = "registry+https://github.com/rust-lang/crates.io-index" 2630 - checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" 3045 + checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" 2631 3046 dependencies = [ 2632 3047 "proc-macro2", 2633 3048 "quote", ··· 2656 3071 2657 3072 [[package]] 2658 3073 name = "system-configuration" 2659 - version = "0.6.1" 3074 + version = "0.7.0" 2660 3075 source = "registry+https://github.com/rust-lang/crates.io-index" 2661 - checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3076 + checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" 2662 3077 dependencies = [ 2663 - "bitflags 2.9.4", 3078 + "bitflags 2.11.0", 2664 3079 "core-foundation 0.9.4", 2665 3080 "system-configuration-sys", 2666 3081 ] ··· 2677 3092 2678 3093 [[package]] 2679 3094 name = "tempfile" 2680 - version = "3.22.0" 3095 + version = "3.25.0" 2681 3096 source = "registry+https://github.com/rust-lang/crates.io-index" 2682 - checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" 3097 + checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" 2683 3098 dependencies = [ 2684 3099 "fastrand", 2685 - "getrandom 0.3.3", 3100 + "getrandom 0.4.1", 2686 3101 "once_cell", 2687 3102 "rustix", 2688 - "windows-sys 0.61.0", 3103 + "windows-sys 0.61.2", 2689 3104 ] 2690 3105 2691 3106 [[package]] ··· 2699 3114 2700 3115 [[package]] 2701 3116 name = "thiserror" 2702 - version = "2.0.16" 3117 + version = "2.0.18" 2703 3118 source = "registry+https://github.com/rust-lang/crates.io-index" 2704 - checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" 3119 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 2705 3120 dependencies = [ 2706 - "thiserror-impl 2.0.16", 3121 + "thiserror-impl 2.0.18", 2707 3122 ] 2708 3123 2709 3124 [[package]] ··· 2719 3134 2720 3135 [[package]] 2721 3136 name = "thiserror-impl" 2722 - version = "2.0.16" 3137 + version = "2.0.18" 2723 3138 source = "registry+https://github.com/rust-lang/crates.io-index" 2724 - checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" 3139 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 2725 3140 dependencies = [ 2726 3141 "proc-macro2", 2727 3142 "quote", ··· 2739 3154 2740 3155 [[package]] 2741 3156 name = "time" 2742 - version = "0.3.44" 3157 + version = "0.3.47" 2743 3158 source = "registry+https://github.com/rust-lang/crates.io-index" 2744 - checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 3159 + checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" 2745 3160 dependencies = [ 2746 3161 "deranged", 2747 3162 "itoa", 2748 3163 "num-conv", 2749 3164 "powerfmt", 2750 - "serde", 3165 + "serde_core", 2751 3166 "time-core", 2752 3167 "time-macros", 2753 3168 ] 2754 3169 2755 3170 [[package]] 2756 3171 name = "time-core" 2757 - version = "0.1.6" 3172 + version = "0.1.8" 2758 3173 source = "registry+https://github.com/rust-lang/crates.io-index" 2759 - checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 3174 + checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" 2760 3175 2761 3176 [[package]] 2762 3177 name = "time-macros" 2763 - version = "0.2.24" 3178 + version = "0.2.27" 2764 3179 source = "registry+https://github.com/rust-lang/crates.io-index" 2765 - checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" 3180 + checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" 2766 3181 dependencies = [ 2767 3182 "num-conv", 2768 3183 "time-core", ··· 2770 3185 2771 3186 [[package]] 2772 3187 name = "tinystr" 2773 - version = "0.8.1" 3188 + version = "0.8.2" 2774 3189 source = "registry+https://github.com/rust-lang/crates.io-index" 2775 - checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 3190 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 2776 3191 dependencies = [ 2777 3192 "displaydoc", 2778 3193 "zerovec", 2779 3194 ] 2780 3195 2781 3196 [[package]] 3197 + name = "tinytemplate" 3198 + version = "1.2.1" 3199 + source = "registry+https://github.com/rust-lang/crates.io-index" 3200 + checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 3201 + dependencies = [ 3202 + "serde", 3203 + "serde_json", 3204 + ] 3205 + 3206 + [[package]] 2782 3207 name = "tinyvec" 2783 3208 version = "1.10.0" 2784 3209 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2795 3220 2796 3221 [[package]] 2797 3222 name = "tokio" 2798 - version = "1.47.1" 3223 + version = "1.49.0" 2799 3224 source = "registry+https://github.com/rust-lang/crates.io-index" 2800 - checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" 3225 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 2801 3226 dependencies = [ 2802 - "backtrace", 2803 3227 "bytes", 2804 - "io-uring", 2805 3228 "libc", 2806 3229 "mio", 2807 - "parking_lot 0.12.4", 3230 + "parking_lot 0.12.5", 2808 3231 "pin-project-lite", 2809 3232 "signal-hook-registry", 2810 - "slab", 2811 - "socket2 0.6.0", 3233 + "socket2", 2812 3234 "tokio-macros", 2813 - "windows-sys 0.59.0", 3235 + "windows-sys 0.61.2", 2814 3236 ] 2815 3237 2816 3238 [[package]] 2817 3239 name = "tokio-macros" 2818 - version = "2.5.0" 3240 + version = "2.6.0" 2819 3241 source = "registry+https://github.com/rust-lang/crates.io-index" 2820 - checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 3242 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 2821 3243 dependencies = [ 2822 3244 "proc-macro2", 2823 3245 "quote", ··· 2836 3258 2837 3259 [[package]] 2838 3260 name = "tokio-postgres" 2839 - version = "0.7.13" 3261 + version = "0.7.16" 2840 3262 source = "registry+https://github.com/rust-lang/crates.io-index" 2841 - checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" 3263 + checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" 2842 3264 dependencies = [ 2843 3265 "async-trait", 2844 3266 "byteorder", ··· 2847 3269 "futures-channel", 2848 3270 "futures-util", 2849 3271 "log", 2850 - "parking_lot 0.12.4", 3272 + "parking_lot 0.12.5", 2851 3273 "percent-encoding", 2852 3274 "phf", 2853 3275 "pin-project-lite", 2854 3276 "postgres-protocol", 2855 3277 "postgres-types", 2856 3278 "rand 0.9.2", 2857 - "socket2 0.5.10", 3279 + "socket2", 2858 3280 "tokio", 2859 3281 "tokio-util", 2860 3282 "whoami", ··· 2862 3284 2863 3285 [[package]] 2864 3286 name = "tokio-rustls" 2865 - version = "0.26.2" 3287 + version = "0.26.4" 2866 3288 source = "registry+https://github.com/rust-lang/crates.io-index" 2867 - checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 3289 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 2868 3290 dependencies = [ 2869 3291 "rustls", 2870 3292 "tokio", ··· 2872 3294 2873 3295 [[package]] 2874 3296 name = "tokio-stream" 2875 - version = "0.1.17" 3297 + version = "0.1.18" 2876 3298 source = "registry+https://github.com/rust-lang/crates.io-index" 2877 - checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 3299 + checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" 2878 3300 dependencies = [ 2879 3301 "futures-core", 2880 3302 "pin-project-lite", ··· 2883 3305 2884 3306 [[package]] 2885 3307 name = "tokio-util" 2886 - version = "0.7.16" 3308 + version = "0.7.18" 2887 3309 source = "registry+https://github.com/rust-lang/crates.io-index" 2888 - checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" 3310 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 2889 3311 dependencies = [ 2890 3312 "bytes", 2891 3313 "futures-core", ··· 2897 3319 2898 3320 [[package]] 2899 3321 name = "toml_datetime" 2900 - version = "0.7.2" 3322 + version = "0.7.5+spec-1.1.0" 2901 3323 source = "registry+https://github.com/rust-lang/crates.io-index" 2902 - checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" 3324 + checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" 2903 3325 dependencies = [ 2904 3326 "serde_core", 2905 3327 ] 2906 3328 2907 3329 [[package]] 2908 3330 name = "toml_edit" 2909 - version = "0.23.6" 3331 + version = "0.23.10+spec-1.0.0" 2910 3332 source = "registry+https://github.com/rust-lang/crates.io-index" 2911 - checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" 3333 + checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" 2912 3334 dependencies = [ 2913 3335 "indexmap", 2914 3336 "toml_datetime", ··· 2918 3340 2919 3341 [[package]] 2920 3342 name = "toml_parser" 2921 - version = "1.0.3" 3343 + version = "1.0.9+spec-1.1.0" 2922 3344 source = "registry+https://github.com/rust-lang/crates.io-index" 2923 - checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" 3345 + checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" 2924 3346 dependencies = [ 2925 3347 "winnow", 2926 3348 ] ··· 2948 3370 2949 3371 [[package]] 2950 3372 name = "tower" 2951 - version = "0.5.2" 3373 + version = "0.5.3" 2952 3374 source = "registry+https://github.com/rust-lang/crates.io-index" 2953 - checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 3375 + checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" 2954 3376 dependencies = [ 2955 3377 "futures-core", 2956 3378 "futures-util", ··· 2963 3385 2964 3386 [[package]] 2965 3387 name = "tower-http" 2966 - version = "0.6.6" 3388 + version = "0.6.8" 2967 3389 source = "registry+https://github.com/rust-lang/crates.io-index" 2968 - checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 3390 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 2969 3391 dependencies = [ 2970 - "bitflags 2.9.4", 3392 + "async-compression", 3393 + "bitflags 2.11.0", 2971 3394 "bytes", 3395 + "futures-core", 2972 3396 "futures-util", 2973 3397 "http", 2974 3398 "http-body", 3399 + "http-body-util", 2975 3400 "iri-string", 2976 3401 "pin-project-lite", 3402 + "tokio", 3403 + "tokio-util", 2977 3404 "tower", 2978 3405 "tower-layer", 2979 3406 "tower-service", ··· 2993 3420 2994 3421 [[package]] 2995 3422 name = "tracing" 2996 - version = "0.1.41" 3423 + version = "0.1.44" 2997 3424 source = "registry+https://github.com/rust-lang/crates.io-index" 2998 - checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 3425 + checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" 2999 3426 dependencies = [ 3000 3427 "pin-project-lite", 3001 3428 "tracing-attributes", ··· 3004 3431 3005 3432 [[package]] 3006 3433 name = "tracing-attributes" 3007 - version = "0.1.30" 3434 + version = "0.1.31" 3008 3435 source = "registry+https://github.com/rust-lang/crates.io-index" 3009 - checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 3436 + checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" 3010 3437 dependencies = [ 3011 3438 "proc-macro2", 3012 3439 "quote", ··· 3015 3442 3016 3443 [[package]] 3017 3444 name = "tracing-core" 3018 - version = "0.1.34" 3445 + version = "0.1.36" 3019 3446 source = "registry+https://github.com/rust-lang/crates.io-index" 3020 - checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 3447 + checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" 3021 3448 dependencies = [ 3022 3449 "once_cell", 3023 3450 "valuable", ··· 3054 3481 3055 3482 [[package]] 3056 3483 name = "tracing-subscriber" 3057 - version = "0.3.20" 3484 + version = "0.3.22" 3058 3485 source = "registry+https://github.com/rust-lang/crates.io-index" 3059 - checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 3486 + checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" 3060 3487 dependencies = [ 3061 3488 "matchers", 3062 3489 "nu-ansi-term", ··· 3077 3504 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 3078 3505 3079 3506 [[package]] 3507 + name = "twox-hash" 3508 + version = "2.1.2" 3509 + source = "registry+https://github.com/rust-lang/crates.io-index" 3510 + checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" 3511 + 3512 + [[package]] 3080 3513 name = "typenum" 3081 - version = "1.18.0" 3514 + version = "1.19.0" 3082 3515 source = "registry+https://github.com/rust-lang/crates.io-index" 3083 - checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 3516 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 3084 3517 3085 3518 [[package]] 3086 3519 name = "uncased" ··· 3099 3532 3100 3533 [[package]] 3101 3534 name = "unicode-ident" 3102 - version = "1.0.19" 3535 + version = "1.0.24" 3103 3536 source = "registry+https://github.com/rust-lang/crates.io-index" 3104 - checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" 3537 + checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" 3105 3538 3106 3539 [[package]] 3107 3540 name = "unicode-normalization" 3108 - version = "0.1.24" 3541 + version = "0.1.25" 3109 3542 source = "registry+https://github.com/rust-lang/crates.io-index" 3110 - checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 3543 + checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" 3111 3544 dependencies = [ 3112 3545 "tinyvec", 3113 3546 ] 3114 3547 3115 3548 [[package]] 3116 3549 name = "unicode-properties" 3117 - version = "0.1.3" 3550 + version = "0.1.4" 3118 3551 source = "registry+https://github.com/rust-lang/crates.io-index" 3119 - checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 3552 + checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" 3553 + 3554 + [[package]] 3555 + name = "unicode-xid" 3556 + version = "0.2.6" 3557 + source = "registry+https://github.com/rust-lang/crates.io-index" 3558 + checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 3559 + 3560 + [[package]] 3561 + name = "unsigned-varint" 3562 + version = "0.8.0" 3563 + source = "registry+https://github.com/rust-lang/crates.io-index" 3564 + checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 3120 3565 3121 3566 [[package]] 3122 3567 name = "untrusted" ··· 3126 3571 3127 3572 [[package]] 3128 3573 name = "url" 3129 - version = "2.5.7" 3574 + version = "2.5.8" 3130 3575 source = "registry+https://github.com/rust-lang/crates.io-index" 3131 - checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 3576 + checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" 3132 3577 dependencies = [ 3133 3578 "form_urlencoded", 3134 3579 "idna", ··· 3155 3600 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 3156 3601 3157 3602 [[package]] 3603 + name = "varint-rs" 3604 + version = "2.2.0" 3605 + source = "registry+https://github.com/rust-lang/crates.io-index" 3606 + checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" 3607 + 3608 + [[package]] 3158 3609 name = "vcpkg" 3159 3610 version = "0.2.15" 3160 3611 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3167 3618 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 3168 3619 3169 3620 [[package]] 3621 + name = "walkdir" 3622 + version = "2.5.0" 3623 + source = "registry+https://github.com/rust-lang/crates.io-index" 3624 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 3625 + dependencies = [ 3626 + "same-file", 3627 + "winapi-util", 3628 + ] 3629 + 3630 + [[package]] 3170 3631 name = "want" 3171 3632 version = "0.3.1" 3172 3633 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3183 3644 3184 3645 [[package]] 3185 3646 name = "wasi" 3186 - version = "0.14.5+wasi-0.2.4" 3647 + version = "0.14.7+wasi-0.2.4" 3187 3648 source = "registry+https://github.com/rust-lang/crates.io-index" 3188 - checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" 3649 + checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" 3189 3650 dependencies = [ 3190 3651 "wasip2", 3191 3652 ] 3192 3653 3193 3654 [[package]] 3194 3655 name = "wasip2" 3195 - version = "1.0.0+wasi-0.2.4" 3656 + version = "1.0.2+wasi-0.2.9" 3657 + source = "registry+https://github.com/rust-lang/crates.io-index" 3658 + checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" 3659 + dependencies = [ 3660 + "wit-bindgen", 3661 + ] 3662 + 3663 + [[package]] 3664 + name = "wasip3" 3665 + version = "0.4.0+wasi-0.3.0-rc-2026-01-06" 3196 3666 source = "registry+https://github.com/rust-lang/crates.io-index" 3197 - checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" 3667 + checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" 3198 3668 dependencies = [ 3199 3669 "wit-bindgen", 3200 3670 ] 3201 3671 3202 3672 [[package]] 3203 3673 name = "wasite" 3204 - version = "0.1.0" 3674 + version = "1.0.2" 3205 3675 source = "registry+https://github.com/rust-lang/crates.io-index" 3206 - checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 3676 + checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" 3677 + dependencies = [ 3678 + "wasi 0.14.7+wasi-0.2.4", 3679 + ] 3207 3680 3208 3681 [[package]] 3209 3682 name = "wasm-bindgen" 3210 - version = "0.2.101" 3683 + version = "0.2.109" 3211 3684 source = "registry+https://github.com/rust-lang/crates.io-index" 3212 - checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" 3685 + checksum = "9ff9c7baef35ac3c0e17d8bfc9ad75eb62f85a2f02bccc906699dadb0aa9c622" 3213 3686 dependencies = [ 3214 3687 "cfg-if", 3215 3688 "once_cell", ··· 3219 3692 ] 3220 3693 3221 3694 [[package]] 3222 - name = "wasm-bindgen-backend" 3223 - version = "0.2.101" 3224 - source = "registry+https://github.com/rust-lang/crates.io-index" 3225 - checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" 3226 - dependencies = [ 3227 - "bumpalo", 3228 - "log", 3229 - "proc-macro2", 3230 - "quote", 3231 - "syn", 3232 - "wasm-bindgen-shared", 3233 - ] 3234 - 3235 - [[package]] 3236 3695 name = "wasm-bindgen-futures" 3237 - version = "0.4.51" 3696 + version = "0.4.59" 3238 3697 source = "registry+https://github.com/rust-lang/crates.io-index" 3239 - checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" 3698 + checksum = "d24699cd39db9966cf6e2ef10d2f72779c961ad905911f395ea201c3ec9f545d" 3240 3699 dependencies = [ 3241 3700 "cfg-if", 3701 + "futures-util", 3242 3702 "js-sys", 3243 3703 "once_cell", 3244 3704 "wasm-bindgen", ··· 3247 3707 3248 3708 [[package]] 3249 3709 name = "wasm-bindgen-macro" 3250 - version = "0.2.101" 3710 + version = "0.2.109" 3251 3711 source = "registry+https://github.com/rust-lang/crates.io-index" 3252 - checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" 3712 + checksum = "39455e84ad887a0bbc93c116d72403f1bb0a39e37dd6f235a43e2128a0c7f1fd" 3253 3713 dependencies = [ 3254 3714 "quote", 3255 3715 "wasm-bindgen-macro-support", ··· 3257 3717 3258 3718 [[package]] 3259 3719 name = "wasm-bindgen-macro-support" 3260 - version = "0.2.101" 3720 + version = "0.2.109" 3261 3721 source = "registry+https://github.com/rust-lang/crates.io-index" 3262 - checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" 3722 + checksum = "dff4761f60b0b51fd13fec8764167b7bbcc34498ce3e52805fe1db6f2d56b6d6" 3263 3723 dependencies = [ 3724 + "bumpalo", 3264 3725 "proc-macro2", 3265 3726 "quote", 3266 3727 "syn", 3267 - "wasm-bindgen-backend", 3268 3728 "wasm-bindgen-shared", 3269 3729 ] 3270 3730 3271 3731 [[package]] 3272 3732 name = "wasm-bindgen-shared" 3273 - version = "0.2.101" 3733 + version = "0.2.109" 3274 3734 source = "registry+https://github.com/rust-lang/crates.io-index" 3275 - checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" 3735 + checksum = "bc6a171c53d98021a93a474c4a4579d76ba97f9517d871bc12e27640f218b6dd" 3276 3736 dependencies = [ 3277 3737 "unicode-ident", 3278 3738 ] 3279 3739 3280 3740 [[package]] 3741 + name = "wasm-encoder" 3742 + version = "0.244.0" 3743 + source = "registry+https://github.com/rust-lang/crates.io-index" 3744 + checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" 3745 + dependencies = [ 3746 + "leb128fmt", 3747 + "wasmparser", 3748 + ] 3749 + 3750 + [[package]] 3751 + name = "wasm-metadata" 3752 + version = "0.244.0" 3753 + source = "registry+https://github.com/rust-lang/crates.io-index" 3754 + checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 3755 + dependencies = [ 3756 + "anyhow", 3757 + "indexmap", 3758 + "wasm-encoder", 3759 + "wasmparser", 3760 + ] 3761 + 3762 + [[package]] 3281 3763 name = "wasm-streams" 3282 3764 version = "0.4.2" 3283 3765 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3306 3788 ] 3307 3789 3308 3790 [[package]] 3791 + name = "wasmparser" 3792 + version = "0.244.0" 3793 + source = "registry+https://github.com/rust-lang/crates.io-index" 3794 + checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" 3795 + dependencies = [ 3796 + "bitflags 2.11.0", 3797 + "hashbrown 0.15.5", 3798 + "indexmap", 3799 + "semver", 3800 + ] 3801 + 3802 + [[package]] 3309 3803 name = "web-sys" 3310 - version = "0.3.78" 3804 + version = "0.3.86" 3311 3805 source = "registry+https://github.com/rust-lang/crates.io-index" 3312 - checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" 3806 + checksum = "668fa5d00434e890a452ab060d24e3904d1be93f7bb01b70e5603baa2b8ab23b" 3313 3807 dependencies = [ 3314 3808 "js-sys", 3315 3809 "wasm-bindgen", ··· 3327 3821 3328 3822 [[package]] 3329 3823 name = "whoami" 3330 - version = "1.6.1" 3824 + version = "2.1.1" 3331 3825 source = "registry+https://github.com/rust-lang/crates.io-index" 3332 - checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 3826 + checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" 3333 3827 dependencies = [ 3828 + "libc", 3334 3829 "libredox", 3830 + "objc2-system-configuration", 3335 3831 "wasite", 3336 3832 "web-sys", 3337 3833 ] 3338 3834 3339 3835 [[package]] 3340 3836 name = "wildmatch" 3341 - version = "2.5.0" 3837 + version = "2.6.1" 3342 3838 source = "registry+https://github.com/rust-lang/crates.io-index" 3343 - checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" 3839 + checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" 3344 3840 3345 3841 [[package]] 3346 3842 name = "winapi" ··· 3359 3855 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 3360 3856 3361 3857 [[package]] 3858 + name = "winapi-util" 3859 + version = "0.1.11" 3860 + source = "registry+https://github.com/rust-lang/crates.io-index" 3861 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" 3862 + dependencies = [ 3863 + "windows-sys 0.61.2", 3864 + ] 3865 + 3866 + [[package]] 3362 3867 name = "winapi-x86_64-pc-windows-gnu" 3363 3868 version = "0.4.0" 3364 3869 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3366 3871 3367 3872 [[package]] 3368 3873 name = "windows-core" 3369 - version = "0.61.2" 3874 + version = "0.62.2" 3370 3875 source = "registry+https://github.com/rust-lang/crates.io-index" 3371 - checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 3876 + checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 3372 3877 dependencies = [ 3373 3878 "windows-implement", 3374 3879 "windows-interface", 3375 - "windows-link 0.1.3", 3880 + "windows-link", 3376 3881 "windows-result", 3377 3882 "windows-strings", 3378 3883 ] 3379 3884 3380 3885 [[package]] 3381 3886 name = "windows-implement" 3382 - version = "0.60.0" 3887 + version = "0.60.2" 3383 3888 source = "registry+https://github.com/rust-lang/crates.io-index" 3384 - checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 3889 + checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 3385 3890 dependencies = [ 3386 3891 "proc-macro2", 3387 3892 "quote", ··· 3390 3895 3391 3896 [[package]] 3392 3897 name = "windows-interface" 3393 - version = "0.59.1" 3898 + version = "0.59.3" 3394 3899 source = "registry+https://github.com/rust-lang/crates.io-index" 3395 - checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 3900 + checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 3396 3901 dependencies = [ 3397 3902 "proc-macro2", 3398 3903 "quote", ··· 3401 3906 3402 3907 [[package]] 3403 3908 name = "windows-link" 3404 - version = "0.1.3" 3909 + version = "0.2.1" 3405 3910 source = "registry+https://github.com/rust-lang/crates.io-index" 3406 - checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 3407 - 3408 - [[package]] 3409 - name = "windows-link" 3410 - version = "0.2.0" 3411 - source = "registry+https://github.com/rust-lang/crates.io-index" 3412 - checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" 3911 + checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 3413 3912 3414 3913 [[package]] 3415 3914 name = "windows-registry" 3416 - version = "0.5.3" 3915 + version = "0.6.1" 3417 3916 source = "registry+https://github.com/rust-lang/crates.io-index" 3418 - checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" 3917 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 3419 3918 dependencies = [ 3420 - "windows-link 0.1.3", 3919 + "windows-link", 3421 3920 "windows-result", 3422 3921 "windows-strings", 3423 3922 ] 3424 3923 3425 3924 [[package]] 3426 3925 name = "windows-result" 3427 - version = "0.3.4" 3926 + version = "0.4.1" 3428 3927 source = "registry+https://github.com/rust-lang/crates.io-index" 3429 - checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 3928 + checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 3430 3929 dependencies = [ 3431 - "windows-link 0.1.3", 3930 + "windows-link", 3432 3931 ] 3433 3932 3434 3933 [[package]] 3435 3934 name = "windows-strings" 3436 - version = "0.4.2" 3935 + version = "0.5.1" 3437 3936 source = "registry+https://github.com/rust-lang/crates.io-index" 3438 - checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 3937 + checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 3439 3938 dependencies = [ 3440 - "windows-link 0.1.3", 3939 + "windows-link", 3441 3940 ] 3442 3941 3443 3942 [[package]] ··· 3451 3950 3452 3951 [[package]] 3453 3952 name = "windows-sys" 3454 - version = "0.59.0" 3455 - source = "registry+https://github.com/rust-lang/crates.io-index" 3456 - checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3457 - dependencies = [ 3458 - "windows-targets 0.52.6", 3459 - ] 3460 - 3461 - [[package]] 3462 - name = "windows-sys" 3463 3953 version = "0.60.2" 3464 3954 source = "registry+https://github.com/rust-lang/crates.io-index" 3465 3955 checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 3466 3956 dependencies = [ 3467 - "windows-targets 0.53.3", 3957 + "windows-targets 0.53.5", 3468 3958 ] 3469 3959 3470 3960 [[package]] 3471 3961 name = "windows-sys" 3472 - version = "0.61.0" 3962 + version = "0.61.2" 3473 3963 source = "registry+https://github.com/rust-lang/crates.io-index" 3474 - checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" 3964 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 3475 3965 dependencies = [ 3476 - "windows-link 0.2.0", 3966 + "windows-link", 3477 3967 ] 3478 3968 3479 3969 [[package]] ··· 3494 3984 3495 3985 [[package]] 3496 3986 name = "windows-targets" 3497 - version = "0.53.3" 3987 + version = "0.53.5" 3498 3988 source = "registry+https://github.com/rust-lang/crates.io-index" 3499 - checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" 3989 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 3500 3990 dependencies = [ 3501 - "windows-link 0.1.3", 3502 - "windows_aarch64_gnullvm 0.53.0", 3503 - "windows_aarch64_msvc 0.53.0", 3504 - "windows_i686_gnu 0.53.0", 3505 - "windows_i686_gnullvm 0.53.0", 3506 - "windows_i686_msvc 0.53.0", 3507 - "windows_x86_64_gnu 0.53.0", 3508 - "windows_x86_64_gnullvm 0.53.0", 3509 - "windows_x86_64_msvc 0.53.0", 3991 + "windows-link", 3992 + "windows_aarch64_gnullvm 0.53.1", 3993 + "windows_aarch64_msvc 0.53.1", 3994 + "windows_i686_gnu 0.53.1", 3995 + "windows_i686_gnullvm 0.53.1", 3996 + "windows_i686_msvc 0.53.1", 3997 + "windows_x86_64_gnu 0.53.1", 3998 + "windows_x86_64_gnullvm 0.53.1", 3999 + "windows_x86_64_msvc 0.53.1", 3510 4000 ] 3511 4001 3512 4002 [[package]] ··· 3517 4007 3518 4008 [[package]] 3519 4009 name = "windows_aarch64_gnullvm" 3520 - version = "0.53.0" 4010 + version = "0.53.1" 3521 4011 source = "registry+https://github.com/rust-lang/crates.io-index" 3522 - checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 4012 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 3523 4013 3524 4014 [[package]] 3525 4015 name = "windows_aarch64_msvc" ··· 3529 4019 3530 4020 [[package]] 3531 4021 name = "windows_aarch64_msvc" 3532 - version = "0.53.0" 4022 + version = "0.53.1" 3533 4023 source = "registry+https://github.com/rust-lang/crates.io-index" 3534 - checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 4024 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 3535 4025 3536 4026 [[package]] 3537 4027 name = "windows_i686_gnu" ··· 3541 4031 3542 4032 [[package]] 3543 4033 name = "windows_i686_gnu" 3544 - version = "0.53.0" 4034 + version = "0.53.1" 3545 4035 source = "registry+https://github.com/rust-lang/crates.io-index" 3546 - checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 4036 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 3547 4037 3548 4038 [[package]] 3549 4039 name = "windows_i686_gnullvm" ··· 3553 4043 3554 4044 [[package]] 3555 4045 name = "windows_i686_gnullvm" 3556 - version = "0.53.0" 4046 + version = "0.53.1" 3557 4047 source = "registry+https://github.com/rust-lang/crates.io-index" 3558 - checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 4048 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 3559 4049 3560 4050 [[package]] 3561 4051 name = "windows_i686_msvc" ··· 3565 4055 3566 4056 [[package]] 3567 4057 name = "windows_i686_msvc" 3568 - version = "0.53.0" 4058 + version = "0.53.1" 3569 4059 source = "registry+https://github.com/rust-lang/crates.io-index" 3570 - checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 4060 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 3571 4061 3572 4062 [[package]] 3573 4063 name = "windows_x86_64_gnu" ··· 3577 4067 3578 4068 [[package]] 3579 4069 name = "windows_x86_64_gnu" 3580 - version = "0.53.0" 4070 + version = "0.53.1" 3581 4071 source = "registry+https://github.com/rust-lang/crates.io-index" 3582 - checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 4072 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 3583 4073 3584 4074 [[package]] 3585 4075 name = "windows_x86_64_gnullvm" ··· 3589 4079 3590 4080 [[package]] 3591 4081 name = "windows_x86_64_gnullvm" 3592 - version = "0.53.0" 4082 + version = "0.53.1" 3593 4083 source = "registry+https://github.com/rust-lang/crates.io-index" 3594 - checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 4084 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 3595 4085 3596 4086 [[package]] 3597 4087 name = "windows_x86_64_msvc" ··· 3601 4091 3602 4092 [[package]] 3603 4093 name = "windows_x86_64_msvc" 3604 - version = "0.53.0" 4094 + version = "0.53.1" 3605 4095 source = "registry+https://github.com/rust-lang/crates.io-index" 3606 - checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 4096 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 3607 4097 3608 4098 [[package]] 3609 4099 name = "winnow" 3610 - version = "0.7.13" 4100 + version = "0.7.14" 3611 4101 source = "registry+https://github.com/rust-lang/crates.io-index" 3612 - checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 4102 + checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" 3613 4103 dependencies = [ 3614 4104 "memchr", 3615 4105 ] 3616 4106 3617 4107 [[package]] 3618 4108 name = "wit-bindgen" 3619 - version = "0.45.1" 4109 + version = "0.51.0" 4110 + source = "registry+https://github.com/rust-lang/crates.io-index" 4111 + checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 4112 + dependencies = [ 4113 + "wit-bindgen-rust-macro", 4114 + ] 4115 + 4116 + [[package]] 4117 + name = "wit-bindgen-core" 4118 + version = "0.51.0" 3620 4119 source = "registry+https://github.com/rust-lang/crates.io-index" 3621 - checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" 4120 + checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" 4121 + dependencies = [ 4122 + "anyhow", 4123 + "heck", 4124 + "wit-parser", 4125 + ] 4126 + 4127 + [[package]] 4128 + name = "wit-bindgen-rust" 4129 + version = "0.51.0" 4130 + source = "registry+https://github.com/rust-lang/crates.io-index" 4131 + checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" 4132 + dependencies = [ 4133 + "anyhow", 4134 + "heck", 4135 + "indexmap", 4136 + "prettyplease", 4137 + "syn", 4138 + "wasm-metadata", 4139 + "wit-bindgen-core", 4140 + "wit-component", 4141 + ] 4142 + 4143 + [[package]] 4144 + name = "wit-bindgen-rust-macro" 4145 + version = "0.51.0" 4146 + source = "registry+https://github.com/rust-lang/crates.io-index" 4147 + checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" 4148 + dependencies = [ 4149 + "anyhow", 4150 + "prettyplease", 4151 + "proc-macro2", 4152 + "quote", 4153 + "syn", 4154 + "wit-bindgen-core", 4155 + "wit-bindgen-rust", 4156 + ] 4157 + 4158 + [[package]] 4159 + name = "wit-component" 4160 + version = "0.244.0" 4161 + source = "registry+https://github.com/rust-lang/crates.io-index" 4162 + checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" 4163 + dependencies = [ 4164 + "anyhow", 4165 + "bitflags 2.11.0", 4166 + "indexmap", 4167 + "log", 4168 + "serde", 4169 + "serde_derive", 4170 + "serde_json", 4171 + "wasm-encoder", 4172 + "wasm-metadata", 4173 + "wasmparser", 4174 + "wit-parser", 4175 + ] 4176 + 4177 + [[package]] 4178 + name = "wit-parser" 4179 + version = "0.244.0" 4180 + source = "registry+https://github.com/rust-lang/crates.io-index" 4181 + checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" 4182 + dependencies = [ 4183 + "anyhow", 4184 + "id-arena", 4185 + "indexmap", 4186 + "log", 4187 + "semver", 4188 + "serde", 4189 + "serde_derive", 4190 + "serde_json", 4191 + "unicode-xid", 4192 + "wasmparser", 4193 + ] 3622 4194 3623 4195 [[package]] 3624 4196 name = "writeable" 3625 - version = "0.6.1" 4197 + version = "0.6.2" 3626 4198 source = "registry+https://github.com/rust-lang/crates.io-index" 3627 - checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 4199 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 3628 4200 3629 4201 [[package]] 3630 4202 name = "x509-parser" ··· 3639 4211 "nom", 3640 4212 "oid-registry", 3641 4213 "rusticata-macros", 3642 - "thiserror 2.0.16", 4214 + "thiserror 2.0.18", 3643 4215 "time", 3644 4216 ] 4217 + 4218 + [[package]] 4219 + name = "xxhash-rust" 4220 + version = "0.8.15" 4221 + source = "registry+https://github.com/rust-lang/crates.io-index" 4222 + checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" 3645 4223 3646 4224 [[package]] 3647 4225 name = "yasna" ··· 3654 4232 3655 4233 [[package]] 3656 4234 name = "yoke" 3657 - version = "0.8.0" 4235 + version = "0.8.1" 3658 4236 source = "registry+https://github.com/rust-lang/crates.io-index" 3659 - checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 4237 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 3660 4238 dependencies = [ 3661 - "serde", 3662 4239 "stable_deref_trait", 3663 4240 "yoke-derive", 3664 4241 "zerofrom", ··· 3666 4243 3667 4244 [[package]] 3668 4245 name = "yoke-derive" 3669 - version = "0.8.0" 4246 + version = "0.8.1" 3670 4247 source = "registry+https://github.com/rust-lang/crates.io-index" 3671 - checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 4248 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 3672 4249 dependencies = [ 3673 4250 "proc-macro2", 3674 4251 "quote", ··· 3678 4255 3679 4256 [[package]] 3680 4257 name = "zerocopy" 3681 - version = "0.8.27" 4258 + version = "0.8.39" 3682 4259 source = "registry+https://github.com/rust-lang/crates.io-index" 3683 - checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" 4260 + checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" 3684 4261 dependencies = [ 3685 4262 "zerocopy-derive", 3686 4263 ] 3687 4264 3688 4265 [[package]] 3689 4266 name = "zerocopy-derive" 3690 - version = "0.8.27" 4267 + version = "0.8.39" 3691 4268 source = "registry+https://github.com/rust-lang/crates.io-index" 3692 - checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" 4269 + checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" 3693 4270 dependencies = [ 3694 4271 "proc-macro2", 3695 4272 "quote", ··· 3719 4296 3720 4297 [[package]] 3721 4298 name = "zeroize" 3722 - version = "1.8.1" 4299 + version = "1.8.2" 3723 4300 source = "registry+https://github.com/rust-lang/crates.io-index" 3724 - checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 4301 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 3725 4302 3726 4303 [[package]] 3727 4304 name = "zerotrie" 3728 - version = "0.2.2" 4305 + version = "0.2.3" 3729 4306 source = "registry+https://github.com/rust-lang/crates.io-index" 3730 - checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 4307 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 3731 4308 dependencies = [ 3732 4309 "displaydoc", 3733 4310 "yoke", ··· 3736 4313 3737 4314 [[package]] 3738 4315 name = "zerovec" 3739 - version = "0.11.4" 4316 + version = "0.11.5" 3740 4317 source = "registry+https://github.com/rust-lang/crates.io-index" 3741 - checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" 4318 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 3742 4319 dependencies = [ 3743 4320 "yoke", 3744 4321 "zerofrom", ··· 3747 4324 3748 4325 [[package]] 3749 4326 name = "zerovec-derive" 3750 - version = "0.11.1" 4327 + version = "0.11.2" 3751 4328 source = "registry+https://github.com/rust-lang/crates.io-index" 3752 - checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 4329 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 3753 4330 dependencies = [ 3754 4331 "proc-macro2", 3755 4332 "quote", 3756 4333 "syn", 3757 4334 ] 4335 + 4336 + [[package]] 4337 + name = "zmij" 4338 + version = "1.0.21" 4339 + source = "registry+https://github.com/rust-lang/crates.io-index" 4340 + checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" 3758 4341 3759 4342 [[package]] 3760 4343 name = "zstd"
+15
Cargo.toml
··· 6 6 edition = "2024" 7 7 default-run = "allegedly" 8 8 9 + 10 + [dev-dependencies] 11 + criterion = "0.8.2" 12 + rmp-serde = "1.3.1" 13 + tempfile = "3.10.1" 14 + 9 15 [dependencies] 10 16 anyhow = "1.0.99" 17 + async-trait = "0.1" 11 18 async-compression = { version = "0.4.30", features = ["futures-io", "tokio", "gzip"] } 12 19 chrono = { version = "0.4.42", features = ["serde"] } 13 20 clap = { version = "4.5.47", features = ["derive", "env"] } 21 + fjall = "3.0.2" 14 22 futures = "0.3.31" 15 23 governor = "0.10.1" 16 24 http-body-util = "0.1.3" ··· 35 43 tracing = "0.1.41" 36 44 tracing-opentelemetry = "0.31.0" 37 45 tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } 46 + data-encoding = "2.10.0" 47 + cid = "0.11.1" 48 + rmp-serde = "1.3.1" 49 + bincode = "1.3.3" 50 + serde_bytes = "0.11.19" 51 + multibase = "0.9.2" 52 +
+61
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1771177547, 6 + "narHash": "sha256-trTtk3WTOHz7hSw89xIIvahkgoFJYQ0G43IlqprFoMA=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "ac055f38c798b0d87695240c7b761b82fc7e5bc2", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixpkgs-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "nixpkgs-lib": { 20 + "locked": { 21 + "lastModified": 1769909678, 22 + "narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=", 23 + "owner": "nix-community", 24 + "repo": "nixpkgs.lib", 25 + "rev": "72716169fe93074c333e8d0173151350670b824c", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "nix-community", 30 + "repo": "nixpkgs.lib", 31 + "type": "github" 32 + } 33 + }, 34 + "parts": { 35 + "inputs": { 36 + "nixpkgs-lib": "nixpkgs-lib" 37 + }, 38 + "locked": { 39 + "lastModified": 1769996383, 40 + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", 41 + "owner": "hercules-ci", 42 + "repo": "flake-parts", 43 + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", 44 + "type": "github" 45 + }, 46 + "original": { 47 + "owner": "hercules-ci", 48 + "repo": "flake-parts", 49 + "type": "github" 50 + } 51 + }, 52 + "root": { 53 + "inputs": { 54 + "nixpkgs": "nixpkgs", 55 + "parts": "parts" 56 + } 57 + } 58 + }, 59 + "root": "root", 60 + "version": 7 61 + }
+33
flake.nix
··· 1 + { 2 + inputs.parts.url = "github:hercules-ci/flake-parts"; 3 + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 + 5 + outputs = 6 + inp: 7 + inp.parts.lib.mkFlake { inputs = inp; } { 8 + systems = [ "x86_64-linux" ]; 9 + perSystem = 10 + { 11 + pkgs, 12 + config, 13 + ... 14 + }: 15 + { 16 + packages.default = pkgs.callPackage ./default.nix {}; 17 + devShells = { 18 + default = pkgs.mkShell { 19 + packages = with pkgs; [ 20 + rustPlatform.rustLibSrc 21 + rust-analyzer 22 + cargo 23 + cargo-outdated 24 + rustc 25 + rustfmt 26 + openssl 27 + pkg-config 28 + ]; 29 + }; 30 + }; 31 + }; 32 + }; 33 + }
+11 -1
readme.md
··· 15 15 --wrap-pg "postgresql://user:pass@pg-host:5432/plc-db" 16 16 ``` 17 17 18 + - Run a fully self-contained mirror using an embedded fjall database (no postgres needed): 19 + 20 + ```bash 21 + # backfill first 22 + allegedly backfill --to-fjall ./plc-data 23 + 24 + # then run the mirror 25 + allegedly mirror --wrap-fjall ./plc-data 26 + ``` 27 + 18 28 - Wrap a plc server, maximalist edition: 19 29 20 30 ```bash ··· 89 99 - [ ] experimental: websocket version of /export 90 100 - [x] experimental: accept writes by forwarding them upstream 91 101 - [ ] experimental: serve a tlog 92 - - [ ] experimental: embed a log database directly for fast and efficient mirroring 102 + - [x] experimental: embed a log database directly for fast and efficient mirroring 93 103 - [ ] experimental: support multiple upstreams? 94 104 95 105 - [ ] new command todo: `zip` or `check` or `diff`: compare two plc logs over some time range
+49 -5
src/bin/backfill.rs
··· 1 1 use allegedly::{ 2 - Db, Dt, ExportPage, FolderSource, HttpSource, backfill, backfill_to_pg, 2 + Db, Dt, ExportPage, FjallDb, FolderSource, HttpSource, backfill, backfill_to_fjall, 3 + backfill_to_pg, 3 4 bin::{GlobalArgs, bin_init}, 4 - full_pages, logo, pages_to_pg, pages_to_stdout, poll_upstream, 5 + full_pages, logo, pages_to_fjall, pages_to_pg, pages_to_stdout, poll_upstream, 5 6 }; 6 7 use clap::Parser; 7 8 use reqwest::Url; ··· 22 23 /// Local folder to fetch bundles from (overrides `http`) 23 24 #[arg(long)] 24 25 dir: Option<PathBuf>, 26 + /// Local fjall database to fetch raw ops from (overrides `http` and `dir`) 27 + #[arg(long, conflicts_with_all = ["dir"])] 28 + from_fjall: Option<PathBuf>, 25 29 /// Don't do weekly bulk-loading at all. 26 30 /// 27 31 /// overrides `http` and `dir`, makes catch_up redundant ··· 45 49 /// only used if `--to-postgres` is present 46 50 #[arg(long, action)] 47 51 postgres_reset: bool, 52 + /// Bulk load into a local fjall embedded database 53 + /// 54 + /// Pass a directory path for the fjall database 55 + #[arg(long, conflicts_with_all = ["to_postgres", "postgres_cert", "postgres_reset"])] 56 + to_fjall: Option<PathBuf>, 57 + /// Delete all operations from the fjall db before starting 58 + /// 59 + /// only used if `--to-fjall` is present 60 + #[arg(long, action, requires = "to_fjall")] 61 + fjall_reset: bool, 48 62 /// Stop at the week ending before this date 49 63 #[arg(long)] 50 64 until: Option<Dt>, ··· 61 75 Args { 62 76 http, 63 77 dir, 78 + from_fjall, 64 79 no_bulk, 65 80 source_workers, 66 81 to_postgres, 67 82 postgres_cert, 68 83 postgres_reset, 84 + to_fjall, 85 + fjall_reset, 69 86 until, 70 87 catch_up, 71 88 }: Args, ··· 105 122 let throttle = Duration::from_millis(upstream_throttle_ms); 106 123 tasks.spawn(poll_upstream(None, upstream, throttle, poll_tx)); 107 124 tasks.spawn(full_pages(poll_out, full_tx)); 108 - tasks.spawn(pages_to_stdout(full_out, None)); 125 + if let Some(fjall_path) = to_fjall { 126 + log::trace!("opening fjall db at {fjall_path:?}..."); 127 + let db = FjallDb::open(&fjall_path)?; 128 + log::trace!("opened fjall db"); 129 + 130 + tasks.spawn(pages_to_fjall(db, full_out)); 131 + } else { 132 + tasks.spawn(pages_to_stdout(full_out, None)); 133 + } 109 134 } else { 110 135 // fun mode 111 136 112 137 // set up bulk sources 113 - if let Some(dir) = dir { 138 + if let Some(fjall_path) = from_fjall { 139 + log::trace!("opening source fjall db at {fjall_path:?}..."); 140 + let db = FjallDb::open(&fjall_path)?; 141 + log::trace!("opened source fjall db"); 142 + tasks.spawn(backfill(db, bulk_tx, source_workers.unwrap_or(4), until)); 143 + } else if let Some(dir) = dir { 114 144 if http != DEFAULT_HTTP.parse()? { 115 145 anyhow::bail!( 116 146 "non-default bulk http setting can't be used with bulk dir setting ({dir:?})" ··· 143 173 } 144 174 145 175 // set up sinks 146 - if let Some(pg_url) = to_postgres { 176 + if let Some(fjall_path) = to_fjall { 177 + log::trace!("opening fjall db at {fjall_path:?}..."); 178 + let db = FjallDb::open(&fjall_path)?; 179 + log::trace!("opened fjall db"); 180 + 181 + tasks.spawn(backfill_to_fjall( 182 + db.clone(), 183 + fjall_reset, 184 + bulk_out, 185 + found_last_tx, 186 + )); 187 + if catch_up { 188 + tasks.spawn(pages_to_fjall(db, full_out)); 189 + } 190 + } else if let Some(pg_url) = to_postgres { 147 191 log::trace!("connecting to postgres..."); 148 192 let db = Db::new(pg_url.as_str(), postgres_cert).await?; 149 193 log::trace!("connected to postgres");
+60 -25
src/bin/mirror.rs
··· 1 1 use allegedly::{ 2 - Db, ExperimentalConf, ListenConf, 2 + Db, ExperimentalConf, FjallDb, ListenConf, 3 3 bin::{GlobalArgs, InstrumentationArgs, bin_init}, 4 - logo, pages_to_pg, poll_upstream, serve, 4 + logo, pages_to_fjall, pages_to_pg, poll_upstream, serve, serve_fjall, 5 5 }; 6 6 use clap::Parser; 7 7 use reqwest::Url; ··· 10 10 11 11 #[derive(Debug, clap::Args)] 12 12 pub struct Args { 13 - /// the wrapped did-method-plc server 13 + /// the wrapped did-method-plc server (not needed when using --wrap-fjall) 14 14 #[arg(long, env = "ALLEGEDLY_WRAP")] 15 - wrap: Url, 15 + wrap: Option<Url>, 16 16 /// the wrapped did-method-plc server's database (write access required) 17 - #[arg(long, env = "ALLEGEDLY_WRAP_PG")] 17 + #[arg(long, env = "ALLEGEDLY_WRAP_PG", conflicts_with = "wrap_fjall")] 18 18 wrap_pg: Option<Url>, 19 19 /// path to tls cert for the wrapped postgres db, if needed 20 20 #[arg(long, env = "ALLEGEDLY_WRAP_PG_CERT")] 21 21 wrap_pg_cert: Option<PathBuf>, 22 + /// path to a local fjall database directory (alternative to postgres) 23 + #[arg(long, env = "ALLEGEDLY_WRAP_FJALL", conflicts_with_all = ["wrap_pg", "wrap_pg_cert"])] 24 + wrap_fjall: Option<PathBuf>, 25 + /// compact the fjall db on startup 26 + #[arg( 27 + long, 28 + env = "ALLEGEDLY_FJALL_COMPACT", 29 + conflicts_with_all = ["wrap_pg", "wrap_pg_cert"] 30 + )] 31 + compact_fjall: bool, 22 32 /// wrapping server listen address 23 33 #[arg(short, long, env = "ALLEGEDLY_BIND")] 24 34 #[clap(default_value = "127.0.0.1:8000")] ··· 70 80 wrap, 71 81 wrap_pg, 72 82 wrap_pg_cert, 83 + wrap_fjall, 84 + compact_fjall, 73 85 bind, 74 86 acme_domain, 75 87 acme_cache_path, ··· 106 118 107 119 let mut tasks = JoinSet::new(); 108 120 109 - let db = if sync { 110 - let wrap_pg = wrap_pg.ok_or(anyhow::anyhow!( 111 - "a wrapped reference postgres must be provided to sync" 112 - ))?; 113 - let db = Db::new(wrap_pg.as_str(), wrap_pg_cert).await?; 121 + if let Some(fjall_path) = wrap_fjall { 122 + let db = FjallDb::open(&fjall_path)?; 123 + if compact_fjall { 124 + log::info!("compacting fjall..."); 125 + db.compact()?; // blocking here is fine, we didn't start anything yet 126 + } 114 127 115 - // TODO: allow starting up with polling backfill from beginning? 116 - log::debug!("getting the latest op from the db..."); 128 + log::debug!("getting the latest op from fjall..."); 117 129 let latest = db 118 - .get_latest() 119 - .await? 130 + .get_latest()? 120 131 .expect("there to be at least one op in the db. did you backfill?"); 132 + log::info!("starting polling from {latest}..."); 121 133 122 134 let (send_page, recv_page) = mpsc::channel(8); 123 135 ··· 126 138 let throttle = Duration::from_millis(upstream_throttle_ms); 127 139 128 140 tasks.spawn(poll_upstream(Some(latest), poll_url, throttle, send_page)); 129 - tasks.spawn(pages_to_pg(db.clone(), recv_page)); 130 - Some(db) 141 + tasks.spawn(pages_to_fjall(db.clone(), recv_page)); 142 + 143 + tasks.spawn(serve_fjall(upstream, listen_conf, experimental_conf, db)); 131 144 } else { 132 - None 133 - }; 145 + let wrap = wrap.ok_or(anyhow::anyhow!( 146 + "--wrap is required unless using --wrap-fjall" 147 + ))?; 148 + 149 + let db: Option<Db> = if sync { 150 + let wrap_pg = wrap_pg.ok_or(anyhow::anyhow!( 151 + "a wrapped reference postgres (--wrap-pg) or fjall db (--wrap-fjall) must be provided to sync" 152 + ))?; 153 + let db = Db::new(wrap_pg.as_str(), wrap_pg_cert).await?; 134 154 135 - tasks.spawn(serve( 136 - upstream, 137 - wrap, 138 - listen_conf, 139 - experimental_conf, 140 - db.clone(), 141 - )); 155 + log::debug!("getting the latest op from the db..."); 156 + let latest = db 157 + .get_latest() 158 + .await? 159 + .expect("there to be at least one op in the db. did you backfill?"); 160 + log::debug!("starting polling from {latest}..."); 161 + 162 + let (send_page, recv_page) = mpsc::channel(8); 163 + 164 + let mut poll_url = upstream.clone(); 165 + poll_url.set_path("/export"); 166 + let throttle = Duration::from_millis(upstream_throttle_ms); 167 + 168 + tasks.spawn(poll_upstream(Some(latest), poll_url, throttle, send_page)); 169 + tasks.spawn(pages_to_pg(db.clone(), recv_page)); 170 + Some(db) 171 + } else { 172 + None 173 + }; 174 + 175 + tasks.spawn(serve(upstream, wrap, listen_conf, experimental_conf, db)); 176 + } 142 177 143 178 while let Some(next) = tasks.join_next().await { 144 179 match next {
+404
src/doc.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + use serde_json::Value; 3 + use std::borrow::Cow; 4 + use std::collections::BTreeMap; 5 + 6 + pub type CowStr<'a> = Cow<'a, str>; 7 + 8 + #[derive(Debug, Clone, Serialize, Deserialize)] 9 + pub struct Service<'a> { 10 + pub r#type: CowStr<'a>, 11 + pub endpoint: CowStr<'a>, 12 + } 13 + 14 + #[derive(Debug, Clone, Serialize, Deserialize)] 15 + #[serde(rename_all = "camelCase")] 16 + pub struct DocumentData<'a> { 17 + pub did: CowStr<'a>, 18 + pub rotation_keys: Vec<CowStr<'a>>, 19 + pub verification_methods: BTreeMap<CowStr<'a>, CowStr<'a>>, 20 + pub also_known_as: Vec<CowStr<'a>>, 21 + pub services: BTreeMap<CowStr<'a>, Service<'a>>, 22 + } 23 + 24 + #[derive(Debug, Clone, Serialize)] 25 + #[serde(rename_all = "camelCase")] 26 + pub struct DidDocument<'a> { 27 + #[serde(rename = "@context")] 28 + pub context: Vec<CowStr<'a>>, 29 + pub id: CowStr<'a>, 30 + pub also_known_as: Vec<CowStr<'a>>, 31 + pub verification_method: Vec<VerificationMethod<'a>>, 32 + pub service: Vec<DocService<'a>>, 33 + } 34 + 35 + #[derive(Debug, Clone, Serialize)] 36 + #[serde(rename_all = "camelCase")] 37 + pub struct VerificationMethod<'a> { 38 + pub id: CowStr<'a>, 39 + pub r#type: CowStr<'a>, 40 + pub controller: CowStr<'a>, 41 + pub public_key_multibase: CowStr<'a>, 42 + } 43 + 44 + #[derive(Debug, Clone, Serialize)] 45 + #[serde(rename_all = "camelCase")] 46 + pub struct DocService<'a> { 47 + pub id: CowStr<'a>, 48 + pub r#type: CowStr<'a>, 49 + pub service_endpoint: CowStr<'a>, 50 + } 51 + 52 + const P256_PREFIX: &str = "zDn"; 53 + const SECP256K1_PREFIX: &str = "zQ3"; 54 + 55 + fn key_context(multibase: &str) -> Option<&'static str> { 56 + if multibase.starts_with(P256_PREFIX) { 57 + Some("https://w3id.org/security/suites/ecdsa-2019/v1") 58 + } else if multibase.starts_with(SECP256K1_PREFIX) { 59 + Some("https://w3id.org/security/suites/secp256k1-2019/v1") 60 + } else { 61 + None 62 + } 63 + } 64 + 65 + pub fn format_did_doc<'a>(data: &'a DocumentData<'a>) -> DidDocument<'a> { 66 + let mut context = vec![ 67 + "https://www.w3.org/ns/did/v1".into(), 68 + "https://w3id.org/security/multikey/v1".into(), 69 + ]; 70 + 71 + let verification_method = data 72 + .verification_methods 73 + .iter() 74 + .map(|(keyid, did_key)| { 75 + let multibase: CowStr = did_key.strip_prefix("did:key:").unwrap_or(did_key).into(); 76 + 77 + if let Some(ctx) = key_context(&multibase) { 78 + if !context.iter().any(|c| c == ctx) { 79 + context.push(ctx.into()); 80 + } 81 + } 82 + VerificationMethod { 83 + id: format!("{}#{keyid}", data.did).into(), 84 + r#type: "Multikey".into(), 85 + controller: data.did.clone(), 86 + public_key_multibase: multibase, 87 + } 88 + }) 89 + .collect(); 90 + 91 + let service = data 92 + .services 93 + .iter() 94 + .map(|(service_id, svc)| DocService { 95 + id: format!("#{service_id}").into(), 96 + r#type: svc.r#type.clone(), 97 + service_endpoint: svc.endpoint.clone(), 98 + }) 99 + .collect(); 100 + 101 + DidDocument { 102 + context, 103 + id: data.did.clone(), 104 + also_known_as: data.also_known_as.clone(), 105 + verification_method, 106 + service, 107 + } 108 + } 109 + 110 + fn ensure_atproto_prefix(s: &str) -> CowStr<'_> { 111 + if s.starts_with("at://") { 112 + return s.into(); 113 + } 114 + let stripped = s 115 + .strip_prefix("http://") 116 + .or_else(|| s.strip_prefix("https://")) 117 + .unwrap_or(s); 118 + format!("at://{stripped}").into() 119 + } 120 + 121 + fn ensure_http_prefix(s: &str) -> CowStr<'_> { 122 + if s.starts_with("http://") || s.starts_with("https://") { 123 + return s.into(); 124 + } 125 + format!("https://{s}").into() 126 + } 127 + 128 + /// extract DocumentData from a single operation json blob. 129 + /// returns None for tombstones. 130 + pub fn op_to_doc_data<'a>(did: &'a str, op: &'a Value) -> Option<DocumentData<'a>> { 131 + // TODO: this shouldnt just short circuit to None, we should provide better information about whats missing in an error 132 + let obj = op.as_object()?; 133 + let op_type = obj.get("type")?.as_str()?; 134 + 135 + match op_type { 136 + "plc_tombstone" => None, 137 + "create" => { 138 + let signing_key = obj.get("signingKey")?.as_str()?; 139 + let recovery_key = obj.get("recoveryKey")?.as_str()?; 140 + let handle = obj.get("handle")?.as_str()?; 141 + let service = obj.get("service")?.as_str()?; 142 + 143 + let mut verification_methods = BTreeMap::new(); 144 + verification_methods.insert("atproto".into(), signing_key.into()); 145 + 146 + let mut services = BTreeMap::new(); 147 + services.insert( 148 + "atproto_pds".into(), 149 + Service { 150 + r#type: "AtprotoPersonalDataServer".into(), 151 + endpoint: ensure_http_prefix(service), 152 + }, 153 + ); 154 + 155 + Some(DocumentData { 156 + did: Cow::Borrowed(did), 157 + rotation_keys: vec![Cow::Borrowed(recovery_key), Cow::Borrowed(signing_key)], 158 + verification_methods, 159 + also_known_as: vec![ensure_atproto_prefix(handle)], 160 + services, 161 + }) 162 + } 163 + "plc_operation" => { 164 + let rotation_keys = obj 165 + .get("rotationKeys")? 166 + .as_array()? 167 + .iter() 168 + .filter_map(|v| v.as_str().map(Cow::Borrowed)) 169 + .collect(); 170 + 171 + let verification_methods = obj 172 + .get("verificationMethods")? 173 + .as_object()? 174 + .iter() 175 + .filter_map(|(k, v)| Some((k.as_str().into(), v.as_str()?.into()))) 176 + .collect(); 177 + 178 + let also_known_as = obj 179 + .get("alsoKnownAs")? 180 + .as_array()? 181 + .iter() 182 + .filter_map(|v| v.as_str().map(Cow::Borrowed)) 183 + .collect(); 184 + 185 + let services = obj 186 + .get("services")? 187 + .as_object()? 188 + .iter() 189 + .filter_map(|(k, v)| { 190 + let svc: Service = Service::deserialize(v).ok()?; 191 + Some((k.as_str().into(), svc)) 192 + }) 193 + .collect(); 194 + 195 + Some(DocumentData { 196 + did: did.into(), 197 + rotation_keys, 198 + verification_methods, 199 + also_known_as, 200 + services, 201 + }) 202 + } 203 + _ => None, 204 + } 205 + } 206 + 207 + /// apply a sequence of operation JSON blobs and return the current document data. 208 + /// returns None if the DID is tombstoned (last op is a tombstone). 209 + pub fn apply_op_log<'a>( 210 + did: &'a str, 211 + ops: impl IntoIterator<Item = &'a Value>, 212 + ) -> Option<DocumentData<'a>> { 213 + // TODO: we don't verify signature chain, we should do that... 214 + ops.into_iter() 215 + .last() 216 + .and_then(|op| op_to_doc_data(did, op)) 217 + } 218 + 219 + #[cfg(test)] 220 + mod tests { 221 + use super::*; 222 + 223 + #[test] 224 + fn normalize_legacy_create() { 225 + let op = serde_json::json!({ 226 + "type": "create", 227 + "signingKey": "did:key:zDnaeSigningKey", 228 + "recoveryKey": "did:key:zQ3shRecoveryKey", 229 + "handle": "alice.bsky.social", 230 + "service": "pds.example.com", 231 + "prev": null, 232 + "sig": "abc" 233 + }); 234 + 235 + let data = op_to_doc_data("did:plc:test", &op).unwrap(); 236 + assert_eq!(data.rotation_keys.len(), 2); 237 + assert_eq!(data.rotation_keys[0], "did:key:zQ3shRecoveryKey"); 238 + assert_eq!(data.rotation_keys[1], "did:key:zDnaeSigningKey"); 239 + assert_eq!( 240 + data.verification_methods.get("atproto").unwrap(), 241 + "did:key:zDnaeSigningKey" 242 + ); 243 + assert_eq!(data.also_known_as, vec!["at://alice.bsky.social"]); 244 + let pds = data.services.get("atproto_pds").unwrap(); 245 + assert_eq!(pds.endpoint, "https://pds.example.com"); 246 + } 247 + 248 + #[test] 249 + fn format_doc_p256_context() { 250 + let data = DocumentData { 251 + did: "did:plc:test123".into(), 252 + rotation_keys: vec!["did:key:zDnaeXYZ".into()], 253 + verification_methods: { 254 + let mut m = BTreeMap::new(); 255 + m.insert("atproto".into(), "did:key:zDnaeXYZ".into()); 256 + m 257 + }, 258 + also_known_as: vec!["at://alice.test".into()], 259 + services: { 260 + let mut m = BTreeMap::new(); 261 + m.insert( 262 + "atproto_pds".into(), 263 + Service { 264 + r#type: "AtprotoPersonalDataServer".into(), 265 + endpoint: "https://pds.test".into(), 266 + }, 267 + ); 268 + m 269 + }, 270 + }; 271 + 272 + let doc = format_did_doc(&data); 273 + assert_eq!(doc.context.len(), 3); 274 + assert!( 275 + doc.context 276 + .iter() 277 + .any(|c| c == "https://w3id.org/security/suites/ecdsa-2019/v1") 278 + ); 279 + assert_eq!(doc.verification_method[0].public_key_multibase, "zDnaeXYZ"); 280 + assert_eq!(doc.verification_method[0].id, "did:plc:test123#atproto"); 281 + } 282 + 283 + #[test] 284 + fn tombstone_returns_none() { 285 + let op = serde_json::json!({ 286 + "type": "plc_tombstone", 287 + "prev": "bafyabc", 288 + "sig": "xyz" 289 + }); 290 + assert!(op_to_doc_data("did:plc:test", &op).is_none()); 291 + } 292 + 293 + #[test] 294 + fn apply_log_with_tombstone() { 295 + let create = serde_json::json!({ 296 + "type": "plc_operation", 297 + "rotationKeys": ["did:key:zQ3shKey1"], 298 + "verificationMethods": {"atproto": "did:key:zDnaeKey1"}, 299 + "alsoKnownAs": ["at://alice.test"], 300 + "services": { 301 + "atproto_pds": {"type": "AtprotoPersonalDataServer", "service_endpoint": "https://pds.test"} 302 + }, 303 + "prev": null, 304 + "sig": "abc" 305 + }); 306 + let tombstone = serde_json::json!({ 307 + "type": "plc_tombstone", 308 + "prev": "bafyabc", 309 + "sig": "xyz" 310 + }); 311 + 312 + let ops = vec![create.clone()]; 313 + let result = apply_op_log("did:plc:test", &ops); 314 + assert!(result.is_some()); 315 + 316 + let ops = vec![create, tombstone]; 317 + let result = apply_op_log("did:plc:test", &ops); 318 + assert!(result.is_none()); 319 + } 320 + 321 + fn load_fixture(name: &str) -> (String, Vec<Value>) { 322 + let path = format!("tests/fixtures/{name}"); 323 + let data = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}")); 324 + let entries: Vec<Value> = serde_json::from_str(&data).unwrap(); 325 + let did = entries[0]["did"].as_str().unwrap().to_string(); 326 + let ops: Vec<Value> = entries 327 + .iter() 328 + .filter(|e| !e["nullified"].as_bool().unwrap_or(false)) 329 + .map(|e| e["operation"].clone()) 330 + .collect(); 331 + (did, ops) 332 + } 333 + 334 + #[test] 335 + fn interop_legacy_dholms() { 336 + let (did, ops) = load_fixture("log_legacy_dholms.json"); 337 + assert_eq!(did, "did:plc:yk4dd2qkboz2yv6tpubpc6co"); 338 + 339 + let data = apply_op_log(&did, &ops).expect("should reconstruct"); 340 + assert_eq!(data.did, did); 341 + assert_eq!(data.also_known_as, vec!["at://dholms.xyz"]); 342 + assert_eq!( 343 + data.services.get("atproto_pds").unwrap().endpoint, 344 + "https://bsky.social" 345 + ); 346 + assert_eq!( 347 + data.verification_methods.get("atproto").unwrap(), 348 + "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 349 + ); 350 + 351 + let doc = format_did_doc(&data); 352 + assert_eq!(doc.id, did); 353 + assert!( 354 + doc.context 355 + .iter() 356 + .any(|c| c == "https://w3id.org/security/suites/secp256k1-2019/v1") 357 + ); 358 + } 359 + 360 + #[test] 361 + fn interop_bskyapp() { 362 + let (did, ops) = load_fixture("log_bskyapp.json"); 363 + assert_eq!(did, "did:plc:z72i7hdynmk6r22z27h6tvur"); 364 + 365 + let data = apply_op_log(&did, &ops).expect("should reconstruct"); 366 + println!("{:?}", data); 367 + assert_eq!(data.also_known_as, vec!["at://bsky.app"]); 368 + assert_eq!( 369 + data.verification_methods.get("atproto").unwrap(), 370 + "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 371 + ); 372 + assert_eq!( 373 + data.services.get("atproto_pds").unwrap().endpoint, 374 + "https://bsky.social" 375 + ); 376 + } 377 + 378 + #[test] 379 + fn interop_tombstone() { 380 + let path = "tests/fixtures/log_tombstone.json"; 381 + let data = std::fs::read_to_string(path).unwrap(); 382 + let entries: Vec<Value> = serde_json::from_str(&data).unwrap(); 383 + let did = entries[0]["did"].as_str().unwrap(); 384 + let ops: Vec<Value> = entries.iter().map(|e| e["operation"].clone()).collect(); 385 + 386 + assert_eq!(did, "did:plc:6adr3q2labdllanslzhqkqd3"); 387 + let result = apply_op_log(did, &ops); 388 + assert!(result.is_none(), "tombstoned DID should return None"); 389 + } 390 + 391 + #[test] 392 + fn interop_nullification() { 393 + let (did, ops) = load_fixture("log_nullification.json"); 394 + assert_eq!(did, "did:plc:2s2mvm52ttz6r4hocmrq7x27"); 395 + 396 + let data = apply_op_log(&did, &ops).expect("should reconstruct"); 397 + assert_eq!(data.did, did); 398 + assert_eq!(data.rotation_keys.len(), 2); 399 + assert_eq!( 400 + data.rotation_keys[0], 401 + "did:key:zQ3shwPdax6jKMbhtzbueGwSjc7RnjsmPcNB1vQUpbKUCN1t1" 402 + ); 403 + } 404 + }
+5 -1
src/lib.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 + 2 3 use tokio::sync::{mpsc, oneshot}; 3 4 4 5 mod backfill; 5 6 mod cached_value; 6 7 mod client; 8 + pub mod doc; 7 9 mod mirror; 10 + mod plc_fjall; 8 11 mod plc_pg; 9 12 mod poll; 10 13 mod ratelimit; ··· 15 18 pub use backfill::backfill; 16 19 pub use cached_value::{CachedValue, Fetcher}; 17 20 pub use client::{CLIENT, UA}; 18 - pub use mirror::{ExperimentalConf, ListenConf, serve}; 21 + pub use mirror::{ExperimentalConf, ListenConf, serve, serve_fjall}; 22 + pub use plc_fjall::{FjallDb, backfill_to_fjall, pages_to_fjall}; 19 23 pub use plc_pg::{Db, backfill_to_pg, pages_to_pg}; 20 24 pub use poll::{PageBoundaryState, get_page, poll_upstream}; 21 25 pub use ratelimit::{CreatePlcOpLimiter, GovernorMiddleware, IpLimiters};
+434
src/mirror/fjall.rs
··· 1 + use super::*; 2 + use futures::StreamExt; 3 + use poem::web::Query; 4 + use serde::Deserialize; 5 + 6 + #[derive(Clone)] 7 + struct FjallState { 8 + client: Client, 9 + upstream: Url, 10 + fjall: FjallDb, 11 + sync_info: FjallSyncInfo, 12 + experimental: ExperimentalConf, 13 + } 14 + 15 + #[derive(Clone)] 16 + struct FjallSyncInfo { 17 + latest_at: CachedValue<Dt, GetFjallLatestAt>, 18 + upstream_status: CachedValue<PlcStatus, CheckUpstream>, 19 + } 20 + 21 + #[derive(Clone)] 22 + struct GetFjallLatestAt(FjallDb); 23 + impl Fetcher<Dt> for GetFjallLatestAt { 24 + async fn fetch(&self) -> Result<Dt, Box<dyn std::error::Error>> { 25 + let db = self.0.clone(); 26 + let now = tokio::task::spawn_blocking(move || db.get_latest()) 27 + .await?? 28 + .ok_or(anyhow::anyhow!( 29 + "expected to find at least one thing in the db" 30 + ))?; 31 + Ok(now) 32 + } 33 + } 34 + 35 + #[derive(Clone)] 36 + struct CheckUpstream(Url, Client); 37 + impl Fetcher<PlcStatus> for CheckUpstream { 38 + async fn fetch(&self) -> Result<PlcStatus, Box<dyn std::error::Error>> { 39 + Ok(plc_status(&self.0, &self.1).await) 40 + } 41 + } 42 + 43 + #[handler] 44 + fn fjall_hello( 45 + Data(FjallState { 46 + upstream, 47 + experimental: exp, 48 + .. 49 + }): Data<&FjallState>, 50 + req: &Request, 51 + ) -> String { 52 + let post_info = match (exp.write_upstream, &exp.acme_domain, req.uri().host()) { 53 + (false, _, _) => " - POST /* Always rejected. This is a mirror.".to_string(), 54 + (_, None, _) => { 55 + " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 56 + } 57 + (_, Some(d), Some(f)) if f == d => { 58 + " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 59 + } 60 + (_, Some(d), _) => format!( 61 + r#" - POST /* Rejected, but experimental upstream op forwarding is 62 + available at `POST https://{d}/:did`!"# 63 + ), 64 + }; 65 + 66 + format!( 67 + r#"{} 68 + 69 + This is a PLC[1] mirror running Allegedly in fjall mirror mode. The PLC API 70 + is served from a the local database, which is mirrored from the upstream PLC 71 + server. 72 + 73 + 74 + Configured upstream: 75 + 76 + {upstream} 77 + 78 + 79 + Available APIs: 80 + 81 + - GET /_health Health and version info 82 + 83 + - GET /did:plc:{{did}} Resolve a DID document 84 + - GET /did:plc:{{did}}/log Operation log 85 + - GET /did:plc:{{did}}/log/audit Full audit log (including nullified ops) 86 + - GET /did:plc:{{did}}/log/last Last operation 87 + - GET /did:plc:{{did}}/data Parsed document data 88 + 89 + {post_info} 90 + 91 + 92 + Allegedly is a suite of open-source CLI tools for working with PLC logs, 93 + from microcosm: 94 + 95 + https://tangled.org/@microcosm.blue/Allegedly 96 + 97 + https://microcosm.blue 98 + 99 + 100 + [1] https://web.plc.directory 101 + [2] https://github.com/did-method-plc/did-method-plc 102 + "#, 103 + logo("mirror (fjall)") 104 + ) 105 + } 106 + 107 + #[handler] 108 + async fn fjall_health(Data(FjallState { sync_info, .. }): Data<&FjallState>) -> impl IntoResponse { 109 + let mut overall_status = StatusCode::OK; 110 + 111 + let (ok, upstream_status) = sync_info 112 + .upstream_status 113 + .get() 114 + .await 115 + .expect("plc_status infallible"); 116 + if !ok { 117 + overall_status = StatusCode::BAD_GATEWAY; 118 + } 119 + let latest = sync_info.latest_at.get().await.ok(); 120 + 121 + ( 122 + overall_status, 123 + Json(serde_json::json!({ 124 + "server": "allegedly (mirror/fjall)", 125 + "version": env!("CARGO_PKG_VERSION"), 126 + "upstream_plc": upstream_status, 127 + "latest_at": latest, 128 + })), 129 + ) 130 + } 131 + 132 + #[handler] 133 + async fn fjall_resolve(req: &Request, Data(state): Data<&FjallState>) -> Result<Response> { 134 + let path = req.uri().path(); 135 + let did_and_rest = path.strip_prefix("/").unwrap_or(path); 136 + 137 + let (did, sub_path) = match did_and_rest.find('/') { 138 + Some(i) => (&did_and_rest[..i], &did_and_rest[i..]), 139 + None => (did_and_rest, ""), 140 + }; 141 + 142 + if !did.starts_with("did:plc:") { 143 + return Err(Error::from_string("invalid DID", StatusCode::BAD_REQUEST)); 144 + } 145 + 146 + let did = did.to_string(); 147 + let db = state.fjall.clone(); 148 + let ops = tokio::task::spawn_blocking(move || { 149 + let iter = db.ops_for_did(&did)?; 150 + iter.collect::<anyhow::Result<Vec<_>>>() 151 + }) 152 + .await 153 + .map_err(|e| Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))? 154 + .map_err(|e| Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; 155 + 156 + if ops.is_empty() { 157 + return Err(Error::from_string( 158 + format!( 159 + "DID not registered: {}", 160 + did_and_rest.split('/').next().unwrap_or(did_and_rest) 161 + ), 162 + StatusCode::NOT_FOUND, 163 + )); 164 + } 165 + 166 + let did_str = &ops[0].did; 167 + 168 + match sub_path { 169 + "" => { 170 + let data = doc::apply_op_log( 171 + did_str, 172 + ops.iter() 173 + .filter(|op| !op.nullified) 174 + .map(|op| &op.operation), 175 + ); 176 + let Some(data) = data else { 177 + return Err(Error::from_string( 178 + format!("DID not available: {did_str}"), 179 + StatusCode::NOT_FOUND, 180 + )); 181 + }; 182 + let doc = doc::format_did_doc(&data); 183 + Ok(Response::builder() 184 + .content_type("application/did+ld+json") 185 + .body(serde_json::to_string(&doc).unwrap())) 186 + } 187 + "/log" => { 188 + let log: Vec<&serde_json::Value> = ops 189 + .iter() 190 + .filter(|op| !op.nullified) 191 + .map(|op| &op.operation) 192 + .collect(); 193 + Ok(Response::builder() 194 + .content_type("application/json") 195 + .body(serde_json::to_string(&log).unwrap())) 196 + } 197 + "/log/audit" => { 198 + let audit: Vec<serde_json::Value> = ops 199 + .iter() 200 + .map(|op| { 201 + serde_json::json!({ 202 + "did": op.did, 203 + "operation": op.operation, 204 + "cid": op.cid, 205 + "nullified": op.nullified, 206 + "createdAt": op.created_at.to_rfc3339(), 207 + }) 208 + }) 209 + .collect(); 210 + Ok(Response::builder() 211 + .content_type("application/json") 212 + .body(serde_json::to_string(&audit).unwrap())) 213 + } 214 + "/log/last" => { 215 + let last = ops 216 + .iter() 217 + .filter(|op| !op.nullified) 218 + .last() 219 + .map(|op| &op.operation); 220 + let Some(last) = last else { 221 + return Err(Error::from_string( 222 + format!("DID not available: {did_str}"), 223 + StatusCode::NOT_FOUND, 224 + )); 225 + }; 226 + Ok(Response::builder() 227 + .content_type("application/json") 228 + .body(serde_json::to_string(&last).unwrap())) 229 + } 230 + "/data" => { 231 + let data = doc::apply_op_log( 232 + did_str, 233 + ops.iter() 234 + .filter(|op| !op.nullified) 235 + .map(|op| &op.operation), 236 + ); 237 + let Some(data) = data else { 238 + return Err(Error::from_string( 239 + format!("DID not available: {did_str}"), 240 + StatusCode::NOT_FOUND, 241 + )); 242 + }; 243 + Ok(Response::builder() 244 + .content_type("application/json") 245 + .body(serde_json::to_string(&data).unwrap())) 246 + } 247 + _ => Err(Error::from_string("not found", StatusCode::NOT_FOUND)), 248 + } 249 + } 250 + 251 + #[derive(Deserialize)] 252 + struct ExportQuery { 253 + after: Option<String>, 254 + #[allow(dead_code)] // we just cap at 1000 for now, matching reference impl 255 + count: Option<usize>, 256 + } 257 + 258 + #[handler] 259 + async fn fjall_export( 260 + _req: &Request, 261 + Query(query): Query<ExportQuery>, 262 + Data(FjallState { fjall, .. }): Data<&FjallState>, 263 + ) -> Result<Body> { 264 + let after = if let Some(a) = query.after { 265 + Some( 266 + chrono::DateTime::parse_from_rfc3339(&a) 267 + .map_err(|e| Error::from_string(e.to_string(), StatusCode::BAD_REQUEST))? 268 + .with_timezone(&chrono::Utc), 269 + ) 270 + } else { 271 + None 272 + }; 273 + 274 + let limit = 1000; 275 + let db = fjall.clone(); 276 + 277 + let ops = tokio::task::spawn_blocking(move || { 278 + let iter = db.export_ops(after.unwrap_or(Dt::UNIX_EPOCH)..)?; 279 + iter.take(limit).collect::<anyhow::Result<Vec<_>>>() 280 + }) 281 + .await 282 + .map_err(|e| Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))? 283 + .map_err(|e| Error::from_string(e.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; 284 + 285 + let stream = futures::stream::iter(ops).map(|op| { 286 + let mut json = serde_json::to_string(&op).unwrap(); 287 + json.push('\n'); 288 + Ok::<_, std::io::Error>(json) 289 + }); 290 + 291 + Ok(Body::from_bytes_stream(stream)) 292 + } 293 + 294 + #[handler] 295 + async fn fjall_nope(Data(FjallState { upstream, .. }): Data<&FjallState>) -> (StatusCode, String) { 296 + ( 297 + StatusCode::BAD_REQUEST, 298 + format!( 299 + r#"{} 300 + 301 + Sorry, this server does not accept POST requests. 302 + 303 + You may wish to try sending that to our upstream: {upstream}. 304 + 305 + If you operate this server, try running with `--experimental-write-upstream`. 306 + "#, 307 + logo("mirror (nope)") 308 + ), 309 + ) 310 + } 311 + 312 + pub async fn serve_fjall( 313 + upstream: Url, 314 + listen: ListenConf, 315 + experimental: ExperimentalConf, 316 + fjall: FjallDb, 317 + ) -> anyhow::Result<&'static str> { 318 + log::info!("starting fjall mirror server..."); 319 + 320 + let client = Client::builder() 321 + .user_agent(UA) 322 + .timeout(Duration::from_secs(10)) 323 + .build() 324 + .expect("reqwest client to build"); 325 + 326 + let sync_info = FjallSyncInfo { 327 + latest_at: CachedValue::new(GetFjallLatestAt(fjall.clone()), Duration::from_secs(2)), 328 + upstream_status: CachedValue::new( 329 + CheckUpstream(upstream.clone(), client.clone()), 330 + Duration::from_secs(6), 331 + ), 332 + }; 333 + 334 + let state = FjallState { 335 + client, 336 + upstream, 337 + fjall, 338 + sync_info, 339 + experimental: experimental.clone(), 340 + }; 341 + 342 + let mut app = Route::new() 343 + .at("/", get(fjall_hello)) 344 + .at("/favicon.ico", get(favicon)) 345 + .at("/_health", get(fjall_health)) 346 + .at("/export", get(fjall_export)); 347 + 348 + if experimental.write_upstream { 349 + log::info!("enabling experimental write forwarding to upstream"); 350 + 351 + let ip_limiter = IpLimiters::new(Quota::per_hour(10.try_into().unwrap())); 352 + let did_limiter = CreatePlcOpLimiter::new(Quota::per_hour(4.try_into().unwrap())); 353 + 354 + let upstream_proxier = fjall_forward_create_op_upstream 355 + .with(GovernorMiddleware::new(did_limiter)) 356 + .with(GovernorMiddleware::new(ip_limiter)); 357 + 358 + app = app.at("/did:plc:*", get(fjall_resolve).post(upstream_proxier)); 359 + } else { 360 + app = app.at("/did:plc:*", get(fjall_resolve).post(fjall_nope)); 361 + } 362 + 363 + let app = app 364 + .with(AddData::new(state)) 365 + .with(Cors::new().allow_credentials(false)) 366 + .with(Compression::new()) 367 + .with(GovernorMiddleware::new(IpLimiters::new(Quota::per_minute( 368 + 3000.try_into().expect("ratelimit middleware to build"), 369 + )))) 370 + .with(CatchPanic::new()) 371 + .with(Tracing); 372 + 373 + bind_or_acme(app, listen).await 374 + } 375 + 376 + #[handler] 377 + async fn fjall_forward_create_op_upstream( 378 + Data(FjallState { 379 + upstream, 380 + client, 381 + experimental, 382 + .. 383 + }): Data<&FjallState>, 384 + Path(did): Path<String>, 385 + req: &Request, 386 + body: Body, 387 + ) -> Result<Response> { 388 + if let Some(expected_domain) = &experimental.acme_domain { 389 + let Some(found_host) = req.uri().host() else { 390 + return Ok(bad_create_op(&format!( 391 + "missing `Host` header, expected {expected_domain:?} for experimental requests." 392 + ))); 393 + }; 394 + if found_host != expected_domain { 395 + return Ok(bad_create_op(&format!( 396 + "experimental requests must be made to {expected_domain:?}, but this request's `Host` header was {found_host}" 397 + ))); 398 + } 399 + } 400 + 401 + let mut headers: reqwest::header::HeaderMap = req.headers().clone(); 402 + log::trace!("original request headers: {headers:?}"); 403 + headers.insert("Host", upstream.host_str().unwrap().parse().unwrap()); 404 + let client_ua = headers 405 + .get(USER_AGENT) 406 + .map(|h| h.to_str().unwrap()) 407 + .unwrap_or("unknown"); 408 + headers.insert( 409 + USER_AGENT, 410 + format!("{UA} (forwarding from {client_ua:?})") 411 + .parse() 412 + .unwrap(), 413 + ); 414 + log::trace!("adjusted request headers: {headers:?}"); 415 + 416 + let mut target = upstream.clone(); 417 + target.set_path(&did); 418 + let upstream_res = client 419 + .post(target) 420 + .timeout(Duration::from_secs(15)) 421 + .headers(headers) 422 + .body(reqwest::Body::wrap_stream(body.into_bytes_stream())) 423 + .send() 424 + .await 425 + .map_err(|e| { 426 + log::warn!("upstream write fail: {e}"); 427 + Error::from_string( 428 + failed_to_reach_named("upstream PLC"), 429 + StatusCode::BAD_GATEWAY, 430 + ) 431 + })?; 432 + 433 + Ok(proxy_response(upstream_res)) 434 + }
+201
src/mirror/mod.rs
··· 1 + use crate::{ 2 + CachedValue, CreatePlcOpLimiter, Db, Dt, Fetcher, FjallDb, GovernorMiddleware, IpLimiters, UA, 3 + doc, logo, 4 + }; 5 + use futures::TryStreamExt; 6 + use governor::Quota; 7 + use poem::{ 8 + Body, Endpoint, EndpointExt, Error, IntoResponse, Request, Response, Result, Route, Server, 9 + get, handler, 10 + http::{StatusCode, header::USER_AGENT}, 11 + listener::{Listener, TcpListener, acme::AutoCert}, 12 + middleware::{AddData, CatchPanic, Compression, Cors, Tracing}, 13 + web::{Data, Json, Path}, 14 + }; 15 + use reqwest::{Client, Url}; 16 + use std::{net::SocketAddr, path::PathBuf, time::Duration}; 17 + 18 + pub mod fjall; 19 + pub mod pg; 20 + 21 + pub use fjall::serve_fjall; 22 + pub use pg::serve; 23 + 24 + #[derive(Debug)] 25 + pub enum ListenConf { 26 + Acme { 27 + domains: Vec<String>, 28 + cache_path: PathBuf, 29 + directory_url: String, 30 + ipv6: bool, 31 + }, 32 + Bind(SocketAddr), 33 + } 34 + 35 + #[derive(Debug, Clone)] 36 + pub struct ExperimentalConf { 37 + pub acme_domain: Option<String>, 38 + pub write_upstream: bool, 39 + } 40 + 41 + #[handler] 42 + pub fn favicon() -> impl IntoResponse { 43 + include_bytes!("../../favicon.ico").with_content_type("image/x-icon") 44 + } 45 + 46 + pub fn failed_to_reach_named(name: &str) -> String { 47 + format!( 48 + r#"{} 49 + 50 + Failed to reach the {name} server. Sorry. 51 + "#, 52 + logo("mirror 502 :( ") 53 + ) 54 + } 55 + 56 + pub fn bad_create_op(reason: &str) -> Response { 57 + Response::builder() 58 + .status(StatusCode::BAD_REQUEST) 59 + .body(format!( 60 + r#"{} 61 + 62 + NooOOOooooo: {reason} 63 + "#, 64 + logo("mirror 400 >:( ") 65 + )) 66 + } 67 + 68 + pub type PlcStatus = (bool, serde_json::Value); 69 + 70 + pub async fn plc_status(url: &Url, client: &Client) -> PlcStatus { 71 + use serde_json::json; 72 + 73 + let mut url = url.clone(); 74 + url.set_path("/_health"); 75 + 76 + let Ok(response) = client.get(url).timeout(Duration::from_secs(3)).send().await else { 77 + return (false, json!({"error": "cannot reach plc server"})); 78 + }; 79 + 80 + let status = response.status(); 81 + 82 + let Ok(text) = response.text().await else { 83 + return (false, json!({"error": "failed to read response body"})); 84 + }; 85 + 86 + let body = match serde_json::from_str(&text) { 87 + Ok(json) => json, 88 + Err(_) => serde_json::Value::String(text.to_string()), 89 + }; 90 + 91 + if status.is_success() { 92 + (true, body) 93 + } else { 94 + ( 95 + false, 96 + json!({ 97 + "error": "non-ok status", 98 + "status": status.as_str(), 99 + "status_code": status.as_u16(), 100 + "response": body, 101 + }), 102 + ) 103 + } 104 + } 105 + 106 + pub fn proxy_response(res: reqwest::Response) -> Response { 107 + let http_res: poem::http::Response<reqwest::Body> = res.into(); 108 + let (parts, reqw_body) = http_res.into_parts(); 109 + 110 + let parts = poem::ResponseParts { 111 + status: parts.status, 112 + version: parts.version, 113 + headers: parts.headers, 114 + extensions: parts.extensions, 115 + }; 116 + 117 + let body = http_body_util::BodyDataStream::new(reqw_body) 118 + .map_err(|e| std::io::Error::other(Box::new(e))); 119 + 120 + Response::from_parts(parts, poem::Body::from_bytes_stream(body)) 121 + } 122 + 123 + async fn run<A, L>(app: A, listener: L) -> std::io::Result<()> 124 + where 125 + A: Endpoint + 'static, 126 + L: Listener + 'static, 127 + { 128 + Server::new(listener) 129 + .name("allegedly (mirror)") 130 + .run(app) 131 + .await 132 + } 133 + 134 + /// kick off a tiny little server on a tokio task to tell people to use 443 135 + async fn run_insecure_notice(ipv6: bool) -> Result<(), std::io::Error> { 136 + #[handler] 137 + fn oop_plz_be_secure() -> (StatusCode, String) { 138 + ( 139 + StatusCode::BAD_REQUEST, 140 + format!( 141 + r#"{} 142 + 143 + You probably want to change your request to use HTTPS instead of HTTP. 144 + "#, 145 + logo("mirror (tls on 443 please)") 146 + ), 147 + ) 148 + } 149 + 150 + let app = Route::new() 151 + .at("/favicon.ico", get(favicon)) 152 + .nest("/", get(oop_plz_be_secure)) 153 + .with(Tracing); 154 + Server::new(TcpListener::bind(if ipv6 { 155 + "[::]:80" 156 + } else { 157 + "0.0.0.0:80" 158 + })) 159 + .name("allegedly (mirror:80 helper)") 160 + .run(app) 161 + .await 162 + } 163 + 164 + pub async fn bind_or_acme<A>(app: A, listen: ListenConf) -> anyhow::Result<&'static str> 165 + where 166 + A: Endpoint + 'static, 167 + { 168 + match listen { 169 + ListenConf::Acme { 170 + domains, 171 + cache_path, 172 + directory_url, 173 + ipv6, 174 + } => { 175 + rustls::crypto::aws_lc_rs::default_provider() 176 + .install_default() 177 + .expect("crypto provider to be installable"); 178 + 179 + let mut auto_cert = AutoCert::builder() 180 + .directory_url(directory_url) 181 + .cache_path(cache_path); 182 + for domain in domains { 183 + auto_cert = auto_cert.domain(domain); 184 + } 185 + let auto_cert = auto_cert.build().expect("acme config to build"); 186 + 187 + log::trace!("auto_cert: {auto_cert:?}"); 188 + 189 + let notice_task = tokio::task::spawn(run_insecure_notice(ipv6)); 190 + let listener = TcpListener::bind(if ipv6 { "[::]:443" } else { "0.0.0.0:443" }); 191 + let app_res = run(app, listener.acme(auto_cert)).await; 192 + log::warn!("server task ended, aborting insecure server task..."); 193 + notice_task.abort(); 194 + app_res?; 195 + notice_task.await??; 196 + } 197 + ListenConf::Bind(addr) => run(app, TcpListener::bind(addr)).await?, 198 + } 199 + 200 + Ok("server (uh oh?)") 201 + }
+334
src/mirror/pg.rs
··· 1 + use super::*; 2 + 3 + #[derive(Clone)] 4 + struct State { 5 + client: Client, 6 + plc: Url, 7 + upstream: Url, 8 + sync_info: Option<SyncInfo>, 9 + experimental: ExperimentalConf, 10 + } 11 + 12 + /// server info that only applies in mirror (synchronizing) mode 13 + #[derive(Clone)] 14 + struct SyncInfo { 15 + latest_at: CachedValue<Dt, GetLatestAt>, 16 + upstream_status: CachedValue<PlcStatus, CheckUpstream>, 17 + } 18 + 19 + #[derive(Clone)] 20 + struct GetLatestAt(Db); 21 + impl Fetcher<Dt> for GetLatestAt { 22 + async fn fetch(&self) -> Result<Dt, Box<dyn std::error::Error>> { 23 + let now = self.0.get_latest().await?.ok_or(anyhow::anyhow!( 24 + "expected to find at least one thing in the db" 25 + ))?; 26 + Ok(now) 27 + } 28 + } 29 + 30 + #[derive(Clone)] 31 + struct CheckUpstream(Url, Client); 32 + impl Fetcher<PlcStatus> for CheckUpstream { 33 + async fn fetch(&self) -> Result<PlcStatus, Box<dyn std::error::Error>> { 34 + Ok(plc_status(&self.0, &self.1).await) 35 + } 36 + } 37 + 38 + #[handler] 39 + fn hello( 40 + Data(State { 41 + sync_info, 42 + upstream, 43 + experimental: exp, 44 + .. 45 + }): Data<&State>, 46 + req: &Request, 47 + ) -> String { 48 + let pre_info = if sync_info.is_some() { 49 + format!( 50 + r#" 51 + This is a PLC[1] mirror running Allegedly in mirror mode. Mirror mode wraps and 52 + synchronizes a local PLC reference server instance[2] (why?[3]). 53 + 54 + 55 + Configured upstream: 56 + 57 + {upstream} 58 + 59 + "# 60 + ) 61 + } else { 62 + format!( 63 + r#" 64 + This is a PLC[1] mirror running Allegedly in wrap mode. Wrap mode reverse- 65 + proxies requests to a PLC server and can terminate TLS, like NGINX or Caddy. 66 + 67 + 68 + Configured upstream (only used if experimental op forwarding is enabled): 69 + 70 + {upstream} 71 + 72 + "# 73 + ) 74 + }; 75 + 76 + let post_info = match (exp.write_upstream, &exp.acme_domain, req.uri().host()) { 77 + (false, _, _) => " - POST /* Always rejected. This is a mirror.".to_string(), 78 + (_, None, _) => { 79 + " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 80 + } 81 + (_, Some(d), Some(f)) if f == d => { 82 + " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 83 + } 84 + (_, Some(d), _) => format!( 85 + r#" - POST /* Rejected, but experimental upstream op forwarding is 86 + available at `POST https://{d}/:did`!"# 87 + ), 88 + }; 89 + 90 + format!( 91 + r#"{} 92 + {pre_info} 93 + 94 + Available APIs: 95 + 96 + - GET /_health Health and version info 97 + 98 + - GET /* Proxies to wrapped server; see PLC API docs: 99 + https://web.plc.directory/api/redoc 100 + 101 + tip: try `GET /{{did}}` to resolve an identity 102 + 103 + {post_info} 104 + 105 + 106 + Allegedly is a suite of open-source CLI tools from for working with PLC logs, 107 + from microcosm: 108 + 109 + https://tangled.org/@microcosm.blue/Allegedly 110 + 111 + https://microcosm.blue 112 + 113 + 114 + [1] https://web.plc.directory 115 + [2] https://github.com/did-method-plc/did-method-plc 116 + [3] https://updates.microcosm.blue/3lz7nwvh4zc2u 117 + "#, 118 + logo("mirror") 119 + ) 120 + } 121 + 122 + #[handler] 123 + async fn health( 124 + Data(State { 125 + plc, 126 + client, 127 + sync_info, 128 + .. 129 + }): Data<&State>, 130 + ) -> impl IntoResponse { 131 + let mut overall_status = StatusCode::OK; 132 + let (ok, wrapped_status) = plc_status(plc, client).await; 133 + if !ok { 134 + overall_status = StatusCode::BAD_GATEWAY; 135 + } 136 + 137 + if let Some(SyncInfo { 138 + latest_at, 139 + upstream_status, 140 + }) = sync_info 141 + { 142 + let (ok, upstream_status) = upstream_status.get().await.expect("plc_status infallible"); 143 + if !ok { 144 + overall_status = StatusCode::BAD_GATEWAY; 145 + } 146 + let latest = latest_at.get().await.ok(); 147 + ( 148 + overall_status, 149 + Json(serde_json::json!({ 150 + "server": "allegedly (mirror)", 151 + "version": env!("CARGO_PKG_VERSION"), 152 + "wrapped_plc": wrapped_status, 153 + "upstream_plc": upstream_status, 154 + "latest_at": latest, 155 + })), 156 + ) 157 + } else { 158 + ( 159 + overall_status, 160 + Json(serde_json::json!({ 161 + "server": "allegedly (mirror)", 162 + "version": env!("CARGO_PKG_VERSION"), 163 + "wrapped_plc": wrapped_status, 164 + })), 165 + ) 166 + } 167 + } 168 + 169 + #[handler] 170 + async fn proxy(req: &Request, Data(state): Data<&State>) -> Result<Response> { 171 + let mut target = state.plc.clone(); 172 + target.set_path(req.uri().path()); 173 + target.set_query(req.uri().query()); 174 + let wrapped_res = state 175 + .client 176 + .get(target) 177 + .timeout(Duration::from_secs(3)) 178 + .headers(req.headers().clone()) 179 + .send() 180 + .await 181 + .map_err(|e| { 182 + log::error!("upstream req fail: {e}"); 183 + Error::from_string( 184 + failed_to_reach_named("wrapped reference PLC"), 185 + StatusCode::BAD_GATEWAY, 186 + ) 187 + })?; 188 + 189 + Ok(proxy_response(wrapped_res)) 190 + } 191 + 192 + #[handler] 193 + async fn forward_create_op_upstream( 194 + Data(State { 195 + upstream, 196 + client, 197 + experimental, 198 + .. 199 + }): Data<&State>, 200 + Path(did): Path<String>, 201 + req: &Request, 202 + body: Body, 203 + ) -> Result<Response> { 204 + if let Some(expected_domain) = &experimental.acme_domain { 205 + let Some(found_host) = req.uri().host() else { 206 + return Ok(bad_create_op(&format!( 207 + "missing `Host` header, expected {expected_domain:?} for experimental requests." 208 + ))); 209 + }; 210 + if found_host != expected_domain { 211 + return Ok(bad_create_op(&format!( 212 + "experimental requests must be made to {expected_domain:?}, but this request's `Host` header was {found_host}" 213 + ))); 214 + } 215 + } 216 + 217 + let mut headers: reqwest::header::HeaderMap = req.headers().clone(); 218 + log::trace!("original request headers: {headers:?}"); 219 + headers.insert("Host", upstream.host_str().unwrap().parse().unwrap()); 220 + let client_ua = headers 221 + .get(USER_AGENT) 222 + .map(|h| h.to_str().unwrap()) 223 + .unwrap_or("unknown"); 224 + headers.insert( 225 + USER_AGENT, 226 + format!("{UA} (forwarding from {client_ua:?})") 227 + .parse() 228 + .unwrap(), 229 + ); 230 + log::trace!("adjusted request headers: {headers:?}"); 231 + 232 + let mut target = upstream.clone(); 233 + target.set_path(&did); 234 + let upstream_res = client 235 + .post(target) 236 + .timeout(Duration::from_secs(15)) 237 + .headers(headers) 238 + .body(reqwest::Body::wrap_stream(body.into_bytes_stream())) 239 + .send() 240 + .await 241 + .map_err(|e| { 242 + log::warn!("upstream write fail: {e}"); 243 + Error::from_string( 244 + failed_to_reach_named("upstream PLC"), 245 + StatusCode::BAD_GATEWAY, 246 + ) 247 + })?; 248 + 249 + Ok(proxy_response(upstream_res)) 250 + } 251 + 252 + #[handler] 253 + async fn nope(Data(State { upstream, .. }): Data<&State>) -> (StatusCode, String) { 254 + ( 255 + StatusCode::BAD_REQUEST, 256 + format!( 257 + r#"{} 258 + 259 + Sorry, this server does not accept POST requests. 260 + 261 + You may wish to try sending that to our upstream: {upstream}. 262 + 263 + If you operate this server, try running with `--experimental-write-upstream`. 264 + "#, 265 + logo("mirror (nope)") 266 + ), 267 + ) 268 + } 269 + 270 + pub async fn serve( 271 + upstream: Url, 272 + plc: Url, 273 + listen: ListenConf, 274 + experimental: ExperimentalConf, 275 + db: Option<Db>, 276 + ) -> anyhow::Result<&'static str> { 277 + log::info!("starting server..."); 278 + 279 + let client = Client::builder() 280 + .user_agent(UA) 281 + .timeout(Duration::from_secs(10)) 282 + .build() 283 + .expect("reqwest client to build"); 284 + 285 + // when `db` is None, we're running in wrap mode. no db access, no upstream sync 286 + let sync_info = db.map(|db| SyncInfo { 287 + latest_at: CachedValue::new(GetLatestAt(db), Duration::from_secs(2)), 288 + upstream_status: CachedValue::new( 289 + CheckUpstream(upstream.clone(), client.clone()), 290 + Duration::from_secs(6), 291 + ), 292 + }); 293 + 294 + let state = State { 295 + client, 296 + plc, 297 + upstream: upstream.clone(), 298 + sync_info, 299 + experimental: experimental.clone(), 300 + }; 301 + 302 + let mut app = Route::new() 303 + .at("/", get(hello)) 304 + .at("/favicon.ico", get(favicon)) 305 + .at("/_health", get(health)) 306 + .at("/export", get(proxy)); 307 + 308 + if experimental.write_upstream { 309 + log::info!("enabling experimental write forwarding to upstream"); 310 + 311 + let ip_limiter = IpLimiters::new(Quota::per_hour(10.try_into().unwrap())); 312 + let did_limiter = CreatePlcOpLimiter::new(Quota::per_hour(4.try_into().unwrap())); 313 + 314 + let upstream_proxier = forward_create_op_upstream 315 + .with(GovernorMiddleware::new(did_limiter)) 316 + .with(GovernorMiddleware::new(ip_limiter)); 317 + 318 + app = app.at("/did:plc:*", get(proxy).post(upstream_proxier)); 319 + } else { 320 + app = app.at("/did:plc:*", get(proxy).post(nope)); 321 + } 322 + 323 + let app = app 324 + .with(AddData::new(state)) 325 + .with(Cors::new().allow_credentials(false)) 326 + .with(Compression::new()) 327 + .with(GovernorMiddleware::new(IpLimiters::new(Quota::per_minute( 328 + 3000.try_into().expect("ratelimit middleware to build"), 329 + )))) 330 + .with(CatchPanic::new()) 331 + .with(Tracing); 332 + 333 + bind_or_acme(app, listen).await 334 + }
-524
src/mirror.rs
··· 1 - use crate::{ 2 - CachedValue, CreatePlcOpLimiter, Db, Dt, Fetcher, GovernorMiddleware, IpLimiters, UA, logo, 3 - }; 4 - use futures::TryStreamExt; 5 - use governor::Quota; 6 - use poem::{ 7 - Body, Endpoint, EndpointExt, Error, IntoResponse, Request, Response, Result, Route, Server, 8 - get, handler, 9 - http::{StatusCode, header::USER_AGENT}, 10 - listener::{Listener, TcpListener, acme::AutoCert}, 11 - middleware::{AddData, CatchPanic, Compression, Cors, Tracing}, 12 - web::{Data, Json, Path}, 13 - }; 14 - use reqwest::{Client, Url}; 15 - use std::{net::SocketAddr, path::PathBuf, time::Duration}; 16 - 17 - #[derive(Clone)] 18 - struct State { 19 - client: Client, 20 - plc: Url, 21 - upstream: Url, 22 - sync_info: Option<SyncInfo>, 23 - experimental: ExperimentalConf, 24 - } 25 - 26 - /// server info that only applies in mirror (synchronizing) mode 27 - #[derive(Clone)] 28 - struct SyncInfo { 29 - latest_at: CachedValue<Dt, GetLatestAt>, 30 - upstream_status: CachedValue<PlcStatus, CheckUpstream>, 31 - } 32 - 33 - #[handler] 34 - fn hello( 35 - Data(State { 36 - sync_info, 37 - upstream, 38 - experimental: exp, 39 - .. 40 - }): Data<&State>, 41 - req: &Request, 42 - ) -> String { 43 - // let mode = if sync_info.is_some() { "mirror" } else { "wrap" }; 44 - let pre_info = if sync_info.is_some() { 45 - format!( 46 - r#" 47 - This is a PLC[1] mirror running Allegedly in mirror mode. Mirror mode wraps and 48 - synchronizes a local PLC reference server instance[2] (why?[3]). 49 - 50 - 51 - Configured upstream: 52 - 53 - {upstream} 54 - 55 - "# 56 - ) 57 - } else { 58 - format!( 59 - r#" 60 - This is a PLC[1] mirror running Allegedly in wrap mode. Wrap mode reverse- 61 - proxies requests to a PLC server and can terminate TLS, like NGINX or Caddy. 62 - 63 - 64 - Configured upstream (only used if experimental op forwarding is enabled): 65 - 66 - {upstream} 67 - 68 - "# 69 - ) 70 - }; 71 - 72 - let post_info = match (exp.write_upstream, &exp.acme_domain, req.uri().host()) { 73 - (false, _, _) => " - POST /* Always rejected. This is a mirror.".to_string(), 74 - (_, None, _) => { 75 - " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 76 - } 77 - (_, Some(d), Some(f)) if f == d => { 78 - " - POST /:did Create a PLC op. Allegedly will forward it upstream.".to_string() 79 - } 80 - (_, Some(d), _) => format!( 81 - r#" - POST /* Rejected, but experimental upstream op forwarding is 82 - available at `POST https://{d}/:did`!"# 83 - ), 84 - }; 85 - 86 - format!( 87 - r#"{} 88 - {pre_info} 89 - 90 - Available APIs: 91 - 92 - - GET /_health Health and version info 93 - 94 - - GET /* Proxies to wrapped server; see PLC API docs: 95 - https://web.plc.directory/api/redoc 96 - 97 - tip: try `GET /{{did}}` to resolve an identity 98 - 99 - {post_info} 100 - 101 - 102 - Allegedly is a suite of open-source CLI tools from for working with PLC logs, 103 - from microcosm: 104 - 105 - https://tangled.org/@microcosm.blue/Allegedly 106 - 107 - https://microcosm.blue 108 - 109 - 110 - [1] https://web.plc.directory 111 - [2] https://github.com/did-method-plc/did-method-plc 112 - [3] https://updates.microcosm.blue/3lz7nwvh4zc2u 113 - "#, 114 - logo("mirror") 115 - ) 116 - } 117 - 118 - #[handler] 119 - fn favicon() -> impl IntoResponse { 120 - include_bytes!("../favicon.ico").with_content_type("image/x-icon") 121 - } 122 - 123 - fn failed_to_reach_named(name: &str) -> String { 124 - format!( 125 - r#"{} 126 - 127 - Failed to reach the {name} server. Sorry. 128 - "#, 129 - logo("mirror 502 :( ") 130 - ) 131 - } 132 - 133 - fn bad_create_op(reason: &str) -> Response { 134 - Response::builder() 135 - .status(StatusCode::BAD_REQUEST) 136 - .body(format!( 137 - r#"{} 138 - 139 - NooOOOooooo: {reason} 140 - "#, 141 - logo("mirror 400 >:( ") 142 - )) 143 - } 144 - 145 - type PlcStatus = (bool, serde_json::Value); 146 - 147 - async fn plc_status(url: &Url, client: &Client) -> PlcStatus { 148 - use serde_json::json; 149 - 150 - let mut url = url.clone(); 151 - url.set_path("/_health"); 152 - 153 - let Ok(response) = client.get(url).timeout(Duration::from_secs(3)).send().await else { 154 - return (false, json!({"error": "cannot reach plc server"})); 155 - }; 156 - 157 - let status = response.status(); 158 - 159 - let Ok(text) = response.text().await else { 160 - return (false, json!({"error": "failed to read response body"})); 161 - }; 162 - 163 - let body = match serde_json::from_str(&text) { 164 - Ok(json) => json, 165 - Err(_) => serde_json::Value::String(text.to_string()), 166 - }; 167 - 168 - if status.is_success() { 169 - (true, body) 170 - } else { 171 - ( 172 - false, 173 - json!({ 174 - "error": "non-ok status", 175 - "status": status.as_str(), 176 - "status_code": status.as_u16(), 177 - "response": body, 178 - }), 179 - ) 180 - } 181 - } 182 - 183 - #[derive(Clone)] 184 - struct GetLatestAt(Db); 185 - impl Fetcher<Dt> for GetLatestAt { 186 - async fn fetch(&self) -> Result<Dt, Box<dyn std::error::Error>> { 187 - let now = self.0.get_latest().await?.ok_or(anyhow::anyhow!( 188 - "expected to find at least one thing in the db" 189 - ))?; 190 - Ok(now) 191 - } 192 - } 193 - 194 - #[derive(Clone)] 195 - struct CheckUpstream(Url, Client); 196 - impl Fetcher<PlcStatus> for CheckUpstream { 197 - async fn fetch(&self) -> Result<PlcStatus, Box<dyn std::error::Error>> { 198 - Ok(plc_status(&self.0, &self.1).await) 199 - } 200 - } 201 - 202 - #[handler] 203 - async fn health( 204 - Data(State { 205 - plc, 206 - client, 207 - sync_info, 208 - .. 209 - }): Data<&State>, 210 - ) -> impl IntoResponse { 211 - let mut overall_status = StatusCode::OK; 212 - let (ok, wrapped_status) = plc_status(plc, client).await; 213 - if !ok { 214 - overall_status = StatusCode::BAD_GATEWAY; 215 - } 216 - if let Some(SyncInfo { 217 - latest_at, 218 - upstream_status, 219 - }) = sync_info 220 - { 221 - // mirror mode 222 - let (ok, upstream_status) = upstream_status.get().await.expect("plc_status infallible"); 223 - if !ok { 224 - overall_status = StatusCode::BAD_GATEWAY; 225 - } 226 - let latest = latest_at.get().await.ok(); 227 - ( 228 - overall_status, 229 - Json(serde_json::json!({ 230 - "server": "allegedly (mirror)", 231 - "version": env!("CARGO_PKG_VERSION"), 232 - "wrapped_plc": wrapped_status, 233 - "upstream_plc": upstream_status, 234 - "latest_at": latest, 235 - })), 236 - ) 237 - } else { 238 - // wrap mode 239 - ( 240 - overall_status, 241 - Json(serde_json::json!({ 242 - "server": "allegedly (mirror)", 243 - "version": env!("CARGO_PKG_VERSION"), 244 - "wrapped_plc": wrapped_status, 245 - })), 246 - ) 247 - } 248 - } 249 - 250 - fn proxy_response(res: reqwest::Response) -> Response { 251 - let http_res: poem::http::Response<reqwest::Body> = res.into(); 252 - let (parts, reqw_body) = http_res.into_parts(); 253 - 254 - let parts = poem::ResponseParts { 255 - status: parts.status, 256 - version: parts.version, 257 - headers: parts.headers, 258 - extensions: parts.extensions, 259 - }; 260 - 261 - let body = http_body_util::BodyDataStream::new(reqw_body) 262 - .map_err(|e| std::io::Error::other(Box::new(e))); 263 - 264 - Response::from_parts(parts, poem::Body::from_bytes_stream(body)) 265 - } 266 - 267 - #[handler] 268 - async fn proxy(req: &Request, Data(state): Data<&State>) -> Result<Response> { 269 - let mut target = state.plc.clone(); 270 - target.set_path(req.uri().path()); 271 - target.set_query(req.uri().query()); 272 - let wrapped_res = state 273 - .client 274 - .get(target) 275 - .timeout(Duration::from_secs(3)) // should be low latency to wrapped server 276 - .headers(req.headers().clone()) 277 - .send() 278 - .await 279 - .map_err(|e| { 280 - log::error!("upstream req fail: {e}"); 281 - Error::from_string( 282 - failed_to_reach_named("wrapped reference PLC"), 283 - StatusCode::BAD_GATEWAY, 284 - ) 285 - })?; 286 - 287 - Ok(proxy_response(wrapped_res)) 288 - } 289 - 290 - #[handler] 291 - async fn forward_create_op_upstream( 292 - Data(State { 293 - upstream, 294 - client, 295 - experimental, 296 - .. 297 - }): Data<&State>, 298 - Path(did): Path<String>, 299 - req: &Request, 300 - body: Body, 301 - ) -> Result<Response> { 302 - if let Some(expected_domain) = &experimental.acme_domain { 303 - let Some(found_host) = req.uri().host() else { 304 - return Ok(bad_create_op(&format!( 305 - "missing `Host` header, expected {expected_domain:?} for experimental requests." 306 - ))); 307 - }; 308 - if found_host != expected_domain { 309 - return Ok(bad_create_op(&format!( 310 - "experimental requests must be made to {expected_domain:?}, but this request's `Host` header was {found_host}" 311 - ))); 312 - } 313 - } 314 - 315 - // adjust proxied headers 316 - let mut headers: reqwest::header::HeaderMap = req.headers().clone(); 317 - log::trace!("original request headers: {headers:?}"); 318 - headers.insert("Host", upstream.host_str().unwrap().parse().unwrap()); 319 - let client_ua = headers 320 - .get(USER_AGENT) 321 - .map(|h| h.to_str().unwrap()) 322 - .unwrap_or("unknown"); 323 - headers.insert( 324 - USER_AGENT, 325 - format!("{UA} (forwarding from {client_ua:?})") 326 - .parse() 327 - .unwrap(), 328 - ); 329 - log::trace!("adjusted request headers: {headers:?}"); 330 - 331 - let mut target = upstream.clone(); 332 - target.set_path(&did); 333 - let upstream_res = client 334 - .post(target) 335 - .timeout(Duration::from_secs(15)) // be a little generous 336 - .headers(headers) 337 - .body(reqwest::Body::wrap_stream(body.into_bytes_stream())) 338 - .send() 339 - .await 340 - .map_err(|e| { 341 - log::warn!("upstream write fail: {e}"); 342 - Error::from_string( 343 - failed_to_reach_named("upstream PLC"), 344 - StatusCode::BAD_GATEWAY, 345 - ) 346 - })?; 347 - 348 - Ok(proxy_response(upstream_res)) 349 - } 350 - 351 - #[handler] 352 - async fn nope(Data(State { upstream, .. }): Data<&State>) -> (StatusCode, String) { 353 - ( 354 - StatusCode::BAD_REQUEST, 355 - format!( 356 - r#"{} 357 - 358 - Sorry, this server does not accept POST requests. 359 - 360 - You may wish to try sending that to our upstream: {upstream}. 361 - 362 - If you operate this server, try running with `--experimental-write-upstream`. 363 - "#, 364 - logo("mirror (nope)") 365 - ), 366 - ) 367 - } 368 - 369 - #[derive(Debug)] 370 - pub enum ListenConf { 371 - Acme { 372 - domains: Vec<String>, 373 - cache_path: PathBuf, 374 - directory_url: String, 375 - ipv6: bool, 376 - }, 377 - Bind(SocketAddr), 378 - } 379 - 380 - #[derive(Debug, Clone)] 381 - pub struct ExperimentalConf { 382 - pub acme_domain: Option<String>, 383 - pub write_upstream: bool, 384 - } 385 - 386 - pub async fn serve( 387 - upstream: Url, 388 - plc: Url, 389 - listen: ListenConf, 390 - experimental: ExperimentalConf, 391 - db: Option<Db>, 392 - ) -> anyhow::Result<&'static str> { 393 - log::info!("starting server..."); 394 - 395 - // not using crate CLIENT: don't want the retries etc 396 - let client = Client::builder() 397 - .user_agent(UA) 398 - .timeout(Duration::from_secs(10)) // fallback 399 - .build() 400 - .expect("reqwest client to build"); 401 - 402 - // when `db` is None, we're running in wrap mode. no db access, no upstream sync 403 - let sync_info = db.map(|db| SyncInfo { 404 - latest_at: CachedValue::new(GetLatestAt(db), Duration::from_secs(2)), 405 - upstream_status: CachedValue::new( 406 - CheckUpstream(upstream.clone(), client.clone()), 407 - Duration::from_secs(6), 408 - ), 409 - }); 410 - 411 - let state = State { 412 - client, 413 - plc, 414 - upstream: upstream.clone(), 415 - sync_info, 416 - experimental: experimental.clone(), 417 - }; 418 - 419 - let mut app = Route::new() 420 - .at("/", get(hello)) 421 - .at("/favicon.ico", get(favicon)) 422 - .at("/_health", get(health)) 423 - .at("/export", get(proxy)); 424 - 425 - if experimental.write_upstream { 426 - log::info!("enabling experimental write forwarding to upstream"); 427 - 428 - let ip_limiter = IpLimiters::new(Quota::per_hour(10.try_into().unwrap())); 429 - let did_limiter = CreatePlcOpLimiter::new(Quota::per_hour(4.try_into().unwrap())); 430 - 431 - let upstream_proxier = forward_create_op_upstream 432 - .with(GovernorMiddleware::new(did_limiter)) 433 - .with(GovernorMiddleware::new(ip_limiter)); 434 - 435 - app = app.at("/did:plc:*", get(proxy).post(upstream_proxier)); 436 - } else { 437 - app = app.at("/did:plc:*", get(proxy).post(nope)); 438 - } 439 - 440 - let app = app 441 - .with(AddData::new(state)) 442 - .with(Cors::new().allow_credentials(false)) 443 - .with(Compression::new()) 444 - .with(GovernorMiddleware::new(IpLimiters::new(Quota::per_minute( 445 - 3000.try_into().expect("ratelimit middleware to build"), 446 - )))) 447 - .with(CatchPanic::new()) 448 - .with(Tracing); 449 - 450 - match listen { 451 - ListenConf::Acme { 452 - domains, 453 - cache_path, 454 - directory_url, 455 - ipv6, 456 - } => { 457 - rustls::crypto::aws_lc_rs::default_provider() 458 - .install_default() 459 - .expect("crypto provider to be installable"); 460 - 461 - let mut auto_cert = AutoCert::builder() 462 - .directory_url(directory_url) 463 - .cache_path(cache_path); 464 - for domain in domains { 465 - auto_cert = auto_cert.domain(domain); 466 - } 467 - let auto_cert = auto_cert.build().expect("acme config to build"); 468 - 469 - log::trace!("auto_cert: {auto_cert:?}"); 470 - 471 - let notice_task = tokio::task::spawn(run_insecure_notice(ipv6)); 472 - let listener = TcpListener::bind(if ipv6 { "[::]:443" } else { "0.0.0.0:443" }); 473 - let app_res = run(app, listener.acme(auto_cert)).await; 474 - log::warn!("server task ended, aborting insecure server task..."); 475 - notice_task.abort(); 476 - app_res?; 477 - notice_task.await??; 478 - } 479 - ListenConf::Bind(addr) => run(app, TcpListener::bind(addr)).await?, 480 - } 481 - 482 - Ok("server (uh oh?)") 483 - } 484 - 485 - async fn run<A, L>(app: A, listener: L) -> std::io::Result<()> 486 - where 487 - A: Endpoint + 'static, 488 - L: Listener + 'static, 489 - { 490 - Server::new(listener) 491 - .name("allegedly (mirror)") 492 - .run(app) 493 - .await 494 - } 495 - 496 - /// kick off a tiny little server on a tokio task to tell people to use 443 497 - async fn run_insecure_notice(ipv6: bool) -> Result<(), std::io::Error> { 498 - #[handler] 499 - fn oop_plz_be_secure() -> (StatusCode, String) { 500 - ( 501 - StatusCode::BAD_REQUEST, 502 - format!( 503 - r#"{} 504 - 505 - You probably want to change your request to use HTTPS instead of HTTP. 506 - "#, 507 - logo("mirror (tls on 443 please)") 508 - ), 509 - ) 510 - } 511 - 512 - let app = Route::new() 513 - .at("/favicon.ico", get(favicon)) 514 - .nest("/", get(oop_plz_be_secure)) 515 - .with(Tracing); 516 - Server::new(TcpListener::bind(if ipv6 { 517 - "[::]:80" 518 - } else { 519 - "0.0.0.0:80" 520 - })) 521 - .name("allegedly (mirror:80 helper)") 522 - .run(app) 523 - .await 524 - }
+1362
src/plc_fjall.rs
··· 1 + use crate::{BundleSource, Week}; 2 + use crate::{Dt, ExportPage, Op as CommonOp, PageBoundaryState}; 3 + use anyhow::Context; 4 + use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD}; 5 + use fjall::{ 6 + Database, Keyspace, KeyspaceCreateOptions, OwnedWriteBatch, PersistMode, 7 + config::BlockSizePolicy, 8 + }; 9 + use futures::Future; 10 + use serde::{Deserialize, Serialize}; 11 + use std::collections::BTreeMap; 12 + use std::fmt; 13 + use std::path::Path; 14 + use std::sync::Arc; 15 + use std::time::Instant; 16 + use tokio::io::{AsyncRead, AsyncWriteExt}; 17 + use tokio::sync::{mpsc, oneshot}; 18 + 19 + const SEP: u8 = 0; 20 + 21 + type IpldCid = cid::CidGeneric<64>; 22 + 23 + // 24 bytes -> 15 bytes 24 + fn encode_did(buf: &mut Vec<u8>, did: &str) -> anyhow::Result<usize> { 25 + let input = did.trim_start_matches("did:plc:").to_uppercase(); 26 + let len = BASE32_NOPAD 27 + .decode_len(input.len()) 28 + .map_err(|_| anyhow::anyhow!("failed to calculate decode len for {did}"))?; 29 + 30 + let start = buf.len(); 31 + buf.resize(start + len, 0); 32 + 33 + BASE32_NOPAD 34 + .decode_mut(input.as_bytes(), &mut buf[start..]) 35 + .map_err(|_| anyhow::anyhow!("failed to encode did {did}")) 36 + } 37 + 38 + // 59 bytes -> 36 bytes 39 + fn decode_cid_str(s: &str) -> anyhow::Result<Vec<u8>> { 40 + let cid = IpldCid::try_from(s)?; 41 + let mut buf = Vec::new(); 42 + cid.write_bytes(&mut buf) 43 + .map_err(|e| anyhow::anyhow!("failed to encode cid {s}: {e}"))?; 44 + Ok(buf) 45 + } 46 + 47 + fn decode_cid(bytes: &[u8]) -> anyhow::Result<String> { 48 + IpldCid::try_from(bytes) 49 + .map_err(|e| anyhow::anyhow!("failed to decode cid: {e}")) 50 + .map(|cid| cid.to_string()) 51 + } 52 + 53 + fn decode_did(bytes: &[u8]) -> String { 54 + let decoded = BASE32_NOPAD.encode(bytes).to_lowercase(); 55 + format!("did:plc:{decoded}") 56 + } 57 + 58 + fn op_key(created_at: &Dt, cid_suffix: &[u8]) -> Vec<u8> { 59 + let micros = created_at.timestamp_micros() as u64; 60 + let mut key = Vec::with_capacity(8 + 1 + cid_suffix.len()); 61 + key.extend_from_slice(&micros.to_be_bytes()); 62 + key.push(SEP); 63 + key.extend_from_slice(cid_suffix); 64 + key 65 + } 66 + 67 + fn by_did_prefix(did: &str) -> anyhow::Result<Vec<u8>> { 68 + let mut p = Vec::with_capacity(BASE32_NOPAD.decode_len(did.len())? + 1); 69 + encode_did(&mut p, did)?; 70 + p.push(SEP); 71 + Ok(p) 72 + } 73 + 74 + fn by_did_key(did: &str, created_at: &Dt, cid_suffix: &[u8]) -> anyhow::Result<Vec<u8>> { 75 + let mut key = by_did_prefix(did)?; 76 + let micros = created_at.timestamp_micros() as u64; 77 + key.extend_from_slice(&micros.to_be_bytes()); 78 + key.push(SEP); 79 + key.extend_from_slice(cid_suffix); 80 + Ok(key) 81 + } 82 + 83 + fn decode_timestamp(key: &[u8]) -> anyhow::Result<Dt> { 84 + let micros = u64::from_be_bytes( 85 + key.try_into() 86 + .map_err(|e| anyhow::anyhow!("invalid timestamp key {key:?}: {e}"))?, 87 + ); 88 + Dt::from_timestamp_micros(micros as i64) 89 + .ok_or_else(|| anyhow::anyhow!("invalid timestamp {micros}")) 90 + } 91 + 92 + /// base64url-encoded ECDSA signature → raw bytes 93 + #[derive(Debug, Clone, Serialize, Deserialize)] 94 + struct Signature(#[serde(with = "serde_bytes")] Vec<u8>); 95 + 96 + impl Signature { 97 + fn from_base64url(s: &str) -> anyhow::Result<Self> { 98 + BASE64URL_NOPAD 99 + .decode(s.as_bytes()) 100 + .map(Self) 101 + .map_err(|e| anyhow::anyhow!("invalid base64url sig {s}: {e}")) 102 + } 103 + } 104 + 105 + impl fmt::Display for Signature { 106 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 + f.write_str(&BASE64URL_NOPAD.encode(&self.0)) 108 + } 109 + } 110 + 111 + /// did:key:z... → raw multicodec public key bytes 112 + #[derive(Debug, Clone, Serialize, Deserialize)] 113 + struct DidKey(#[serde(with = "serde_bytes")] Vec<u8>); 114 + 115 + impl DidKey { 116 + fn from_did_key(s: &str) -> anyhow::Result<Self> { 117 + let multibase_str = s 118 + .strip_prefix("did:key:") 119 + .ok_or_else(|| anyhow::anyhow!("missing did:key: prefix in {s}"))?; 120 + let (_base, bytes) = multibase::decode(multibase_str) 121 + .map_err(|e| anyhow::anyhow!("invalid multibase in did:key {s}: {e}"))?; 122 + Ok(Self(bytes)) 123 + } 124 + } 125 + 126 + impl fmt::Display for DidKey { 127 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 + write!( 129 + f, 130 + "did:key:{}", 131 + multibase::encode(multibase::Base::Base58Btc, &self.0) 132 + ) 133 + } 134 + } 135 + 136 + /// CID string → binary CID bytes 137 + #[derive(Debug, Clone, Serialize, Deserialize)] 138 + struct PlcCid(#[serde(with = "serde_bytes")] Vec<u8>); 139 + 140 + impl PlcCid { 141 + fn from_cid_str(s: &str) -> anyhow::Result<Self> { 142 + let cid = IpldCid::try_from(s)?; 143 + let mut buf = Vec::new(); 144 + cid.write_bytes(&mut buf) 145 + .map_err(|e| anyhow::anyhow!("failed to encode cid {s}: {e}"))?; 146 + Ok(Self(buf)) 147 + } 148 + } 149 + 150 + impl fmt::Display for PlcCid { 151 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 152 + let cid = IpldCid::try_from(self.0.as_slice()).map_err(|_| fmt::Error)?; 153 + write!(f, "{cid}") 154 + } 155 + } 156 + 157 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 158 + enum Aka { 159 + Bluesky(String), 160 + Atproto(String), 161 + Other(String), 162 + } 163 + 164 + impl Aka { 165 + fn from_str(s: &str) -> Self { 166 + if let Some(stripped) = s.strip_prefix("at://") { 167 + if let Some(handle) = stripped.strip_suffix(".bsky.social") { 168 + Self::Bluesky(handle.to_string()) 169 + } else { 170 + Self::Atproto(stripped.to_string()) 171 + } 172 + } else { 173 + Self::Other(s.to_string()) 174 + } 175 + } 176 + } 177 + 178 + impl fmt::Display for Aka { 179 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 180 + match self { 181 + Self::Bluesky(h) => write!(f, "at://{h}.bsky.social"), 182 + Self::Atproto(h) => write!(f, "at://{h}"), 183 + Self::Other(s) => f.write_str(s), 184 + } 185 + } 186 + } 187 + 188 + #[derive(Debug, Clone, Serialize, Deserialize)] 189 + #[serde(rename_all = "snake_case")] 190 + enum OpType { 191 + PlcOperation, 192 + Create, 193 + PlcTombstone, 194 + Other(String), 195 + } 196 + 197 + impl OpType { 198 + fn from_str(s: &str) -> Self { 199 + match s { 200 + "plc_operation" => Self::PlcOperation, 201 + "create" => Self::Create, 202 + "plc_tombstone" => Self::PlcTombstone, 203 + other => Self::Other(other.to_string()), 204 + } 205 + } 206 + 207 + fn as_str(&self) -> &str { 208 + match self { 209 + Self::PlcOperation => "plc_operation", 210 + Self::Create => "create", 211 + Self::PlcTombstone => "plc_tombstone", 212 + Self::Other(s) => s, 213 + } 214 + } 215 + } 216 + 217 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 218 + enum StoredOpField { 219 + Type, 220 + Sig, 221 + Prev, 222 + RotationKeys, 223 + VerificationMethods, 224 + AlsoKnownAs, 225 + Services, 226 + SigningKey, 227 + RecoveryKey, 228 + Handle, 229 + Service, 230 + } 231 + 232 + impl StoredOpField { 233 + fn as_str(&self) -> &'static str { 234 + match self { 235 + Self::Type => "type", 236 + Self::Sig => "sig", 237 + Self::Prev => "prev", 238 + Self::RotationKeys => "rotationKeys", 239 + Self::VerificationMethods => "verificationMethods", 240 + Self::AlsoKnownAs => "alsoKnownAs", 241 + Self::Services => "services", 242 + Self::SigningKey => "signingKey", 243 + Self::RecoveryKey => "recoveryKey", 244 + Self::Handle => "handle", 245 + Self::Service => "service", 246 + } 247 + } 248 + } 249 + 250 + impl AsRef<str> for StoredOpField { 251 + fn as_ref(&self) -> &str { 252 + self.as_str() 253 + } 254 + } 255 + 256 + impl std::ops::Deref for StoredOpField { 257 + type Target = str; 258 + fn deref(&self) -> &Self::Target { 259 + self.as_str() 260 + } 261 + } 262 + 263 + impl fmt::Display for StoredOpField { 264 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 265 + f.write_str(self.as_str()) 266 + } 267 + } 268 + 269 + #[derive(Debug, thiserror::Error)] 270 + enum StoredOpError { 271 + #[error("operation is not an object")] 272 + NotAnObject, 273 + #[error("missing required field: {0}")] 274 + MissingField(StoredOpField), 275 + #[error("invalid field {0}: {1}")] 276 + InvalidField(StoredOpField, #[source] anyhow::Error), 277 + #[error("type mismatch for field {0}: expected {1}")] 278 + TypeMismatch(StoredOpField, &'static str), 279 + } 280 + 281 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] 282 + enum VerificationMethodKey { 283 + Atproto, 284 + Other(String), 285 + } 286 + 287 + impl VerificationMethodKey { 288 + fn from_str(s: &str) -> Self { 289 + match s { 290 + "atproto" => Self::Atproto, 291 + _ => Self::Other(s.to_string()), 292 + } 293 + } 294 + 295 + fn as_str(&self) -> &str { 296 + match self { 297 + Self::Atproto => "atproto", 298 + Self::Other(s) => s, 299 + } 300 + } 301 + } 302 + 303 + impl fmt::Display for VerificationMethodKey { 304 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 305 + f.write_str(self.as_str()) 306 + } 307 + } 308 + 309 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] 310 + enum ServiceKey { 311 + AtprotoPds, 312 + Other(String), 313 + } 314 + 315 + impl ServiceKey { 316 + fn from_str(s: &str) -> Self { 317 + match s { 318 + "atproto_pds" => Self::AtprotoPds, 319 + _ => Self::Other(s.to_string()), 320 + } 321 + } 322 + 323 + fn as_str(&self) -> &str { 324 + match self { 325 + Self::AtprotoPds => "atproto_pds", 326 + Self::Other(s) => s, 327 + } 328 + } 329 + } 330 + 331 + impl fmt::Display for ServiceKey { 332 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 333 + f.write_str(self.as_str()) 334 + } 335 + } 336 + 337 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 338 + enum ServiceType { 339 + AtprotoPersonalDataServer, 340 + Other(String), 341 + } 342 + 343 + impl ServiceType { 344 + fn from_str(s: &str) -> Self { 345 + match s { 346 + "AtprotoPersonalDataServer" => Self::AtprotoPersonalDataServer, 347 + _ => Self::Other(s.to_string()), 348 + } 349 + } 350 + 351 + fn as_str(&self) -> &str { 352 + match self { 353 + Self::AtprotoPersonalDataServer => "AtprotoPersonalDataServer", 354 + Self::Other(s) => s, 355 + } 356 + } 357 + } 358 + 359 + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 360 + enum ServiceEndpoint { 361 + BlueskyPds(String), 362 + Other(String), 363 + } 364 + 365 + impl ServiceEndpoint { 366 + fn from_str(s: &str) -> Self { 367 + if let Some(host) = s 368 + .strip_prefix("https://") 369 + .and_then(|h| h.strip_suffix(".host.bsky.network")) 370 + { 371 + Self::BlueskyPds(host.to_string()) 372 + } else { 373 + Self::Other(s.to_string()) 374 + } 375 + } 376 + 377 + fn as_string(&self) -> String { 378 + match self { 379 + Self::BlueskyPds(h) => format!("https://{h}.host.bsky.network"), 380 + Self::Other(s) => s.clone(), 381 + } 382 + } 383 + } 384 + 385 + #[derive(Debug, Clone, Serialize, Deserialize)] 386 + struct StoredService { 387 + r#type: ServiceType, 388 + endpoint: ServiceEndpoint, 389 + } 390 + 391 + #[derive(Debug, Clone, Serialize, Deserialize)] 392 + struct StoredOp { 393 + op_type: OpType, 394 + sig: Signature, 395 + prev: Option<PlcCid>, 396 + 397 + rotation_keys: Option<Vec<DidKey>>, 398 + verification_methods: Option<BTreeMap<VerificationMethodKey, DidKey>>, 399 + also_known_as: Option<Vec<Aka>>, 400 + services: Option<BTreeMap<ServiceKey, StoredService>>, 401 + 402 + // legacy create fields 403 + signing_key: Option<DidKey>, 404 + recovery_key: Option<DidKey>, 405 + handle: Option<String>, 406 + service: Option<String>, 407 + 408 + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 409 + unknown: BTreeMap<String, serde_json::Value>, 410 + } 411 + 412 + impl StoredOp { 413 + fn from_json_value(v: serde_json::Value) -> (Option<Self>, Vec<StoredOpError>) { 414 + let serde_json::Value::Object(mut obj) = v else { 415 + return (None, vec![StoredOpError::NotAnObject]); 416 + }; 417 + 418 + let mut errors = Vec::new(); 419 + let mut unknown = BTreeMap::new(); 420 + 421 + let op_type = match obj.remove(&*StoredOpField::Type) { 422 + Some(serde_json::Value::String(s)) => OpType::from_str(&s), 423 + Some(v) => { 424 + errors.push(StoredOpError::TypeMismatch(StoredOpField::Type, "string")); 425 + unknown.insert(StoredOpField::Type.to_string(), v); 426 + OpType::Other(String::new()) 427 + } 428 + Option::None => { 429 + errors.push(StoredOpError::MissingField(StoredOpField::Type)); 430 + OpType::Other(String::new()) 431 + } 432 + }; 433 + 434 + let sig = match obj.remove(&*StoredOpField::Sig) { 435 + Some(serde_json::Value::String(s)) => match Signature::from_base64url(&s) { 436 + Ok(sig) => sig, 437 + Err(e) => { 438 + errors.push(StoredOpError::InvalidField(StoredOpField::Sig, e)); 439 + unknown.insert(StoredOpField::Sig.to_string(), serde_json::Value::String(s)); 440 + Signature(Vec::new()) 441 + } 442 + }, 443 + Some(v) => { 444 + errors.push(StoredOpError::TypeMismatch(StoredOpField::Sig, "string")); 445 + unknown.insert(StoredOpField::Sig.to_string(), v); 446 + Signature(Vec::new()) 447 + } 448 + Option::None => { 449 + errors.push(StoredOpError::MissingField(StoredOpField::Sig)); 450 + Signature(Vec::new()) 451 + } 452 + }; 453 + 454 + let prev = match obj.remove(&*StoredOpField::Prev) { 455 + Some(serde_json::Value::Null) | Option::None => Option::None, 456 + Some(serde_json::Value::String(s)) => match PlcCid::from_cid_str(&s) { 457 + Ok(p) => Some(p), 458 + Err(e) => { 459 + errors.push(StoredOpError::InvalidField(StoredOpField::Prev, e)); 460 + unknown.insert( 461 + StoredOpField::Prev.to_string(), 462 + serde_json::Value::String(s), 463 + ); 464 + Option::None 465 + } 466 + }, 467 + Some(v) => { 468 + errors.push(StoredOpError::TypeMismatch(StoredOpField::Prev, "string")); 469 + unknown.insert(StoredOpField::Prev.to_string(), v); 470 + Option::None 471 + } 472 + }; 473 + 474 + let rotation_keys = match obj.remove(&*StoredOpField::RotationKeys) { 475 + Some(serde_json::Value::Array(arr)) => { 476 + let mut keys = Vec::with_capacity(arr.len()); 477 + let mut failed = false; 478 + for v in &arr { 479 + match v { 480 + serde_json::Value::String(s) => match DidKey::from_did_key(s) { 481 + Ok(k) => keys.push(k), 482 + Err(e) => { 483 + errors.push(StoredOpError::InvalidField( 484 + StoredOpField::RotationKeys, 485 + e, 486 + )); 487 + failed = true; 488 + break; 489 + } 490 + }, 491 + _ => { 492 + errors.push(StoredOpError::TypeMismatch( 493 + StoredOpField::RotationKeys, 494 + "string inside array", 495 + )); 496 + failed = true; 497 + break; 498 + } 499 + } 500 + } 501 + if failed { 502 + unknown.insert( 503 + StoredOpField::RotationKeys.to_string(), 504 + serde_json::Value::Array(arr), 505 + ); 506 + Option::None 507 + } else { 508 + Some(keys) 509 + } 510 + } 511 + Some(v) => { 512 + errors.push(StoredOpError::TypeMismatch( 513 + StoredOpField::RotationKeys, 514 + "array", 515 + )); 516 + unknown.insert(StoredOpField::RotationKeys.to_string(), v); 517 + Option::None 518 + } 519 + Option::None => Option::None, 520 + }; 521 + 522 + let verification_methods = match obj.remove(&*StoredOpField::VerificationMethods) { 523 + Some(serde_json::Value::Object(map)) => { 524 + let mut methods = BTreeMap::new(); 525 + let mut failed = false; 526 + for (k, v) in &map { 527 + match v { 528 + serde_json::Value::String(s) => match DidKey::from_did_key(s) { 529 + Ok(key) => { 530 + methods.insert(VerificationMethodKey::from_str(k), key); 531 + } 532 + Err(e) => { 533 + errors.push(StoredOpError::InvalidField( 534 + StoredOpField::VerificationMethods, 535 + e, 536 + )); 537 + failed = true; 538 + break; 539 + } 540 + }, 541 + _ => { 542 + errors.push(StoredOpError::TypeMismatch( 543 + StoredOpField::VerificationMethods, 544 + "string value in object", 545 + )); 546 + failed = true; 547 + break; 548 + } 549 + } 550 + } 551 + if failed { 552 + unknown.insert( 553 + StoredOpField::VerificationMethods.to_string(), 554 + serde_json::Value::Object(map), 555 + ); 556 + Option::None 557 + } else { 558 + Some(methods) 559 + } 560 + } 561 + Some(v) => { 562 + errors.push(StoredOpError::TypeMismatch( 563 + StoredOpField::VerificationMethods, 564 + "object", 565 + )); 566 + unknown.insert(StoredOpField::VerificationMethods.to_string(), v); 567 + Option::None 568 + } 569 + Option::None => Option::None, 570 + }; 571 + 572 + let also_known_as = match obj.remove(&*StoredOpField::AlsoKnownAs) { 573 + Some(serde_json::Value::Array(arr)) => { 574 + let mut akas = Vec::with_capacity(arr.len()); 575 + let mut failed = false; 576 + for v in &arr { 577 + match v { 578 + serde_json::Value::String(s) => akas.push(Aka::from_str(s)), 579 + _ => { 580 + errors.push(StoredOpError::TypeMismatch( 581 + StoredOpField::AlsoKnownAs, 582 + "string inside array", 583 + )); 584 + failed = true; 585 + break; 586 + } 587 + } 588 + } 589 + if failed { 590 + unknown.insert( 591 + StoredOpField::AlsoKnownAs.to_string(), 592 + serde_json::Value::Array(arr), 593 + ); 594 + Option::None 595 + } else { 596 + Some(akas) 597 + } 598 + } 599 + Some(v) => { 600 + errors.push(StoredOpError::TypeMismatch( 601 + StoredOpField::AlsoKnownAs, 602 + "array", 603 + )); 604 + unknown.insert(StoredOpField::AlsoKnownAs.to_string(), v); 605 + Option::None 606 + } 607 + Option::None => Option::None, 608 + }; 609 + 610 + let services = match obj.remove(&*StoredOpField::Services) { 611 + Some(serde_json::Value::Object(map)) => { 612 + let mut svcs = BTreeMap::new(); 613 + let mut failed = false; 614 + for (k, v) in &map { 615 + if let (Some(r#type), Some(endpoint)) = ( 616 + v.get("type").and_then(|t| t.as_str()), 617 + v.get("endpoint").and_then(|e| e.as_str()), 618 + ) { 619 + let svc = StoredService { 620 + r#type: ServiceType::from_str(r#type), 621 + endpoint: ServiceEndpoint::from_str(endpoint), 622 + }; 623 + svcs.insert(ServiceKey::from_str(k), svc); 624 + } else { 625 + errors.push(StoredOpError::TypeMismatch( 626 + StoredOpField::Services, 627 + "missing or invalid type/endpoint in service object", 628 + )); 629 + failed = true; 630 + break; 631 + } 632 + } 633 + if failed { 634 + unknown.insert( 635 + StoredOpField::Services.to_string(), 636 + serde_json::Value::Object(map), 637 + ); 638 + Option::None 639 + } else { 640 + Some(svcs) 641 + } 642 + } 643 + Some(v) => { 644 + errors.push(StoredOpError::TypeMismatch( 645 + StoredOpField::Services, 646 + "object", 647 + )); 648 + unknown.insert(StoredOpField::Services.to_string(), v); 649 + Option::None 650 + } 651 + Option::None => Option::None, 652 + }; 653 + 654 + let signing_key = match obj.remove(&*StoredOpField::SigningKey) { 655 + Some(serde_json::Value::String(s)) => match DidKey::from_did_key(&s) { 656 + Ok(key) => Some(key), 657 + Err(e) => { 658 + errors.push(StoredOpError::InvalidField(StoredOpField::SigningKey, e)); 659 + unknown.insert( 660 + StoredOpField::SigningKey.to_string(), 661 + serde_json::Value::String(s), 662 + ); 663 + Option::None 664 + } 665 + }, 666 + Some(v) => { 667 + errors.push(StoredOpError::TypeMismatch( 668 + StoredOpField::SigningKey, 669 + "string", 670 + )); 671 + unknown.insert(StoredOpField::SigningKey.to_string(), v); 672 + Option::None 673 + } 674 + Option::None => Option::None, 675 + }; 676 + 677 + let recovery_key = match obj.remove(&*StoredOpField::RecoveryKey) { 678 + Some(serde_json::Value::String(s)) => match DidKey::from_did_key(&s) { 679 + Ok(key) => Some(key), 680 + Err(e) => { 681 + errors.push(StoredOpError::InvalidField(StoredOpField::RecoveryKey, e)); 682 + unknown.insert( 683 + StoredOpField::RecoveryKey.to_string(), 684 + serde_json::Value::String(s), 685 + ); 686 + Option::None 687 + } 688 + }, 689 + Some(v) => { 690 + errors.push(StoredOpError::TypeMismatch( 691 + StoredOpField::RecoveryKey, 692 + "string", 693 + )); 694 + unknown.insert(StoredOpField::RecoveryKey.to_string(), v); 695 + Option::None 696 + } 697 + Option::None => Option::None, 698 + }; 699 + 700 + let handle = match obj.remove(&*StoredOpField::Handle) { 701 + Some(serde_json::Value::String(s)) => Some(s), 702 + Some(v) => { 703 + errors.push(StoredOpError::TypeMismatch(StoredOpField::Handle, "string")); 704 + unknown.insert(StoredOpField::Handle.to_string(), v); 705 + Option::None 706 + } 707 + Option::None => Option::None, 708 + }; 709 + 710 + let service = match obj.remove(&*StoredOpField::Service) { 711 + Some(serde_json::Value::String(s)) => Some(s), 712 + Some(v) => { 713 + errors.push(StoredOpError::TypeMismatch( 714 + StoredOpField::Service, 715 + "string", 716 + )); 717 + unknown.insert(StoredOpField::Service.to_string(), v); 718 + Option::None 719 + } 720 + Option::None => Option::None, 721 + }; 722 + 723 + for (k, v) in obj { 724 + unknown.insert(k, v); 725 + } 726 + 727 + ( 728 + Some(Self { 729 + op_type, 730 + sig, 731 + prev, 732 + rotation_keys, 733 + verification_methods, 734 + also_known_as, 735 + services, 736 + signing_key, 737 + recovery_key, 738 + handle, 739 + service, 740 + unknown, 741 + }), 742 + errors, 743 + ) 744 + } 745 + 746 + fn to_json_value(&self) -> serde_json::Value { 747 + let mut map = serde_json::Map::new(); 748 + 749 + map.insert((*StoredOpField::Type).into(), self.op_type.as_str().into()); 750 + map.insert((*StoredOpField::Sig).into(), self.sig.to_string().into()); 751 + map.insert( 752 + (*StoredOpField::Prev).into(), 753 + self.prev 754 + .as_ref() 755 + .map(|c| serde_json::Value::String(c.to_string())) 756 + .unwrap_or(serde_json::Value::Null), 757 + ); 758 + 759 + if let Some(keys) = &self.rotation_keys { 760 + map.insert( 761 + (*StoredOpField::RotationKeys).into(), 762 + keys.iter() 763 + .map(|k| serde_json::Value::String(k.to_string())) 764 + .collect::<Vec<_>>() 765 + .into(), 766 + ); 767 + } 768 + 769 + if let Some(methods) = &self.verification_methods { 770 + let obj: serde_json::Map<String, serde_json::Value> = methods 771 + .iter() 772 + .map(|(k, v)| { 773 + ( 774 + k.as_str().to_string(), 775 + serde_json::Value::String(v.to_string()), 776 + ) 777 + }) 778 + .collect(); 779 + map.insert((*StoredOpField::VerificationMethods).into(), obj.into()); 780 + } 781 + 782 + if let Some(aka) = &self.also_known_as { 783 + map.insert( 784 + (*StoredOpField::AlsoKnownAs).into(), 785 + aka.iter() 786 + .map(|h| serde_json::Value::String(h.to_string())) 787 + .collect::<Vec<_>>() 788 + .into(), 789 + ); 790 + } 791 + 792 + if let Some(services) = &self.services { 793 + let obj: serde_json::Map<String, serde_json::Value> = services 794 + .iter() 795 + .map(|(k, svc)| { 796 + ( 797 + k.as_str().to_string(), 798 + serde_json::json!({ 799 + "type": svc.r#type.as_str(), 800 + "endpoint": svc.endpoint.as_string(), 801 + }), 802 + ) 803 + }) 804 + .collect(); 805 + map.insert((*StoredOpField::Services).into(), obj.into()); 806 + } 807 + 808 + // legacy create fields 809 + if let Some(key) = &self.signing_key { 810 + map.insert((*StoredOpField::SigningKey).into(), key.to_string().into()); 811 + } 812 + if let Some(key) = &self.recovery_key { 813 + map.insert((*StoredOpField::RecoveryKey).into(), key.to_string().into()); 814 + } 815 + if let Some(handle) = &self.handle { 816 + map.insert((*StoredOpField::Handle).into(), handle.clone().into()); 817 + } 818 + if let Some(service) = &self.service { 819 + map.insert((*StoredOpField::Service).into(), service.clone().into()); 820 + } 821 + 822 + for (k, v) in &self.unknown { 823 + map.insert(k.clone(), v.clone()); 824 + } 825 + 826 + serde_json::Value::Object(map) 827 + } 828 + } 829 + 830 + // this is basically Op, but without the cid and created_at fields 831 + // since we have them in the key already 832 + #[derive(Debug, Deserialize, Serialize)] 833 + #[serde(rename_all = "camelCase")] 834 + struct DbOp { 835 + #[serde(with = "serde_bytes")] 836 + pub did: Vec<u8>, 837 + #[serde(with = "serde_bytes")] 838 + pub cid_prefix: Vec<u8>, 839 + pub nullified: bool, 840 + pub operation: StoredOp, 841 + } 842 + 843 + // we have our own Op struct for fjall since we dont want to have to convert Value back to RawValue 844 + #[derive(Debug, Serialize)] 845 + pub struct Op { 846 + pub did: String, 847 + pub cid: String, 848 + pub created_at: Dt, 849 + pub nullified: bool, 850 + pub operation: serde_json::Value, 851 + } 852 + 853 + #[derive(Clone)] 854 + pub struct FjallDb { 855 + inner: Arc<FjallInner>, 856 + } 857 + 858 + struct FjallInner { 859 + db: Database, 860 + ops: Keyspace, 861 + by_did: Keyspace, 862 + } 863 + 864 + impl FjallDb { 865 + pub fn open(path: impl AsRef<Path>) -> fjall::Result<Self> { 866 + const fn kb(kb: u32) -> u32 { 867 + kb * 1_024 868 + } 869 + const fn mb(mb: u32) -> u64 { 870 + kb(mb) as u64 * 1_024 871 + } 872 + 873 + let db = Database::builder(path) 874 + // 32mb is too low we can afford more 875 + // this should be configurable though! 876 + .cache_size(mb(256)) 877 + .open()?; 878 + let opts = KeyspaceCreateOptions::default; 879 + let ops = db.keyspace("ops", || { 880 + opts() 881 + // this is mainly for when backfilling 882 + .max_memtable_size(mb(192)) 883 + // this wont compress terribly well since its a bunch of CIDs and signatures and did:keys 884 + // and we want to keep reads fast since we'll be reading a lot... 885 + .data_block_size_policy(BlockSizePolicy::new([kb(4), kb(8), kb(32)])) 886 + // this has no downsides, since the only point reads that might miss we do is on by_did 887 + .expect_point_read_hits(true) 888 + })?; 889 + let by_did = db.keyspace("by_did", || { 890 + opts() 891 + .max_memtable_size(mb(64)) 892 + // this isn't gonna compress well anyway, since its just keys (did + timestamp + cid) 893 + // and dids dont have many operations in the first place, so we can use small blocks 894 + .data_block_size_policy(BlockSizePolicy::all(kb(2))) 895 + })?; 896 + Ok(Self { 897 + inner: Arc::new(FjallInner { db, ops, by_did }), 898 + }) 899 + } 900 + 901 + pub fn clear(&self) -> fjall::Result<()> { 902 + self.inner.ops.clear()?; 903 + self.inner.by_did.clear()?; 904 + Ok(()) 905 + } 906 + 907 + pub fn persist(&self) -> fjall::Result<()> { 908 + self.inner.db.persist(PersistMode::SyncAll) 909 + } 910 + 911 + pub fn compact(&self) -> fjall::Result<()> { 912 + self.inner.ops.major_compact()?; 913 + self.inner.by_did.major_compact()?; 914 + Ok(()) 915 + } 916 + 917 + pub fn get_latest(&self) -> anyhow::Result<Option<Dt>> { 918 + let Some(guard) = self.inner.ops.last_key_value() else { 919 + return Ok(None); 920 + }; 921 + let key = guard 922 + .key() 923 + .map_err(|e| anyhow::anyhow!("fjall key error: {e}"))?; 924 + 925 + key.get(..8) 926 + .ok_or_else(|| anyhow::anyhow!("invalid timestamp key {key:?}")) 927 + .map(decode_timestamp) 928 + .flatten() 929 + .map(Some) 930 + } 931 + 932 + pub fn insert_op(&self, batch: &mut OwnedWriteBatch, op: &CommonOp) -> anyhow::Result<usize> { 933 + let cid_bytes = decode_cid_str(&op.cid)?; 934 + let cid_prefix = cid_bytes 935 + .get(..30) 936 + .ok_or_else(|| anyhow::anyhow!("invalid cid length (prefix): {}", op.cid))? 937 + .to_vec(); 938 + let cid_suffix = cid_bytes 939 + .get(30..) 940 + .ok_or_else(|| anyhow::anyhow!("invalid cid length (suffix): {}", op.cid))?; 941 + 942 + let pk = by_did_key(&op.did, &op.created_at, cid_suffix)?; 943 + if self.inner.by_did.get(&pk)?.is_some() { 944 + return Ok(0); 945 + } 946 + let ts_key = op_key(&op.created_at, cid_suffix); 947 + 948 + let mut encoded_did = Vec::with_capacity(15); 949 + encode_did(&mut encoded_did, &op.did)?; 950 + 951 + let json_val: serde_json::Value = serde_json::from_str(op.operation.get())?; 952 + let (stored, mut errors) = StoredOp::from_json_value(json_val); 953 + 954 + let Some(operation) = stored else { 955 + return Err(errors.remove(0)).context("fatal operation parse error"); 956 + }; 957 + 958 + for e in &errors { 959 + log::warn!("failed to parse operation {} {}: {}", op.did, op.cid, e); 960 + } 961 + if !errors.is_empty() { 962 + // if parse failed but not fatal, we just dont store it 963 + return Ok(0); 964 + } 965 + 966 + let db_op = DbOp { 967 + did: encoded_did, 968 + cid_prefix, 969 + nullified: op.nullified, 970 + operation, 971 + }; 972 + let value = rmp_serde::to_vec(&db_op)?; 973 + batch.insert(&self.inner.ops, &ts_key, &value); 974 + batch.insert(&self.inner.by_did, &pk, &[]); 975 + Ok(1) 976 + } 977 + 978 + pub fn ops_for_did( 979 + &self, 980 + did: &str, 981 + ) -> anyhow::Result<impl Iterator<Item = anyhow::Result<Op>> + '_> { 982 + let prefix = by_did_prefix(did)?; 983 + 984 + Ok(self.inner.by_did.prefix(&prefix).map(move |guard| { 985 + let (by_did_key, _) = guard 986 + .into_inner() 987 + .map_err(|e| anyhow::anyhow!("fjall read error: {e}"))?; 988 + 989 + let key_rest = by_did_key 990 + .get(prefix.len()..) 991 + .ok_or_else(|| anyhow::anyhow!("invalid by_did key {by_did_key:?}"))?; 992 + 993 + let ts_bytes = key_rest 994 + .get(..8) 995 + .ok_or_else(|| anyhow::anyhow!("invalid length: {key_rest:?}"))?; 996 + let cid_suffix = key_rest 997 + .get(9..) 998 + .ok_or_else(|| anyhow::anyhow!("invalid length: {key_rest:?}"))?; 999 + 1000 + let op_key = [ts_bytes, &[SEP][..], cid_suffix].concat(); 1001 + let ts = decode_timestamp(ts_bytes)?; 1002 + 1003 + let value = self 1004 + .inner 1005 + .ops 1006 + .get(&op_key)? 1007 + .ok_or_else(|| anyhow::anyhow!("op not found: {op_key:?}"))?; 1008 + 1009 + let op: DbOp = rmp_serde::from_slice(&value)?; 1010 + let mut full_cid_bytes = op.cid_prefix.clone(); 1011 + full_cid_bytes.extend_from_slice(cid_suffix); 1012 + 1013 + let cid = decode_cid(&full_cid_bytes)?; 1014 + let did = decode_did(&op.did); 1015 + 1016 + Ok(Op { 1017 + did, 1018 + cid, 1019 + created_at: ts, 1020 + nullified: op.nullified, 1021 + operation: op.operation.to_json_value(), 1022 + }) 1023 + })) 1024 + } 1025 + 1026 + pub fn export_ops( 1027 + &self, 1028 + range: impl std::ops::RangeBounds<Dt>, 1029 + ) -> anyhow::Result<impl Iterator<Item = anyhow::Result<Op>> + '_> { 1030 + use std::ops::Bound; 1031 + let map_bound = |b: Bound<&Dt>| -> Bound<[u8; 8]> { 1032 + match b { 1033 + Bound::Included(dt) => Bound::Included(dt.timestamp_micros().to_be_bytes()), 1034 + Bound::Excluded(dt) => Bound::Excluded(dt.timestamp_micros().to_be_bytes()), 1035 + Bound::Unbounded => Bound::Unbounded, 1036 + } 1037 + }; 1038 + let range = (map_bound(range.start_bound()), map_bound(range.end_bound())); 1039 + 1040 + let iter = self.inner.ops.range(range); 1041 + 1042 + Ok(iter.map(|item| { 1043 + let (key, value) = item 1044 + .into_inner() 1045 + .map_err(|e| anyhow::anyhow!("fjall read error: {e}"))?; 1046 + let db_op: DbOp = rmp_serde::from_slice(&value)?; 1047 + let created_at = decode_timestamp( 1048 + key.get(..8) 1049 + .ok_or_else(|| anyhow::anyhow!("invalid op key {key:?}"))?, 1050 + )?; 1051 + let cid_suffix = key 1052 + .get(9..) 1053 + .ok_or_else(|| anyhow::anyhow!("invalid op key {key:?}"))?; 1054 + 1055 + let mut full_cid_bytes = db_op.cid_prefix.clone(); 1056 + full_cid_bytes.extend_from_slice(cid_suffix); 1057 + 1058 + let cid = decode_cid(&full_cid_bytes)?; 1059 + let did = decode_did(&db_op.did); 1060 + 1061 + Ok(Op { 1062 + did, 1063 + cid, 1064 + created_at, 1065 + nullified: db_op.nullified, 1066 + operation: db_op.operation.to_json_value(), 1067 + }) 1068 + })) 1069 + } 1070 + 1071 + pub fn export_ops_week( 1072 + &self, 1073 + week: Week, 1074 + ) -> anyhow::Result<impl Iterator<Item = anyhow::Result<Op>> + '_> { 1075 + let after: Dt = week.into(); 1076 + let before: Dt = week.next().into(); 1077 + 1078 + self.export_ops(after..before) 1079 + } 1080 + } 1081 + 1082 + impl BundleSource for FjallDb { 1083 + fn reader_for( 1084 + &self, 1085 + week: Week, 1086 + ) -> impl Future<Output = anyhow::Result<impl AsyncRead + Send>> + Send { 1087 + let db = self.clone(); 1088 + 1089 + async move { 1090 + let (mut tx, rx) = tokio::io::duplex(1024 * 1024 * 64); 1091 + 1092 + tokio::task::spawn_blocking(move || -> anyhow::Result<()> { 1093 + let iter = db.export_ops_week(week)?; 1094 + 1095 + let rt = tokio::runtime::Handle::current(); 1096 + 1097 + for op_res in iter { 1098 + let op = op_res?; 1099 + let operation_str = serde_json::to_string(&op.operation)?; 1100 + let common_op = crate::Op { 1101 + did: op.did, 1102 + cid: op.cid, 1103 + created_at: op.created_at, 1104 + nullified: op.nullified, 1105 + operation: serde_json::value::RawValue::from_string(operation_str)?, 1106 + }; 1107 + 1108 + let mut json_bytes = serde_json::to_vec(&common_op)?; 1109 + json_bytes.push(b'\n'); 1110 + 1111 + if rt.block_on(tx.write_all(&json_bytes)).is_err() { 1112 + break; 1113 + } 1114 + } 1115 + 1116 + Ok(()) 1117 + }); 1118 + 1119 + Ok(rx) 1120 + } 1121 + } 1122 + } 1123 + 1124 + pub async fn backfill_to_fjall( 1125 + db: FjallDb, 1126 + reset: bool, 1127 + mut pages: mpsc::Receiver<ExportPage>, 1128 + notify_last_at: Option<oneshot::Sender<Option<Dt>>>, 1129 + ) -> anyhow::Result<&'static str> { 1130 + let t0 = Instant::now(); 1131 + 1132 + if reset { 1133 + let db = db.clone(); 1134 + tokio::task::spawn_blocking(move || db.clear()).await??; 1135 + log::warn!("fjall reset: cleared all data"); 1136 + } 1137 + 1138 + let mut last_at = None; 1139 + let mut ops_inserted: usize = 0; 1140 + 1141 + while let Some(page) = pages.recv().await { 1142 + let should_track = notify_last_at.is_some(); 1143 + if should_track { 1144 + if let Some(s) = PageBoundaryState::new(&page) { 1145 + last_at = last_at.filter(|&l| l >= s.last_at).or(Some(s.last_at)); 1146 + } 1147 + } 1148 + 1149 + let db = db.clone(); 1150 + let count = tokio::task::spawn_blocking(move || -> anyhow::Result<usize> { 1151 + let mut batch = db.inner.db.batch(); 1152 + let mut count: usize = 0; 1153 + for op in &page.ops { 1154 + count += db.insert_op(&mut batch, op)?; 1155 + } 1156 + batch.commit()?; 1157 + Ok(count) 1158 + }) 1159 + .await??; 1160 + ops_inserted += count; 1161 + } 1162 + log::debug!("finished receiving bulk pages"); 1163 + 1164 + if let Some(notify) = notify_last_at { 1165 + log::trace!("notifying last_at: {last_at:?}"); 1166 + if notify.send(last_at).is_err() { 1167 + log::error!("receiver for last_at dropped, can't notify"); 1168 + }; 1169 + } 1170 + 1171 + let db = db.clone(); 1172 + tokio::task::spawn_blocking(move || db.persist()).await??; 1173 + 1174 + log::info!( 1175 + "backfill_to_fjall: inserted {ops_inserted} ops in {:?}", 1176 + t0.elapsed() 1177 + ); 1178 + Ok("backfill_to_fjall") 1179 + } 1180 + 1181 + pub async fn pages_to_fjall( 1182 + db: FjallDb, 1183 + mut pages: mpsc::Receiver<ExportPage>, 1184 + ) -> anyhow::Result<&'static str> { 1185 + log::info!("starting pages_to_fjall writer..."); 1186 + 1187 + let t0 = Instant::now(); 1188 + let mut ops_inserted: usize = 0; 1189 + 1190 + while let Some(page) = pages.recv().await { 1191 + log::trace!("writing page with {} ops", page.ops.len()); 1192 + let db = db.clone(); 1193 + let count = tokio::task::spawn_blocking(move || -> anyhow::Result<usize> { 1194 + let mut batch = db.inner.db.batch(); 1195 + let mut count: usize = 0; 1196 + for op in &page.ops { 1197 + count += db.insert_op(&mut batch, op)?; 1198 + } 1199 + batch.commit()?; 1200 + Ok(count) 1201 + }) 1202 + .await??; 1203 + ops_inserted += count; 1204 + } 1205 + 1206 + log::info!( 1207 + "no more pages. inserted {ops_inserted} ops in {:?}", 1208 + t0.elapsed() 1209 + ); 1210 + Ok("pages_to_fjall") 1211 + } 1212 + 1213 + #[cfg(test)] 1214 + mod tests { 1215 + use super::*; 1216 + 1217 + #[test] 1218 + fn signature_roundtrip() { 1219 + let original = "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA"; 1220 + let sig = Signature::from_base64url(original).unwrap(); 1221 + assert_eq!(sig.0.len(), 64); 1222 + assert_eq!(sig.to_string(), original); 1223 + } 1224 + 1225 + #[test] 1226 + fn did_key_roundtrip() { 1227 + let original = "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg"; 1228 + let key = DidKey::from_did_key(original).unwrap(); 1229 + assert_eq!(key.to_string(), original); 1230 + } 1231 + 1232 + #[test] 1233 + fn plc_cid_roundtrip() { 1234 + let original = "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm"; 1235 + let cid = PlcCid::from_cid_str(original).unwrap(); 1236 + assert_eq!(cid.to_string(), original); 1237 + } 1238 + 1239 + #[test] 1240 + fn bsky_handle_roundtrip() { 1241 + let h = Aka::from_str("at://alice.bsky.social"); 1242 + assert_eq!(h, Aka::Bluesky("alice".to_string())); 1243 + assert_eq!(h.to_string(), "at://alice.bsky.social"); 1244 + } 1245 + 1246 + #[test] 1247 + fn atproto_handle_roundtrip() { 1248 + let h = Aka::from_str("at://alice.example.com"); 1249 + assert_eq!(h, Aka::Atproto("alice.example.com".to_string())); 1250 + assert_eq!(h.to_string(), "at://alice.example.com"); 1251 + } 1252 + 1253 + #[test] 1254 + fn other_handle_roundtrip() { 1255 + let h = Aka::from_str("https://something.else"); 1256 + assert_eq!(h, Aka::Other("https://something.else".to_string())); 1257 + assert_eq!(h.to_string(), "https://something.else"); 1258 + } 1259 + 1260 + #[test] 1261 + fn verification_method_key_roundtrip() { 1262 + let k1 = VerificationMethodKey::from_str("atproto"); 1263 + assert_eq!(k1, VerificationMethodKey::Atproto); 1264 + assert_eq!(k1.to_string(), "atproto"); 1265 + 1266 + let k2 = VerificationMethodKey::from_str("other_key"); 1267 + assert_eq!(k2, VerificationMethodKey::Other("other_key".to_string())); 1268 + assert_eq!(k2.to_string(), "other_key"); 1269 + } 1270 + 1271 + #[test] 1272 + fn service_key_roundtrip() { 1273 + let k1 = ServiceKey::from_str("atproto_pds"); 1274 + assert_eq!(k1, ServiceKey::AtprotoPds); 1275 + assert_eq!(k1.to_string(), "atproto_pds"); 1276 + 1277 + let k2 = ServiceKey::from_str("other_svc"); 1278 + assert_eq!(k2, ServiceKey::Other("other_svc".to_string())); 1279 + assert_eq!(k2.to_string(), "other_svc"); 1280 + } 1281 + 1282 + #[test] 1283 + fn service_type_roundtrip() { 1284 + let t1 = ServiceType::from_str("AtprotoPersonalDataServer"); 1285 + assert_eq!(t1, ServiceType::AtprotoPersonalDataServer); 1286 + assert_eq!(t1.as_str(), "AtprotoPersonalDataServer"); 1287 + 1288 + let t2 = ServiceType::from_str("OtherType"); 1289 + assert_eq!(t2, ServiceType::Other("OtherType".to_string())); 1290 + assert_eq!(t2.as_str(), "OtherType"); 1291 + } 1292 + 1293 + #[test] 1294 + fn service_endpoint_roundtrip() { 1295 + let e1 = ServiceEndpoint::from_str("https://example.host.bsky.network"); 1296 + assert_eq!(e1, ServiceEndpoint::BlueskyPds("example".to_string())); 1297 + assert_eq!(e1.as_string(), "https://example.host.bsky.network"); 1298 + 1299 + let e2 = ServiceEndpoint::from_str("https://other.endpoint.com"); 1300 + assert_eq!( 1301 + e2, 1302 + ServiceEndpoint::Other("https://other.endpoint.com".to_string()) 1303 + ); 1304 + assert_eq!(e2.as_string(), "https://other.endpoint.com"); 1305 + } 1306 + 1307 + #[test] 1308 + fn op_type_roundtrip() { 1309 + assert_eq!(OpType::from_str("plc_operation").as_str(), "plc_operation"); 1310 + assert_eq!(OpType::from_str("create").as_str(), "create"); 1311 + assert_eq!(OpType::from_str("plc_tombstone").as_str(), "plc_tombstone"); 1312 + assert_eq!(OpType::from_str("weird_thing").as_str(), "weird_thing"); 1313 + } 1314 + 1315 + #[test] 1316 + fn stored_op_fixture_roundtrip() { 1317 + let fixtures = [ 1318 + "tests/fixtures/log_bskyapp.json", 1319 + "tests/fixtures/log_legacy_dholms.json", 1320 + "tests/fixtures/log_nullification.json", 1321 + "tests/fixtures/log_tombstone.json", 1322 + ]; 1323 + 1324 + let mut total_json_size = 0; 1325 + let mut total_packed_size = 0; 1326 + 1327 + for path in fixtures { 1328 + let data = std::fs::read_to_string(path).unwrap(); 1329 + let entries: Vec<serde_json::Value> = serde_json::from_str(&data).unwrap(); 1330 + 1331 + for entry in &entries { 1332 + let op = &entry["operation"]; 1333 + let (stored, errors) = StoredOp::from_json_value(op.clone()); 1334 + if !errors.is_empty() { 1335 + let mut msg = format!("failed to parse op in {path}:\n"); 1336 + for e in errors { 1337 + msg.push_str(&format!(" - {e:?}\n")); 1338 + } 1339 + msg.push_str(&format!("op: {op}\n")); 1340 + panic!("{msg}"); 1341 + } 1342 + 1343 + // msgpack verification 1344 + let packed = rmp_serde::to_vec(&stored).unwrap(); 1345 + let unpacked: StoredOp = rmp_serde::from_slice(&packed).unwrap(); 1346 + 1347 + let reconstructed = unpacked.to_json_value(); 1348 + assert_eq!(*op, reconstructed, "roundtrip mismatch in {path}"); 1349 + 1350 + total_json_size += serde_json::to_vec(op).unwrap().len(); 1351 + total_packed_size += packed.len(); 1352 + } 1353 + } 1354 + 1355 + println!( 1356 + "json size: {} bytes, msgpack size: {} bytes, saved: {} bytes", 1357 + total_json_size, 1358 + total_packed_size, 1359 + total_json_size as isize - total_packed_size as isize 1360 + ); 1361 + } 1362 + }
+42 -20
src/poll.rs
··· 139 139 /// 140 140 /// Extracts the final op so it can be used to fetch the following page 141 141 pub async fn get_page(url: Url) -> Result<(ExportPage, Option<LastOp>), GetPageError> { 142 + use futures::TryStreamExt; 143 + use tokio::io::{AsyncBufReadExt, BufReader}; 144 + use tokio_util::compat::FuturesAsyncReadCompatExt; 145 + 142 146 log::trace!("Getting page: {url}"); 143 147 144 - let ops: Vec<Op> = CLIENT 145 - .get(url) 146 - .send() 147 - .await? 148 - .error_for_status()? 149 - .text() 150 - .await? 151 - .trim() 152 - .split('\n') 153 - .filter_map(|s| { 154 - serde_json::from_str::<Op>(s) 155 - .inspect_err(|e| { 156 - if !s.is_empty() { 157 - log::warn!("failed to parse op: {e} ({s})") 158 - } 159 - }) 160 - .ok() 161 - }) 162 - .collect(); 148 + let res = CLIENT.get(url).send().await?.error_for_status()?; 149 + let stream = Box::pin( 150 + res.bytes_stream() 151 + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) 152 + .into_async_read() 153 + .compat(), 154 + ); 155 + 156 + let mut lines = BufReader::new(stream).lines(); 157 + let mut ops = Vec::new(); 158 + 159 + loop { 160 + match lines.next_line().await { 161 + Ok(Some(line)) => { 162 + let line = line.trim(); 163 + if line.is_empty() { 164 + continue; 165 + } 166 + match serde_json::from_str::<Op>(line) { 167 + Ok(op) => ops.push(op), 168 + Err(e) => log::warn!("failed to parse op: {e} ({line})"), 169 + } 170 + } 171 + Ok(None) => break, 172 + Err(e) => { 173 + log::warn!("transport error mid-page: {}; returning partial page", e); 174 + break; 175 + } 176 + } 177 + } 163 178 164 179 let last_op = ops.last().map(Into::into); 165 180 ··· 214 229 .append_pair("after", &pl.created_at.to_rfc3339()); 215 230 }; 216 231 217 - let (mut page, next_last) = get_page(url).await?; 232 + let (mut page, next_last) = match get_page(url).await { 233 + Ok(res) => res, 234 + Err(e) => { 235 + log::warn!("error polling upstream: {e}"); 236 + continue; 237 + } 238 + }; 239 + 218 240 if let Some(ref mut state) = boundary_state { 219 241 state.apply_to_next(&mut page); 220 242 } else {
+61 -27
src/weekly.rs
··· 101 101 let file = File::open(path) 102 102 .await 103 103 .inspect_err(|e| log::error!("failed to open file: {e}"))?; 104 - Ok(file) 104 + let decoder = GzipDecoder::new(BufReader::new(file)); 105 + Ok(decoder) 105 106 } 106 107 } 107 108 ··· 112 113 use futures::TryStreamExt; 113 114 let HttpSource(base) = self; 114 115 let url = base.join(&format!("{}.jsonl.gz", week.0))?; 115 - Ok(CLIENT 116 + let stream = CLIENT 116 117 .get(url) 117 118 .send() 118 119 .await? ··· 120 121 .bytes_stream() 121 122 .map_err(futures::io::Error::other) 122 123 .into_async_read() 123 - .compat()) 124 + .compat(); 125 + let decoder = GzipDecoder::new(BufReader::new(stream)); 126 + Ok(decoder) 124 127 } 125 128 } 126 129 ··· 197 200 dest: mpsc::Sender<ExportPage>, 198 201 ) -> anyhow::Result<()> { 199 202 use futures::TryStreamExt; 200 - let reader = source 201 - .reader_for(week) 202 - .await 203 - .inspect_err(|e| log::error!("week_to_pages reader failed: {e}"))?; 204 - let decoder = GzipDecoder::new(BufReader::new(reader)); 205 - let mut chunks = pin!(LinesStream::new(BufReader::new(decoder).lines()).try_chunks(10000)); 203 + let mut retry_backoff = std::time::Duration::from_secs(2); 206 204 207 - while let Some(chunk) = chunks 208 - .try_next() 209 - .await 210 - .inspect_err(|e| log::error!("failed to get next chunk: {e}"))? 211 - { 212 - let ops: Vec<Op> = chunk 213 - .into_iter() 214 - .filter_map(|s| { 215 - serde_json::from_str::<Op>(&s) 216 - .inspect_err(|e| log::warn!("failed to parse op: {e} ({s})")) 217 - .ok() 218 - }) 219 - .collect(); 220 - let page = ExportPage { ops }; 221 - dest.send(page) 222 - .await 223 - .inspect_err(|e| log::error!("failed to send page: {e}"))?; 205 + loop { 206 + let reader = match source.reader_for(week).await { 207 + Ok(r) => r, 208 + Err(e) => { 209 + log::warn!( 210 + "week_to_pages reader_for failed {e}, retrying in {}s", 211 + retry_backoff.as_secs() 212 + ); 213 + tokio::time::sleep(retry_backoff).await; 214 + retry_backoff = (retry_backoff * 2).min(std::time::Duration::from_secs(300)); 215 + continue; 216 + } 217 + }; 218 + 219 + let mut chunks = pin!(LinesStream::new(BufReader::new(reader).lines()).try_chunks(10000)); 220 + let mut success = true; 221 + 222 + while let Some(chunk) = match chunks.as_mut().try_next().await { 223 + Ok(Some(c)) => Some(c), 224 + Ok(None) => None, 225 + Err(e) => { 226 + log::warn!( 227 + "failed to get next chunk: {e}, retrying week in {}s", 228 + retry_backoff.as_secs() 229 + ); 230 + tokio::time::sleep(retry_backoff).await; 231 + retry_backoff = (retry_backoff * 2).min(std::time::Duration::from_secs(300)); 232 + success = false; 233 + None 234 + } 235 + } { 236 + let ops: Vec<Op> = chunk 237 + .into_iter() 238 + .filter_map(|s| { 239 + serde_json::from_str::<Op>(&s) 240 + .inspect_err(|e| log::warn!("failed to parse op: {e} ({s})")) 241 + .ok() 242 + }) 243 + .collect(); 244 + 245 + if ops.is_empty() { 246 + continue; 247 + } 248 + 249 + let page = ExportPage { ops }; 250 + if let Err(e) = dest.send(page).await { 251 + log::error!("failed to send page (receiver closed): {e}"); 252 + return Err(e.into()); 253 + } 254 + } 255 + 256 + if success { 257 + return Ok(()); 258 + } 224 259 } 225 - Ok(()) 226 260 }
+56
tests/fixtures/log_bskyapp.json
··· 1 + [ 2 + { 3 + "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 4 + "operation": { 5 + "sig": "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA", 6 + "prev": null, 7 + "type": "plc_operation", 8 + "services": { 9 + "atproto_pds": { 10 + "type": "AtprotoPersonalDataServer", 11 + "endpoint": "https://bsky.social" 12 + } 13 + }, 14 + "alsoKnownAs": [ 15 + "at://bluesky-team.bsky.social" 16 + ], 17 + "rotationKeys": [ 18 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 19 + "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 20 + ], 21 + "verificationMethods": { 22 + "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 23 + } 24 + }, 25 + "cid": "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm", 26 + "nullified": false, 27 + "createdAt": "2023-04-12T04:53:57.057Z" 28 + }, 29 + { 30 + "did": "did:plc:z72i7hdynmk6r22z27h6tvur", 31 + "operation": { 32 + "sig": "1mEWzRtFOgeRXH-YCSPTxb990JOXxa__n8Qw6BOKl7Ndm6OFFmwYKiiMqMCpAbxpnGjF5abfIsKc7u3a77Cbnw", 33 + "prev": "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm", 34 + "type": "plc_operation", 35 + "services": { 36 + "atproto_pds": { 37 + "type": "AtprotoPersonalDataServer", 38 + "endpoint": "https://bsky.social" 39 + } 40 + }, 41 + "alsoKnownAs": [ 42 + "at://bsky.app" 43 + ], 44 + "rotationKeys": [ 45 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 46 + "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 47 + ], 48 + "verificationMethods": { 49 + "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 50 + } 51 + }, 52 + "cid": "bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq", 53 + "nullified": false, 54 + "createdAt": "2023-04-12T17:26:46.468Z" 55 + } 56 + ]
+125
tests/fixtures/log_legacy_dholms.json
··· 1 + [ 2 + { 3 + "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 4 + "operation": { 5 + "sig": "7QTzqO1BcL3eDzP4P_YBxMmv5U4brHzAItkM9w5o8gZA7ElZkrVYEwsfQCfk5EoWLk58Z1y6fyNP9x1pthJnlw", 6 + "prev": null, 7 + "type": "create", 8 + "handle": "dan.bsky.social", 9 + "service": "https://bsky.social", 10 + "signingKey": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ", 11 + "recoveryKey": "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg" 12 + }, 13 + "cid": "bafyreigcxay6ucqlwowfpu35alyxqtv3c4vsj7gmdtmnidsnqs6nblyarq", 14 + "nullified": false, 15 + "createdAt": "2022-11-17T01:07:13.996Z" 16 + }, 17 + { 18 + "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 19 + "operation": { 20 + "sig": "n-VWsPZY4xkFN8wlg-kJBU_yzWTNd2oBnbjkjxXu3HdjbBLaEB7K39JHIPn_DZVALKRjts6bUicjSEecZy8eIw", 21 + "prev": "bafyreigcxay6ucqlwowfpu35alyxqtv3c4vsj7gmdtmnidsnqs6nblyarq", 22 + "type": "plc_operation", 23 + "services": { 24 + "atproto_pds": { 25 + "type": "AtprotoPersonalDataServer", 26 + "endpoint": "https://bsky.social" 27 + } 28 + }, 29 + "alsoKnownAs": [ 30 + "at://dholms.xyz" 31 + ], 32 + "rotationKeys": [ 33 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 34 + "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 35 + ], 36 + "verificationMethods": { 37 + "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 38 + } 39 + }, 40 + "cid": "bafyreiho5sanautvnw3det66jcwic4vkeabc35y7iou3ygwj2l3xqcxdau", 41 + "nullified": false, 42 + "createdAt": "2023-03-06T18:47:09.501Z" 43 + }, 44 + { 45 + "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 46 + "operation": { 47 + "sig": "HWgrfQXxUN3mhR5TR-nrwGJwVr9RDbyDn6eCmqBg32x2zIjhe98YxOtFOLI9jQkBlTTzqzUOwJh1KZd4O2pDOw", 48 + "prev": "bafyreiho5sanautvnw3det66jcwic4vkeabc35y7iou3ygwj2l3xqcxdau", 49 + "type": "plc_operation", 50 + "services": { 51 + "atproto_pds": { 52 + "type": "AtprotoPersonalDataServer", 53 + "endpoint": "https://bsky.social" 54 + } 55 + }, 56 + "alsoKnownAs": [ 57 + "at://dholms.bsky.social" 58 + ], 59 + "rotationKeys": [ 60 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 61 + "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 62 + ], 63 + "verificationMethods": { 64 + "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 65 + } 66 + }, 67 + "cid": "bafyreic3am2nmgykxtwsxwigzn6faibxv5ef5kalcv7li3eatcqldcqrku", 68 + "nullified": false, 69 + "createdAt": "2023-03-06T19:50:49.987Z" 70 + }, 71 + { 72 + "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 73 + "operation": { 74 + "sig": "9Fy2iHCSK5mtgLNCkS9CyI0r7lu6H1SVgusaD1jQdsMUySUU6apde0z7SobpYZKp4sThk4hxOWtO-bXhu1cNjg", 75 + "prev": "bafyreic3am2nmgykxtwsxwigzn6faibxv5ef5kalcv7li3eatcqldcqrku", 76 + "type": "plc_operation", 77 + "services": { 78 + "atproto_pds": { 79 + "type": "AtprotoPersonalDataServer", 80 + "endpoint": "https://bsky.social" 81 + } 82 + }, 83 + "alsoKnownAs": [ 84 + "at://dholms.xyz" 85 + ], 86 + "rotationKeys": [ 87 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 88 + "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 89 + ], 90 + "verificationMethods": { 91 + "atproto": "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 92 + } 93 + }, 94 + "cid": "bafyreicwybxr6h6vkxpoarismso3liozdzswshmzcvl4tyckdazn5lxjte", 95 + "nullified": false, 96 + "createdAt": "2023-03-06T19:51:09.950Z" 97 + }, 98 + { 99 + "did": "did:plc:yk4dd2qkboz2yv6tpubpc6co", 100 + "operation": { 101 + "sig": "lBXd8rHZ84hCuQysGdi_5A9C8yPHTHasPibO4DZiuZVrehs2hiBcjAL0srLSTsF1kvsHTw1ddai-QwH0Wd_drQ", 102 + "prev": "bafyreicwybxr6h6vkxpoarismso3liozdzswshmzcvl4tyckdazn5lxjte", 103 + "type": "plc_operation", 104 + "services": { 105 + "atproto_pds": { 106 + "type": "AtprotoPersonalDataServer", 107 + "endpoint": "https://bsky.social" 108 + } 109 + }, 110 + "alsoKnownAs": [ 111 + "at://dholms.xyz" 112 + ], 113 + "rotationKeys": [ 114 + "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg", 115 + "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 116 + ], 117 + "verificationMethods": { 118 + "atproto": "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 119 + } 120 + }, 121 + "cid": "bafyreidfrpuegbqd5r56shka4duythb7phb6d7i3bck2dkeb5fjppwd7gi", 122 + "nullified": false, 123 + "createdAt": "2023-03-09T23:18:31.709Z" 124 + } 125 + ]
+59
tests/fixtures/log_nullification.json
··· 1 + [ 2 + { 3 + "did": "did:plc:2s2mvm52ttz6r4hocmrq7x27", 4 + "operation": { 5 + "type": "plc_operation", 6 + "rotationKeys": [ 7 + "did:key:zQ3shwPdax6jKMbhtzbueGwSjc7RnjsmPcNB1vQUpbKUCN1t1", 8 + "did:key:zQ3shmMcsFLvSZQy2pSZFFKZNmQSNmyuSepPXMHpgTYrontu2" 9 + ], 10 + "verificationMethods": {}, 11 + "alsoKnownAs": [ 12 + "at://foo" 13 + ], 14 + "services": {}, 15 + "prev": null, 16 + "sig": "1pTjQD5-LA4gN6tz1t7-28Tzct9NI-oYMN8QVUQkB7493j-vKeZnZO0YqNNBCXsORatmfKUXwteC9zW82mi_yw" 17 + }, 18 + "cid": "bafyreiguwtflhou46pupb3qtemh56x6o3fn3y7ym2q3jgoms35tdu2d5ga", 19 + "nullified": false, 20 + "createdAt": "1970-01-01T00:00:00.000Z" 21 + }, 22 + { 23 + "did": "did:plc:2s2mvm52ttz6r4hocmrq7x27", 24 + "operation": { 25 + "prev": "bafyreiguwtflhou46pupb3qtemh56x6o3fn3y7ym2q3jgoms35tdu2d5ga", 26 + "type": "plc_operation", 27 + "services": {}, 28 + "alsoKnownAs": [ 29 + "at://foo" 30 + ], 31 + "rotationKeys": [], 32 + "verificationMethods": {}, 33 + "sig": "XfbRKnfzmJyRXUrajAISgdiJWNdNFMFgtDnlKvj-bIZhpfqGd39xiylJVXXu5Usw2cc2Js4EeSPh2xfhUQNxSA" 34 + }, 35 + "cid": "bafyreicbxgoelatj24iyqnevm7v4f22utxb75p756ztyu5v7cjxlwti3oa", 36 + "nullified": true, 37 + "createdAt": "1970-01-01T00:00:01.000Z" 38 + }, 39 + { 40 + "did": "did:plc:2s2mvm52ttz6r4hocmrq7x27", 41 + "operation": { 42 + "prev": "bafyreiguwtflhou46pupb3qtemh56x6o3fn3y7ym2q3jgoms35tdu2d5ga", 43 + "type": "plc_operation", 44 + "services": {}, 45 + "alsoKnownAs": [ 46 + "at://foo" 47 + ], 48 + "rotationKeys": [ 49 + "did:key:zQ3shwPdax6jKMbhtzbueGwSjc7RnjsmPcNB1vQUpbKUCN1t1", 50 + "did:key:zQ3shmMcsFLvSZQy2pSZFFKZNmQSNmyuSepPXMHpgTYrontu2" 51 + ], 52 + "verificationMethods": {}, 53 + "sig": "zCcbDgFbv5b0aGggLepLcARzNzH0sG6uuvq2GacyPsAsjDsLMZn6KmOxa50iWZEMRknQHsMYyMTS99eeTPu0fQ" 54 + }, 55 + "cid": "bafyreidef3g2vvlshwzf7vxjyxg3lraahujvhtkpo7y3g4br52ft3lzwem", 56 + "nullified": false, 57 + "createdAt": "1970-01-01T00:00:02.000Z" 58 + } 59 + ]
+33
tests/fixtures/log_tombstone.json
··· 1 + [ 2 + { 3 + "did": "did:plc:6adr3q2labdllanslzhqkqd3", 4 + "operation": { 5 + "sig": "ZznbxHinpBI3NgEYWzXUXLA65s2U1ezJooreZlscHYQRfd4EMlijqhpikGMabs-81Tsy4Mt9Iscpmk7Uz13aAg", 6 + "prev": null, 7 + "type": "plc_operation", 8 + "services": {}, 9 + "alsoKnownAs": [ 10 + "at://op0" 11 + ], 12 + "rotationKeys": [ 13 + "did:key:zQ3shmWf4f6ZwzNyjUDYw4oFQjgHWZoYZDjJYtz75YfYYrphB", 14 + "did:key:zQ3shr2PwdoF6kbuYYymyZq6YbGWShiTXAkdMioCPdSdPL9NV" 15 + ], 16 + "verificationMethods": {} 17 + }, 18 + "cid": "bafyreihqa4o4gsyai22ydms6j4cua6yr6tuc7trq2temvnycadk2daniru", 19 + "nullified": false, 20 + "createdAt": "2025-07-25T15:50:58.440Z" 21 + }, 22 + { 23 + "did": "did:plc:6adr3q2labdllanslzhqkqd3", 24 + "operation": { 25 + "sig": "iKFOLyEujXdHWujAAnwFhKfb9kkIcpMDGOV15eNWqlxLM2GtdQD3lVWCshTKWi0dF7InXGseVFDexS0kg0KmEQ", 26 + "prev": "bafyreihqa4o4gsyai22ydms6j4cua6yr6tuc7trq2temvnycadk2daniru", 27 + "type": "plc_tombstone" 28 + }, 29 + "cid": "bafyreifafe44xylxagcom47xb3lrya5pysu3b7zfjmmmitiuimhmgmerze", 30 + "nullified": false, 31 + "createdAt": "2025-07-25T15:50:58.837Z" 32 + } 33 + ]
+112
tests/fjall_mirror_test.rs
··· 1 + use allegedly::{ 2 + ExperimentalConf, FjallDb, ListenConf, backfill_to_fjall, poll_upstream, serve_fjall, 3 + }; 4 + use reqwest::Url; 5 + use std::time::Duration; 6 + use tokio::sync::mpsc; 7 + 8 + #[tokio::test] 9 + async fn test_fjall_mirror_mode() -> anyhow::Result<()> { 10 + let _ = tracing_subscriber::fmt::try_init(); 11 + 12 + // setup 13 + let temp_dir = tempfile::tempdir()?; 14 + let db_path = temp_dir.path().join("fjall.db"); 15 + let db = FjallDb::open(&db_path)?; 16 + 17 + // backfill (limited to 1 page) 18 + let (backfill_tx, backfill_rx) = mpsc::channel(1); 19 + let (upstream_tx, mut upstream_rx) = mpsc::channel(1); 20 + 21 + // spawn upstream poller 22 + let upstream_url: Url = "https://plc.directory/export".parse()?; 23 + tokio::spawn(async move { 24 + // poll fresh data so our data matches the upstream 25 + let start_at = chrono::Utc::now() - chrono::Duration::try_minutes(5).unwrap(); 26 + let _ = poll_upstream( 27 + Some(start_at), 28 + upstream_url, 29 + Duration::from_millis(100), 30 + upstream_tx, 31 + ) 32 + .await; 33 + }); 34 + 35 + // bridge: take 1 page from upstream and forward to backfill 36 + println!("waiting for page from upstream..."); 37 + let page = upstream_rx 38 + .recv() 39 + .await 40 + .expect("to receive page from upstream"); 41 + println!("received page with {} ops", page.ops.len()); 42 + let sample_did = page.ops.last().unwrap().did.clone(); 43 + 44 + backfill_tx.send(page).await?; 45 + drop(backfill_tx); // close backfill input 46 + 47 + backfill_to_fjall(db.clone(), false, backfill_rx, None).await?; 48 + 49 + // get free port 50 + let listener = std::net::TcpListener::bind("127.0.0.1:17548")?; 51 + let port = listener.local_addr()?.port(); 52 + drop(listener); 53 + 54 + let listen_conf = ListenConf::Bind(([127, 0, 0, 1], port).into()); 55 + let exp_conf = ExperimentalConf { 56 + acme_domain: None, 57 + write_upstream: false, 58 + }; 59 + 60 + let db_for_server = db.clone(); 61 + let server_handle = tokio::spawn(async move { 62 + let upstream: Url = "https://plc.directory".parse().unwrap(); 63 + serve_fjall(upstream, listen_conf, exp_conf, db_for_server).await 64 + }); 65 + 66 + // wait for server to be ready (retry loop) 67 + let client = reqwest::Client::new(); 68 + let base_url = format!("http://127.0.0.1:{}", port); 69 + let mut ready = false; 70 + for _ in 0..50 { 71 + if client 72 + .get(format!("{}/_health", base_url)) 73 + .send() 74 + .await 75 + .is_ok() 76 + { 77 + ready = true; 78 + break; 79 + } 80 + tokio::time::sleep(Duration::from_millis(100)).await; 81 + } 82 + assert!(ready, "server failed to start"); 83 + 84 + // verify health 85 + let resp = client.get(format!("{}/_health", base_url)).send().await?; 86 + assert!(resp.status().is_success()); 87 + let json: serde_json::Value = resp.json().await?; 88 + assert_eq!(json["server"], "allegedly (mirror/fjall)"); 89 + 90 + // verify did resolution against upstream 91 + let upstream_resp = client 92 + .get(format!("https://plc.directory/{}", sample_did)) 93 + .send() 94 + .await?; 95 + assert!(upstream_resp.status().is_success()); 96 + let upstream_doc: serde_json::Value = upstream_resp.json().await?; 97 + 98 + let resp = client 99 + .get(format!("{}/{}", base_url, sample_did)) 100 + .send() 101 + .await?; 102 + assert!(resp.status().is_success()); 103 + let doc: serde_json::Value = resp.json().await?; 104 + assert_eq!( 105 + doc, upstream_doc, 106 + "local doc != upstream doc.\nlocal: {:#?}\nupstream: {:#?}", 107 + doc, upstream_doc 108 + ); 109 + 110 + server_handle.abort(); 111 + Ok(()) 112 + }

History

1 round 0 comments
sign up or login to add to the discussion
ptr.pet submitted #0
18 commits
expand
fjall mirror mode
don't exit the program on reqwest / decode errors, try again instead
fix inclusivity in fjall export ops
add fjall to --no-bulk
increase memtable size as test
refactor apply_op_log to take IntoIterator
fjall: use msgpack for storing DbOps, and store the decoded did bytes
fjall: store serde_json::Value in db instead of storing RawValue
log latest op in mirror mode
fjall: add option to major compact on startup
fjall: parse operation to store types more efficiently
fjall: dont use padded base64url to parse the signatures
fjall: dont propagate parsing error unless its fatal
tune fjall db config, add option to backfill from a local fjall db
fjall: store more types as bsky-optimized variants if possible
fjall: only use 6 bytes of CID in keys
fjall: add missing errors for when op parsing, fallback to inserting into unknown in places where we werent
update from-fjall backfill to use the weekly code by implementing BundleSource
no conflicts, ready to merge
expand 0 comments